Python sandbox / eval escape
When it applies
User input reaches eval(), exec(), a "safe expression" evaluator, a template engine that runs
Python, or a feature that runs user formulas — and a blocklist tries to make it safe by banning
words like import, os, or system. If you can evaluate Python at all, the blocklist is almost
always bypassable.
Why it works
String-based keyword filters remove names, but the objects they'd name are still reachable through
Python's object graph. From any object you can walk __class__ → __base__ → __subclasses__() to
find a class whose module gives you code execution (subprocess.Popen, os.system, builtins),
without ever typing a banned identifier literally.
Method
- Confirm evaluation. Send an arithmetic probe (
7*7→49) or a type leak (().__class__) to prove Python is evaluating your input. - Reach the class tree. From a literal you're allowed:
().__class__.__base__.__subclasses__()— enumerate to find a useful class (look forsubprocess.Popen,os._wrap_close,warnings.catch_warnings→builtins). Index into the list once you know the position. - Execute via the found class, e.g. (index varies per build):
().__class__.__base__.__subclasses__()[N]('id',shell=True,stdout=-1).communicate(). - Defeat banned identifiers without typing them:
- Attribute by string:
getattr(obj,'sys'+'tem'). - Build names from chars:
"__imp"+"ort__",chr(...), or index into strings. - Reach builtins:
().__class__.__base__.__subclasses__()[i].__init__.__globals__['__builtins__'].
- Attribute by string:
- Alternate sinks — if the input is YAML/pickle rather than eval: unsafe
yaml.loadandpickle.loadsexecute__reduce__;str.format/f-string on attacker templates can read object internals ({0.__class__}) → info leak → escalate.
Gotchas
__subclasses__()ordering is not stable across Python versions/builds — enumerate at runtime to find your target's index, don't hardcode it.- Blocklist vs allowlist — a true AST allowlist (only permitted nodes) is far harder; test whether attribute access / subscripting is even parsed before going deep.
evalwith restricted__builtins__still leaks via the class tree — that's the whole point.- This overlaps
web-ssti(Jinja uses the same gadget) — if it's a template, start there.
Verify success
Command output from the host (e.g. id, a file read) returned through the evaluator, proving code
execution past the sandbox — not just a leaked class name.
References
"Python jail escape" / pyjail write-ups; PayloadsAllTheThings (Python sandbox); CWE-94/95.