概述

项目 内容
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 已修复

防御措施

  1. 升级 NGINX 至 1.31.0 或 1.30.1
  2. 审查 nginx.conf,移除不必要的 rewrite + set 组合
  3. 如果暂不能升级,可以临时禁用 rewrite 模块(但会影响业务)
  4. 启用堆保护机制,如 glibc malloc hardening
  5. 容器环境禁用 ASLR 时特别注意,这是 PoC 的前提条件

思维延伸

这个漏洞本质上是一个状态机 race condition——两阶段处理之间,状态没有保持同步。is_args 标志位在计算阶段是 0,拷贝阶段是 1,导致同一段代码走了两个完全不同的分支。

这类漏洞的可怕之处在于:代码逻辑看起来完全正确,却藏着一个18年没人发现的 bug。如果不是 AI 系统主动扫描,很难想象会有人花时间在 rewrite module 里找漏洞。


参考