webfreemedium

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/
file_protocol_abuse_via_adminbothardcoded_xor_key_extractionsecretpickle_format_forgingadminbot_screenshot_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.

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:

  1. Server (server.py): FastAPI app that deserializes incoming requests using secretpickle_load() with a safe_pickle_load decoder. Supports actions: hello, register, login, whoami, encrypt, decrypt, and adminbot.

  2. Client (client.py): Runs in-browser via Pyodide (Python-in-WASM). Parses URL query parameters as YAML, adds localStorage.username and localStorage.password to the payload, sends secretpickle-encoded POST to server. Response HTML goes through DOMPurify 3.4.7.

  3. Adminbot (adminbot.py): Playwright/Chromium bot that:

    • Registers user "admin" with password=FLAG (read from /flag.txt)
    • Logs in (stores credentials in localStorage)
    • Visits whoami to confirm login
    • Visits attacker-supplied URL
    • Takes a screenshot and returns it
  4. SecretPickle format (secretpickle.py): base64(XOR(pickle_bytes[14:], key)) where the XOR key is hardcoded: 77c07f8fd2ae7ad9f5aabc008c79d0d3.

  5. Safe loader (safe_loader.py): Spawns a subprocess, applies seccomp filter (default=KILL, allow only write syscall), then does pickle.loads()json.dumps() → stdout. Parent reads stdout and does json.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