miscfreemedium

build-a-builtin

b01lersc

Task: a Python pyjail forbids literal dots, wipes builtins, and executes attacker input with only a set_builtin helper. Solution: rebuild the primitives step by step, use dotless import syntax to walk Python's object graph, recover os, and leak the randomized flag file through an assertion traceback.

$ ls tags/ techniques/
builtin_reconstructiondotless_attribute_accessobject_graph_traversaltraceback_exfiltration

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

build-a-builtin — b01lers CTF 2026

Description

No separate organizer prompt was included in the provided files; the challenge was distributed through the service source and Dockerfile.

We are given a Python jail that reads one line of code, rejects any input containing a literal dot, clears builtins, and then executes our code with only one exposed helper: set_builtin(key, val). The goal is to escape that restricted environment, locate the randomized flag filename, and print the flag from the remote service.

Challenge Summary

The intended trap is that normal Python code becomes almost unusable after builtins.__dict__.clear(), and the . blacklist appears to kill normal attribute access. But the jail leaves behind exactly the primitive we need: a function that can repopulate builtins with arbitrary objects. By abusing from m import attr as x as a dotless attribute-access gadget, we can bootstrap from basic object metadata to _sitebuiltins._Printer, recover sys, grab os, and finally read /flag-<hex>.txt.

Source Analysis

The challenge code is short:

#!/usr/local/bin/python3 import builtins code = input("code > ") if "." in code: print("Nuh uh") exit(1) def set_builtin(key, val): builtins.__dict__[key] = val exec = exec builtins.__dict__.clear() exec(code, {"set_builtin": set_builtin}, {})

Important observations:

  1. Only the literal . character is blocked. There is no AST filtering, no ban on import, and no restriction on dunder names.
  2. exec is preserved before the wipe. So our payload still executes even after builtins is cleared.
  3. set_builtin survives inside globals. That means we can write arbitrary names back into builtins.__dict__.
  4. The Dockerfile randomizes the flag filename. We cannot hardcode /flag.txt; we must enumerate / and find flag-<32 hex>.txt.
RUN chmod 755 /app/run && \ chmod 444 /flag.txt && \ mv /flag.txt "/flag-$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 32).txt"

...