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 块中时,漏洞会被触发:
rewrite指令的源模式中包含未命名的 PCRE 捕获组(例如^/api/(.*))。- rewrite 替换字符串中包含字面量
?(例如/internal?migrated=true),这会激活 URI 参数编码。 - 后续的
set、if或rewrite指令以$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/Dockerfile、env/docker-compose.yml、 env/nginx.conf、env/entrypoint.sh 和 env/backend.py。
启动环境:
确认这是存在漏洞的环境:
容器启动后,执行以下三项检查以确认漏洞基底正确:
# 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 up、api=200、spray=200、上述提交哈希、personality 标志 00040000,以及空的 /app/marker/ 目录。
在运行中的源代码树中搜索修复内容,可确认 is_args 重置代码不存在,预修复代码路径处于活跃状态。
3. 利用步骤¶
技术概述¶
利用链由两个纯 HTTP 原语组成:
-
堆喷射(POST
/spray) — 触发溢出之前,通过 POST 请求将包含伪造ngx_pool_cleanup_s结构体的二进制载荷注入请求池。每个伪造结构体包含handler = system()(ASLR 已禁用,该 libc 地址固定可知)、data = <命令字符串地址>和next = 0。命令字符串(echo <nonce> > /app/marker/<nonce>)紧随其后出现在同一请求体中。URI 字节无法包含空字节,因此含空终止符的二进制载荷只能通过 POST 请求体传递。 -
溢出(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.sh 和 exploit/poc.py。
第 1 步. 启动环境(见上文)并验证冒烟测试通过。
第 2 步. 生成本次运行的随机 nonce。nonce 在每次运行中唯一;利用程序运行前它不存在于服务器上,运行后出现则证明命令已被执行。
第 3 步. 确认 nonce 在服务器上不存在(基线检查):
第 4 步. 运行利用程序。它完全通过 HTTP 实现:向 nginx 监听器建立普通 TCP 连接,发送 spray POST 请求体,再发送溢出 GET 请求。不使用任何 shell、凭据或直接文件系统访问。
run.sh 构造命令字符串 echo $NONCE > /app/marker/$NONCE,传入 exploit/poc.py,poc.py 在偏移候选值和尝试预算范围内迭代(--max-rounds 8)。成功通常在 1–2 分钟内完成。不要用 timeout 命令包裹(macOS runner 上不存在该命令);脚本自带终止边界。
第 5 步(清理). 操作完成后:
成功的证明¶
成功的证据通过独立的特权渠道观察:通过 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、替换字符串中的 ? 以及后续引用 $N 的 set/if 组合在一起的 location 块。
其他缓解措施¶
在宿主机上启用 ASLR(Linux 默认配置)可将已知公开技术的实际影响限制在 worker 崩溃(DoS)。Worker 进程由 NGINX master 自动重启,因此 DoS 表现为短暂的可用性中断。即便如此,通过单个未经认证的请求即可触发 DoS,对生产系统而言本身就是显著风险;补丁是唯一完整的修复方案。
目前没有任何 WAF 规则能够可靠地阻断触发条件:漏洞触发的 URI 可以完全由可打印 ASCII 字符组成,且三个触发要素位于服务器配置中,而非请求本身。
参考资料¶
- NVD CVE-2026-42945 — CVSS 评分、CWE、发布日期
- GHSA-gcgv-v5gf-c543 — 受影响/已修复版本、安全公告文本
- F5 Security Advisory K000161019 — 厂商公告(需要登录)
- depthfirst — NGINX Rift 技术分析 — 根本原因分析、ASLR 说明、披露时间线
- depthfirst — 完整研究报告 — 堆喷射、
ngx_pool_t损坏、代码执行链 - DepthFirstDisclosures/Nginx-Rift (GitHub) — 权威 PoC(
poc.py、nginx.conf、Dockerfile、entrypoint.sh) - nginx 补丁提交 2046b45aa0c6 — 单行修复:在
ngx_http_script_regex_end_code()中添加e->is_args = 0 - nginx.org 安全公告 — 不受影响:1.31.0+、1.30.1+;受影响:0.6.27–1.30.0
- The Hacker News — 18 年历史的 NGINX Rewrite 模块漏洞 — 背景及利用摘要
- 1dayexploit.com — 详细分析 — 两阶段不匹配详情、ASLR 章节
- Help Net Security — 已在野外被利用 — 主动利用报告