概述
| 项目 | 内容 |
|---|---|
| CVE ID | CVE-2026-42945 |
| 严重程度 | 关键(Critical,CVSS 9.2) |
| 影响版本 | NGINX Open Source 0.6.27 – 1.30.0 |
| 修复版本 | 1.31.0、1.30.1 |
| 漏洞引入时间 | 2008年 |
| 披露时间 | 2026年4月 |
| 发现方式 | AI 自动分析系统发现 |
背景
NGINX 是目前最流行的 Web 服务器,支撑着全球近三分之一的网站。从静态内容服务到反向代理,NGINX 处于现代互联网的关键边缘位置。一个漏洞可以影响无数后端系统。
2026年4月,安全公司 DepthFirst 使用其 AI 分析系统对 NGINX 源码进行扫描,在6小时内发现了4个内存破坏漏洞,其中最严重的就是这个在代码中存在了18年之久的堆溢出。
漏洞原理
两阶段处理机制
NGINX 的 script engine 在处理变量值时,采用了两阶段处理:
第一阶段(计算长度):计算需要多少内存
第二阶段(拷贝数据):真正拷贝数据到缓冲区
这避免了一次请求多次小内存分配,要求两阶段计算结果完全一致。
Root Cause:is_args 标志位状态不一致
漏洞在 src/http/ngx_http_script.c 中。当 rewrite 的 replacement 包含 ? 时,会触发 ngx_http_script_start_args_code,将引擎的 e->is_args = 1 标志位置位:
void
ngx_http_script_start_args_code(ngx_http_script_engine_t *e)
{
e->is_args = 1; // 标志位被置1,且永远不会重置
e->args = e->pos;
e->ip += sizeof(uintptr_t);
}
关键问题在于:这个标志位在两次 pass 之间不会重置。
当后续的 set 指令引用正则捕获组时,触发 ngx_http_script_complex_value_code。在计算长度阶段,使用的是全新零值初始化的 sub-engine:
void
ngx_http_script_complex_value_code(ngx_http_script_engine_t *e)
{
ngx_http_script_engine_t le;
ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); // 全新零值引擎
le.ip = code->lengths->elts;
// le.is_args = 0(因为是零值)
...
}
在拷贝数据阶段,使用的是原始主引擎,e->is_args = 1。
计算长度 vs 拷贝数据的不一致
// ngx_http_script_copy_capture_len_code(长度计算阶段)
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
// 调用 ngx_escape_uri,字符扩成3字节
return cap[n + 1] - cap[n]
+ 2 * ngx_escape_uri(NULL, &p[cap[n]], cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS);
} else {
return cap[n + 1] - cap[n]; // 直接返回原始长度
}
因为 le.is_args = 0,长度计算走了 else 分支,返回原始捕获长度(假设 100 字节)。
但拷贝阶段走了 if 分支:
// ngx_http_script_copy_capture_code(拷贝数据阶段)
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
// ngx_escape_uri 扩增,每个可转义字符 → 3字节
e->pos = (u_char *) ngx_escape_uri(pos, &p[cap[n]],
cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS); // 溢出点
}
+ 被扩成 %2B(3字节),实际写入 300 字节,但缓冲区只分配了 100 字节——堆溢出。
触发条件
一个典型的触发配置:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
当请求 GET /api/AAAAA+AAAAA HTTP/1.1 时:
- rewrite replacement 含
?,is_args被置1 set $original_endpoint $1引用捕获组- 长度计算:raw capture length = 11 字节
- 拷贝数据:
+被扩成 3 字节 → 实际写入 33 字节 - 缓冲区只分配了 11 字节,溢出 22 字节
漏洞利用过程
第一步:Heap Feng Shui(堆布局)
攻击者通过 POST body 向 /spray 发送大量数据,在堆上喷洒精心构造的 ngx_pool_cleanup_s 结构:
fake_struct = struct.pack('<QQQ', SYSTEM_ADDR, data_addr, 0)
cmd_bytes = cmd + b'\x00'
payload = fake_struct + cmd_bytes
第二步:触发溢出
GET /api/[AAAAA... + +++++ ... + target_bytes]
349 字节的 A 填满缓冲区,969 个 + 触发 ngx_escape_uri 扩增 3 倍,冲刷到堆上相邻的 ngx_pool_t->cleanup 指针。
第三步:控制执行流
NGINX 处理完请求后销毁 pool,cleanup 链表被遍历,fake_ngx_pool_cleanup_s.handler 指向 system() 函数:
typedef void (*ngx_pool_cleanup_pt)(void *data);
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // ← 被覆写为 system()
void *data; // ← 指向命令字符串
ngx_pool_cleanup_t *next;
};
pool 销毁时自动调用 system(cmd),实现 RCE。
利用限制
| 限制 | 说明 |
|---|---|
| 需要关闭 ASLR | PoC 使用固定偏移,容器环境用 setarch x86_64 -R 禁用 ASLR |
| 需要 rewrite + set 组合 | 单纯 rewrite 或单纯 set 无法触发 |
| URI 安全字符限制 | 无法直接注入 NULL 字节,指针需要通过 spray 构造 |
| 需要多 requests 配合 | 跨请求 heap feng shui 需要精确控制连接时序 |
修复版本
| 版本 | 状态 |
|---|---|
| NGINX Open Source 1.31.0 | 已修复 |
| NGINX Open Source 1.30.1 | 已修复 |
| NGINX Plus R36 P4 | 已修复 |
| NGINX Plus R35 P2 | 已修复 |
| NGINX Plus R32 P6 | 已修复 |
防御措施
- 升级 NGINX 至 1.31.0 或 1.30.1
- 审查 nginx.conf,移除不必要的
rewrite+set组合 - 如果暂不能升级,可以临时禁用 rewrite 模块(但会影响业务)
- 启用堆保护机制,如 glibc malloc hardening
- 容器环境禁用 ASLR 时特别注意,这是 PoC 的前提条件
思维延伸
这个漏洞本质上是一个状态机 race condition——两阶段处理之间,状态没有保持同步。is_args 标志位在计算阶段是 0,拷贝阶段是 1,导致同一段代码走了两个完全不同的分支。
这类漏洞的可怕之处在于:代码逻辑看起来完全正确,却藏着一个18年没人发现的 bug。如果不是 AI 系统主动扫描,很难想象会有人花时间在 rewrite module 里找漏洞。
参考
- GitHub: https://github.com/DepthFirstDisclosures/Nginx-Rift
- 技术分析:https://depthfirst.com/research/nginx-rift-achieving-nginx-rce-via-an-18-year-old-vulnerability
- F5 官方通告:https://my.f5.com/manage/s/article/K000160932