Secure Secretpickle
gpnctf
Task: Custom 'secretpickle' serialization (XOR + pickle) with seccomp-sandboxed server-side deserialization, Pyodide client, and Playwright adminbot that stores flag as admin password. Solution: Bypass all pickle/seccomp complexity by sending file:///flag.txt URL to adminbot, which renders the flag in Chromium and returns a screenshot.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Secure Secretpickle — GPNCTF 2025
Description
The only serialization method that I found in the restaurant were Pickles. So I made an encrypted (and secure) version of it that nobody can crack or pwn!
A multi-component web challenge with a custom "secretpickle" serialization format (XOR encryption + pickle), a seccomp-sandboxed server, a Pyodide-based browser client, and a Playwright adminbot. The flag is stored as the admin user's password in the same container.
Analysis
Architecture Overview
The challenge has four main components:
-
Server (
server.py): FastAPI app that deserializes incoming requests usingsecretpickle_load()with asafe_pickle_loaddecoder. Supports actions:hello,register,login,whoami,encrypt,decrypt, andadminbot. -
Client (
client.py): Runs in-browser via Pyodide (Python-in-WASM). Parses URL query parameters as YAML, addslocalStorage.usernameandlocalStorage.passwordto the payload, sends secretpickle-encoded POST to server. Response HTML goes through DOMPurify 3.4.7. -
Adminbot (
adminbot.py): Playwright/Chromium bot that:- Registers user "admin" with
password=FLAG(read from/flag.txt) - Logs in (stores credentials in localStorage)
- Visits
whoamito confirm login - Visits attacker-supplied URL
- Takes a screenshot and returns it
- Registers user "admin" with
-
SecretPickle format (
secretpickle.py):base64(XOR(pickle_bytes[14:], key))where the XOR key is hardcoded:77c07f8fd2ae7ad9f5aabc008c79d0d3. -
Safe loader (
safe_loader.py): Spawns a subprocess, applies seccomp filter (default=KILL, allow onlywritesyscall), then doespickle.loads()→json.dumps()→ stdout. Parent reads stdout and doesjson.loads().
Key Source Code
secretpickle.py — hardcoded XOR key:
SECRETPICKLE_OBJECT_PREFIX = bytes.fromhex("8004 950000000000000000 7d 94 28") SECRETPICKLE_XOR_KEY = bytes.fromhex("77c07f8fd2ae7ad9f5aabc008c79d0d3") def secretpickle_dump(decoded, encoder=pickle.dumps): raw = encoder(decoded) trimmed = raw[len(SECRETPICKLE_OBJECT_PREFIX):] xored = secretpickle_encrypt(trimmed) encoded = base64.b64encode(xored).decode() return encoded
adminbot.py — reads flag and visits arbitrary URLs:
FLAG = open("/flag.txt").read().strip() async def visit(url): # ... registers admin with password=FLAG, logs in ... await page.goto(url) # visits attacker-supplied URL screenshot = await page.screenshot(full_page=True) return screenshot
...
$ grep --similar
Similar writeups
- [web][free]SecretPickle— gpnctf
- [web][Pro]Lab 13 — WebForge — Insecure Deserialization in Config Import— hackadvisor
- [web][free]cookoff— gpnctf
- [web][Pro]Photo Storage— miptctf
- [web][Pro]Lab 379 — CrawlBase — Stored XSS to SSRF to Pickle Deserialization RCE— hackadvisor