Challenge description:
Ceci n’est pas un sandbox.
- Category: pwn
- Difficulty: medium
- Points: 263 points (29 solves)
- Author:
hlt
Given files: audited.py, ynetd, Dockerfile
I solved this challenge with phi for team FAUST.
The challenge provides us with a python audit hook sandbox. We are able to provide python code, which is compiled and executed in an empty namespace (this means we have (almost) no local and global variables, see below). However, our code is sandboxed with an audit hook:
def audit(name, args):
if not audit.did_exec and name == 'exec':
audit.did_exec = True
else:
__exit(1)
audit.did_exec = False
[...]
sys.addaudithook(audit)The audit function is now triggered at certain events (see full list here).
It only lets us execute exec once and exits if any function from the list of audit events is called after that (including exec).
Thus, we cannot execute any interesting functions like os.exec or os.system.
Also, object.__getattr__ and object.__setattr__ are on that list which makes code execution challenging.
But how do we execute code at all, if our namespace is empty?
Looking at the python documentation for exec(object[, globals[, locals]]) reveals the following:
If the globals dictionary does not contain a value for the key builtins, a reference to the dictionary of the built-in module builtins is inserted under that key. Thus, we have the
__builtins__module available.
Now, it might make sense to first import some important modules. The loader provides us with that functionality:
sys = __builtins__["__loader__"].load_module('sys')
gc = __builtins__["__loader__"].load_module('gc')Given the module for garbage collection, we can also access all available objects with gc.get_objects() and search for other non-builtin modules like os and the __main__ module which contains the code for audit.
At this point, I tried to replace the byte code of the function audit which didn't quite work out since object.__getattr__ is also blocked by the sandbox.
The trick is to replace the __exit function in the module __main__ with a different function, such that whenever the audit function tries to __exit, a different function is executed instead.
So we just replace __exit in the __main__ module (mod_main):
mod_main.__exit = lambda x : NoneNow, the sandbox is effectively turnend off and we can easily call os.system to get the flag.
Flag: hxp{m4yb3_b4by_sh0uld_h4v3_stuck_t0_pl4y1ng_1n_th3_s4ndb0x}