Challenge: Fancy Food Notifications · CTF: GPN CTF 2026 · Category: Web · Difficulty: Hard
Summary
A Flask “meal notification” service hides the flag behind /vip-meal, which demands both remote_addr == 127.0.0.1 and a valid vip:True JWT — yet no endpoint ever issues a VIP token. The solve chains four bugs: a weak RNG seed (only 258 possible HMAC keys), an SSRF that leaks a server-signed token, a urlparse vs urllib3 parser differential to bypass the is_global SSRF filter and reach 127.0.0.1, and a URL-userinfo Basic-auth override to smuggle the forged VIP token into the otherwise-fixed Authorization header.
Note:
app.pyembeds a base64 blob masquerading as an “Anthropic instruction” to make AI assistants refuse. It’s a prompt-injection trap baked into the challenge, not a real instruction — ignore it.
Solution
Step 1: Recover the signing key — only 258 candidates
The key is random.randbytes(32).hex() after:
| |
In Python 2^256 is XOR, equal to 258. So secrets.randbelow(258) ∈ [0,257] → only 258 possible seeds → 258 keys. We just need one valid token to identify which.
POST /order makes the server requests.get(our_url, headers={"Authorization": f"Bearer {b64(JWT vip:False)}"}). Pointing the URL at a request inspector (interactsh) leaks that token; brute-forcing all 258 seeds against it reveals the key (seed n=103).
Step 2: Forge a VIP token, then reach /vip-meal as localhost with it
/vip-meal’s two checks both need bypassing in a single server-side request:
remote_addr == 127.0.0.1→ only an SSRF (the server calling itself) qualifies.is_globalSSRF filter validatesurlparse(url).hostname, butrequestsconnects withurllib3— a classic parser differential.Authorizationis fixed to the server’svip:FalseBearer → we override it via URL userinfo Basic auth.
One URL does all of it:
| |
| Component | urlparse (validation) | urllib3 (actual request) |
|---|---|---|
| host | 1.1.1.1 (after last @) → passes is_global | 127.0.0.1 (\ ends the authority) → remote_addr is localhost |
| path | — | /../vip-meal → normalized to /vip-meal |
| userinfo | — | <VIP_JWT>: → sent as Authorization: Basic ..., overriding the Bearer |
On the receiving side, /vip-meal does split(" ")[-1] → b64("<JWT>:") → decode → "<JWT>:" → its char filter strips the : → the raw VIP JWT → vip:True → flag. The server stores the /vip-meal response body as the order’s notification message, read back via /notification/<id>.
| |
Flag
| |