miscfreehard

bctf-infra

b01lersc

Task: three Python sandboxes (pyjails) with increasingly restrictive whitelists served under nsjail, each running as a separate UID; chal3's whitelist is whitespace-only and unsolvable in isolation. Solution: escape the weakest sandbox (chal1) via octal-escaped string construction into __builtins__.__dict__, then abuse the fact that nsjail keeps CAP_SETUID/CAP_SETGID and maps every challenge UID, so os.setuid() pivots across tenants to read chal3's flag.

$ ls tags/ techniques/
octal_string_constructionbuiltins_dict_accesssetuid_via_cap_setuidcross_user_flag_read

$ cat /etc/rate-limit

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

bctf-infra — b01lersCTF 2026

Description

I got access to the b01lersCTF backend infrastructure, can you take a look and see what you can find?

The remote endpoint is a TLS socket:

ncat --ssl bctf-infra.opus4-7.b01le.rs 8443

On connect the server lists three sub-challenges and asks which one to run:

Challenges:
chal3
chal1
chal2
> 

Each sub-challenge is a Python pyjail of the form exec(input()) guarded by a per-challenge character whitelist. The interesting twist is that chal3's whitelist is literally string.whitespace — no alphanumerics, no punctuation, not even a single letter. Taken on its own, chal3 is unsolvable; the challenge is an infrastructure problem, not a pyjail problem.

Analysis

The three sandboxes

All three challenges use the same skeleton:

# chals/chalN/chal.py inp = input("> ") for c in inp: if c not in allowed_chars: print(f"Illegal char {c}") exit() exec(inp)

Only the whitelist differs:

ChallengeAllowed charactersNotable restrictions
chal1string.ascii_lowercase + string.punctuation + string.digits with e and o removedNo whitespace, no uppercase, no e/o
chal2string.ascii_lowercase + "._[]; "No digits, no parens, no quotes, no =, no :
chal3string.whitespace onlyEffectively impossible to form any payload

Flags are located at /app/chals/chalN/flag.txt. Each folder is owned by the matching chalN user (UID 65001/65002/65003) and is chmod 700 (see the Dockerfile loop below). On the live deployment only chal3's flag.txt contains the real flag; chal1 and chal2 return fake{fake_flag}.

Server-side user separation

app/challenge_server.py listens on 127.0.0.1:1337 and spawns a multiprocessing.Process per connection. Inside that process, before executing the challenge, it drops privileges to the matching UID:

...

$ grep --similar

Similar writeups