Challenge: SecretPickle · CTF: GPN CTF 2026 · Category: Web · Difficulty: Easy
Summary
The site ships a Pyodide client that serializes requests with “secretpickle” — plain pickle obfuscated by a hardcoded XOR key and a fixed prefix. The server pickle.loads() attacker data, giving unauthenticated root RCE. The flag isn’t on the web pod; it’s the adminbot’s admin password, which the client sends to the server in plaintext on login — captured by hooking the live request handler.
Solution
Step 1: Forge a pickle for RCE
secretpickle_dump = pickle.dumps → trim a fixed prefix → XOR with a hardcoded 128-bit key (77c0...d0d3) → base64. The server route POST /{b64} runs secretpickle_load → pickle.loads. Since the key/prefix are public, we reuse the exact functions to forge any pickle. Embedding an object with __reduce__ as params.name and using the hello action (which reflects params.get('name')) gives us command output back. RCE confirmed as uid=0(root).
Step 2: Locate the flag → hook the handler
There’s no flag file on the web pod. adminbot.py reads /flag.txt and registers/logs in as admin with password = FLAG. Passwords are stored only as SHA-256 hashes, but the login request itself carries the password in cleartext. So we use exec-based RCE to monkeypatch the module-global async action_handler (FastAPI resolves it per request — no restart needed) to log every incoming payload, then trigger the adminbot action. The bot logs in and its plaintext password lands in our log.
| |
Output:
| |
Flag
| |