跳转至

CVE-2026-42945 — NGINX Rift:堆缓冲区溢出导致远程代码执行

发布于 2026-06-04

前提条件:ASLR 已禁用

本页演示的远程代码执行要求目标 NGINX worker 进程在禁用地址空间布局随机化(ASLR)的环境下运行。在默认 Linux 设置(启用 ASLR)下,同一漏洞只会导致 worker 崩溃并重启(DoS)。目前尚无针对启用 ASLR 目标的公开 RCE 利用技术。无论 ASLR 状态如何,缓解措施请参阅安全建议

字段 详情
漏洞编号 CVE-2026-42945 / GHSA-gcgv-v5gf-c543
项目 NGINX
受影响组件 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 基于堆的缓冲区溢出
影响 未经认证的 RCE(ASLR 关闭)· Worker 崩溃 / DoS(ASLR 开启)
受影响版本 NGINX OSS 0.6.27–1.30.0 · NGINX Plus R32–R36
已修复版本 NGINX OSS 1.30.1(稳定版)/ 1.31.0(主线版)· NGINX Plus R36 P4、R35 P2、R32 P6

1. 漏洞概述

NGINX 是使用最广泛的 HTTP 服务器和反向代理之一。CVE-2026-42945("NGINX Rift")是其重写脚本引擎中的一个堆缓冲区溢出漏洞,该代码路径自 0.6.27(2008 年)起便已存在,影响 1.30.0 之前的所有版本。

当以下三个配置要素同时出现在同一个 location 块中时,漏洞会被触发:

  1. rewrite 指令的源模式中包含未命名的 PCRE 捕获组(例如 ^/api/(.*))。
  2. rewrite 替换字符串中包含字面量 ?(例如 /internal?migrated=true),这会激活 URI 参数编码。
  3. 后续的 setifrewrite 指令以 $1$2 等形式引用了该捕获组。

向满足上述条件的 location 发送一个构造好的 HTTP 请求,即可触发溢出并破坏相邻堆内存,通常导致 NGINX worker 进程崩溃。禁用 ASLR 时,堆布局是确定性的,攻击者无需身份认证、无需预知目标配置即可实现任意命令执行。

根本原因

文件: src/http/ngx_http_script.c 函数: ngx_http_script_regex_end_code()

NGINX 的重写脚本引擎以两个阶段处理指令:先进行长度计算以确定所需的缓冲区大小,再进行分配与复制以填充已分配的缓冲区。漏洞源于状态标志 is_args:当替换字符串中出现 ? 时,该标志被置为 1,表示需要应用 URI 参数编码,但在两个阶段之间从未被重置。

两个阶段使用不同的引擎实例。长度计算阶段在全新的子引擎 le 上运行,此时 le.is_args == 0,返回捕获字符串的原始未转义字节数。复制阶段在主引擎 e 上运行,此时 e.is_args == 1,因此调用 ngx_escape_uri(buf, capture, len, NGX_ESCAPE_ARGS),将每个 URI 特殊字节(如 +%&)从 1 字节扩展为 3 字节的百分比编码形式。分配的缓冲区容纳不下转义后的输出,攻击者控制的 URI 字节使其溢出。

修复方案是提交 2046b45aa0c6e712c216b9075886f3f26e9b4ca9 添加的一行代码:

// src/http/ngx_http_script.c  —  ngx_http_script_regex_end_code()

     r = e->request;

+    e->is_args = 0;      /* ← the one-line fix */
     e->quote = 0;

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

在正则表达式结束处理器完成后立即将 e->is_args 重置为 0,使两个阶段看到相同的标志值,从而对缓冲区大小保持一致。

触发该漏洞所需的最简 nginx.conf 配置:

location ~ ^/api/(.*)$ {
    rewrite ^/api/(.*)$ /internal?migrated=true;  # sets is_args=1
    set $original_endpoint $1;                    # copy pass expands $1
}

2. 漏洞环境

复现环境运行于单个 Docker 容器中。容器从上游 git 仓库的预修复提交 98fc3bb78e8daef25c3d850c9cba8c2f787fb99e(官方 PoC 的固定版本)构建 NGINX,不对源代码做任何修改。在该提交处,NGINX 版本字符串自报为 1.31.0,因为开发版本号在修复落地之前已被提升;正确的参照是 git 提交哈希,与 PoC 的固定版本完全一致。

容器以 linux/amd64 方式运行(在 ARM 主机上通过 Docker Desktop QEMU 模拟),原因是 PoC 硬编码了 x86-64 地址。通过入口点中的 setarch x86_64 -R 禁用 ASLR,以单个 NGINX worker(worker_processes 1)启动,使堆地址和 system() 地址在重启之间保持确定性。容器内循环接口上运行一个小型 Python 后端(backend.py),它保持上游连接开放,使 spray POST 请求体得以在请求池中保留;该后端无法从宿主机直接访问。

网络拓扑:

服务 职责 是否可从宿主机访问
NGINX worker(提交 98fc3bb78 存在漏洞的 HTTP 监听器——触发端点和 spray 端点 http://127.0.0.1:19321/
backend.py /internal/spray 的代理后端 否(仅限容器内部)

下载环境文件——(env.zip)——其中包含 env/Dockerfileenv/docker-compose.ymlenv/nginx.confenv/entrypoint.shenv/backend.py

启动环境:

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

确认这是存在漏洞的环境:

容器启动后,执行以下三项检查以确认漏洞基底正确:

# 1. HTTP 服务器正常运行,两个攻击面端点均可响应:
curl -s http://127.0.0.1:19321/ | grep -q ok && echo "nginx up"
curl -s -o /dev/null -w "api=%{http_code}\n"  "http://127.0.0.1:19321/api/x" -H 'X-Delay: 0'
curl -s -o /dev/null -w "spray=%{http_code}\n" -X POST --data x http://127.0.0.1:19321/spray -H 'X-Delay: 0'

# 2. 固定的预修复提交且 ASLR 已禁用:
docker exec cve-2026-42945-nginx git -C /nginx-src rev-parse HEAD
# 预期:98fc3bb78e8daef25c3d850c9cba8c2f787fb99e

docker exec cve-2026-42945-nginx bash -c 'cat /proc/$(pgrep nginx|sort -n|tail -1)/personality'
# 预期:00040000(ADDR_NO_RANDOMIZE)

# 3. marker 目录为空——无预先植入的内容:
docker exec cve-2026-42945-nginx ls -A /app/marker/
# 预期:(空)

预期输出:nginx upapi=200spray=200、上述提交哈希、personality 标志 00040000,以及空的 /app/marker/ 目录。

在运行中的源代码树中搜索修复内容,可确认 is_args 重置代码不存在,预修复代码路径处于活跃状态。


3. 利用步骤

技术概述

利用链由两个纯 HTTP 原语组成:

  1. 堆喷射(POST /spray — 触发溢出之前,通过 POST 请求将包含伪造 ngx_pool_cleanup_s 结构体的二进制载荷注入请求池。每个伪造结构体包含 handler = system()(ASLR 已禁用,该 libc 地址固定可知)、data = <命令字符串地址>next = 0。命令字符串(echo <nonce> > /app/marker/<nonce>)紧随其后出现在同一请求体中。URI 字节无法包含空字节,因此含空终止符的二进制载荷只能通过 POST 请求体传递。

  2. 溢出(GET /api/... — 一个构造好的 GET 请求触发长度/复制不匹配。URI 依次编码了 349 个填充字节(A)、969 个可 URI 转义的字节(+)以及 6 字节的目标地址。在复制阶段,每个 + 扩展为 %2B(1 字节 → 3 字节),产生约 2000 字节的溢出,覆盖相邻的 ngx_pool_t。溢出只覆盖相邻池 cleanup 指针的低 6 字节,将其重定向到已喷射的伪造结构体。该池被销毁(ngx_destroy_pool)时,NGINX 遍历清理链表并调用 handler(data),在 worker 进程内执行 system("echo <nonce> > /app/marker/<nonce>")

利用程序遍历 20 个预先计算的堆偏移候选值,每个候选值最多尝试 10 次(共 8 轮),每次尝试之间通过并发的 keep-alive 连接检测崩溃。其概率性来自堆喷射:伪造结构体必须恰好落在被覆盖指针所指向的偏移处。

分步操作

下载利用文件——(exploit.zip)——其中包含 exploit/run.shexploit/poc.py

第 1 步. 启动环境(见上文)并验证冒烟测试通过。

第 2 步. 生成本次运行的随机 nonce。nonce 在每次运行中唯一;利用程序运行前它不存在于服务器上,运行后出现则证明命令已被执行。

NONCE="rift_$(openssl rand -hex 6)"

第 3 步. 确认 nonce 在服务器上不存在(基线检查):

docker exec cve-2026-42945-nginx ls /app/marker/"$NONCE"
# 预期:No such file or directory

第 4 步. 运行利用程序。它完全通过 HTTP 实现:向 nginx 监听器建立普通 TCP 连接,发送 spray POST 请求体,再发送溢出 GET 请求。不使用任何 shell、凭据或直接文件系统访问。

bash exploit/run.sh "$NONCE" 127.0.0.1 19321

run.sh 构造命令字符串 echo $NONCE > /app/marker/$NONCE,传入 exploit/poc.py,poc.py 在偏移候选值和尝试预算范围内迭代(--max-rounds 8)。成功通常在 1–2 分钟内完成。不要用 timeout 命令包裹(macOS runner 上不存在该命令);脚本自带终止边界。

第 5 步(清理). 操作完成后:

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

成功的证明

成功的证据通过独立的特权渠道观察:通过 docker exec 进入运行中的容器,直接读取服务器文件系统。利用程序本身从不接触该渠道,其自身的 stdout 不是判断信号。

以下三项子检查必须全部通过:

# (a) marker 文件现在存在(基线时不存在):
docker exec cve-2026-42945-nginx ls -A /app/marker/
# -> rift_33a425b86a2d

# (b) 文件内容与 nonce 完全一致——逐字节匹配:
docker exec cve-2026-42945-nginx cat /app/marker/rift_33a425b86a2d
# -> rift_33a425b86a2d

# (c) 文件由 uid 65534(nobody)拥有——即 NGINX worker 的进程上下文:
docker exec cve-2026-42945-nginx stat -c 'uid=%u gid=%g %n' /app/marker/rift_33a425b86a2d
# -> uid=65534 gid=65534 /app/marker/rift_33a425b86a2d

本次运行中,以 nonce rift_33a425b86a2d 完成了上述三项检查。marker 在基线时不存在,仅在 exploit/run.sh 执行完毕后出现。Worker 的 uid(65534,nobody)是执行 system() 的进程身份,而非宿主机侧或 root 的产物。nonce 由验证方生成,除 worker 执行命令字符串外,它没有其他途径到达服务器,因此这是在正确权限级别上的明确服务器端状态变更。

NGINX 错误日志也独立记录了溢出的触发情况:

[alert] 7#0: worker process 661 exited on signal 11 (core dumped)
[notice] 7#0: start worker process 663
[alert] 7#0: worker process 663 exited on signal 11 (core dumped)
[notice] 7#0: start worker process 665

反复出现的 SIGSEGV + core dump + master 重新拉起,正是溢出到达并重定向清理指针的预期特征。其中一次重定向尝试落在已喷射的结构体上,调用了 system(),写入了 marker。Worker 的 pid 从基线值 11 推进到了 665。

该效果与漏洞代码路径直接绑定:运行中的构建版本中不存在 is_args 重置修复(已在环境设置时确认),marker 只能通过该缺失重置所触发的 ngx_destroy_pool → 清理处理器 → system() 调用链写入。


4. 安全建议

修复措施

升级到已修复的版本。单行修复(在 ngx_http_script_regex_end_code() 中添加 e->is_args = 0)已包含于:

  • NGINX Open Source: 1.30.1(稳定分支)或 1.31.0(主线版)
  • NGINX Plus: R36 P4、R35 P2 或 R32 P6

所有 NGINX OSS 0.6.27 至 1.30.0 版本,以及未应用相关补丁的 NGINX Plus R32 至 R36 版本,至少存在未经认证的 HTTP 请求导致 worker 崩溃的 DoS 风险。DoS 无需任何特殊条件;RCE 需要 ASLR 已禁用(或存在独立的 ASLR 绕过)。

临时缓解措施(若无法立即升级)

rewrite 指令中的未命名 PCRE 捕获组改为命名捕获组,可阻断三要素触发条件同时成立:

# 存在漏洞的配置:
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;

# 使用命名捕获组的安全等效配置:
rewrite ^/api/(?P<ep>.*)$ /internal?migrated=true;
set $original_endpoint $ep;

命名捕获通过 $ep(捕获名称)而非 $1/$2 引用,复制阶段不会查找位置捕获,溢出因此无法触发。该临时措施有效,但需要审计所有将未命名捕获的 rewrite、替换字符串中的 ? 以及后续引用 $Nset/if 组合在一起的 location 块。

其他缓解措施

在宿主机上启用 ASLR(Linux 默认配置)可将已知公开技术的实际影响限制在 worker 崩溃(DoS)。Worker 进程由 NGINX master 自动重启,因此 DoS 表现为短暂的可用性中断。即便如此,通过单个未经认证的请求即可触发 DoS,对生产系统而言本身就是显著风险;补丁是唯一完整的修复方案。

目前没有任何 WAF 规则能够可靠地阻断触发条件:漏洞触发的 URI 可以完全由可打印 ASCII 字符组成,且三个触发要素位于服务器配置中,而非请求本身。

参考资料