
NGINX ngx_http_rewrite_module 中的堆缓冲区溢出
| 指标 | 值 |
|---|---|
| CVSS v4.0 | 9.2(严重) |
| CVSS v3.1 | 8.1(高危) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | 122 — 基于堆的缓冲区溢出 |
| 引入时间 | 2008年6月 — v0.6.27 |
| 发现时间 | 2026年4月 — DepthFirst Research |
| 修复时间 | 2026年5月13日 — v1.30.1, v1.31.0 |
| CVE 发布时间 | 2026年5月21日 |
| 存在周期 | 约18年(未被发现) |
| 修复提交 | 524977e7c534e87e5b55739fa74601c9f1102686 |
未经认证的远程攻击者可以通过向具有特定 rewrite + set/if/rewrite 配置模式的服务器发送特制 HTTP 请求,在 NGINX worker 进程中触发确定性的堆缓冲区溢出。该溢出会破坏堆元数据(ngx_pool_cleanup_t 指针),从而通过堆喷(Heap Spray)和堆风水(Feng Shui)技术实现远程代码执行(RCE)。
server { listen 19321;
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
}
**关键要求:**
- 一个 `rewrite` 指令,其替换内容中含有 `?`(查询字符串分隔符)
- 后续的 `set`、`if` 或 `rewrite` 指令引用了**未命名的 PCRE 捕获组**(`$1`、`$2` 等)
- 重写替换内容中的 `?` 会触发 `ngx_http_script_start_args_code`,从而设置 `e->is_args = 1`
### 攻击者可以实现什么
| 能力 | 描述 |
|-----------|-------------|
| **拒绝服务** | 确定性地使工作进程崩溃,导致进程循环重启(无论是否启用 ASLR 都有效) |
| **远程代码执行** | 在 ASLR 被禁用(或通过部分覆盖绕过)的情况下,以 nginx 用户身份实现完整的 RCE |
| **数据泄露** | 通过内存读取原语,从工作进程堆中提取敏感数据 |
| **持久化** | 通过在工作进程内存中执行代码植入后门 |
---
## 2. 根本原因分析
### 两遍脚本引擎
NGINX 的 `ngx_http_rewrite_module` 在 `src/http/ngx_http_script.c` 中使用**两遍脚本引擎**:
1. **长度遍历** (`ngx_http_script_run`):遍历所有脚本代码以计算所需的总缓冲区大小。将长度写入 `le.ip` 和 `le.pos`。
2. **复制遍历** (`ngx_http_script_copy_len`/`_code`):再次遍历,将实际字节写入 `e->ip` 和 `e->pos` 处的预分配缓冲区。
每个脚本代码都有两个处理函数:每个遍历对应一个。例如:
- `ngx_http_script_copy_len` → `ngx_http_script_copy_code`
- `ngx_http_script_start_args_len` → `ngx_http_script_start_args_code`
### `is_args` 标志
**引擎结构** (`ngx_http_script_engine_t`) 上的标志 `e->is_args` 控制复制遍历如何处理某些字符:```c
typedef struct {
u_char *ip;
u_char *pos;
ngx_http_variable_value_t *sp;
ngx_str_t buf;
int flushed;
unsigned is_args:1; // <-- THE BUG
unsigned ncaptures:1;
ngx_uint_t captures_size;
// ...
} ngx_http_script_engine_t;
当 e->is_args = 1 时,$N 捕获引用的复制代码调用 ngx_escape_uri() 并传入 NGX_ESCAPE_ARGS,该函数会展开:
+ → %2B(1 字节 → 3 字节,+200%)% → %25(1 字节 → 3 字节,+200%)& → %26(1 字节 → 3 字节,+200%)漏洞模式的执行流程:``` rewrite ^/api/(.*)$ /internal?migrated=true;
1. 在**重写评估**期间,引擎在替换字符串中遇到 `?`,这会触发 `ngx_http_script_start_args_code`,设置 `e->is_args = 1`。
2. 重写会修改请求 URI,然后继续执行下一条指令。
3. **`e->is_args` 永远不会被清除**。
然后:```
set $original_endpoint $1;
le): ```c
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
这会正确地将 le.is_args 归零为 0,因此长度传递返回的是原始、未转义的捕获长度。
e,而 e 在步骤 1 中仍具有 e->is_args = 1。复制传递应用 URI 转义,将每个可转义字符从 1 字节扩展为 3 字节,而缓冲区的大小却按原始长度分配——堆溢出。Pass 1 (Length — sub-engine le): le.is_args = 0 capture $1 = "A+++++B" → length = 7
Buffer allocated: 7 bytes
Pass 2 (Copy — main engine e): e.is_args = 1 ← LEAKED from rewrite capture $1 = "A+++++B" ngx_escape_uri("A+++++B", NGX_ESCAPE_ARGS): A → A (1 byte) + → %2B (3 bytes) ← EXPANSION + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) B → B (1 byte) total written: 17 bytes buffer size: 7 bytes OVERFLOW: 10 bytes
扩展比例为 `7 + (n_escapable * 2)`,其中 `n_escapable` 是捕获中 `+`、`%` 和 `&` 的数量。
---
## 3. 利用机制
### 概述
| 步骤 | 技术 | 描述 |
|------|-----------|-------------|
| 1 | 溢出 | 发送带有 `+` 填充的精心构造的 URI,以溢出堆缓冲区 |
| 2 | 堆喷射 | 向 `/spray` POST 大体积请求体,用受控数据填充堆 |
| 3 | 堆风水 | 精心安排分配,使溢出目标(`ngx_pool_cleanup_t`)位于相邻位置 |
| 4 | 破坏处理器 | 溢出用 `system()` 地址覆盖 `ngx_pool_cleanup_t.handler` |
| 5 | 触发清理 | 等待池销毁 → `system(cmd)` 执行攻击者命令 |
| 6 | 反向 Shell | 串联反向 Shell 载荷以获得交互式访问 |
### 跨请求堆风水
**单请求堆风水会失败**,因为溢出在到达 `cleanup` 指针之前就破坏了池的元数据(`->d.next`、`->d.failed`)。当请求结束时池被销毁,损坏的元数据会导致**在调用 `system()` 之前崩溃**。
相反,该漏洞利用采用**跨请求堆风水**:
1. **请求 1(喷射)**:向 `/spray` POST 大体积请求体。后端(`server.py`)使用 `X-Delay` 响应头挂起响应,保持连接打开,从而保留堆分配。喷射用伪造的 `ngx_pool_cleanup_t` 块填充堆。
2. **请求 2(溢出)**:发送溢出 URI。溢出仅损坏 `cleanup` 指针(而非池元数据),使其指向喷射的伪造块。
3. **池销毁**:当喷射响应完成(延迟到期)时,池的清理链会遍历到伪造块并调用 `system(cmd)`。
### 地址要求
| 符号 | 数值(Docker,ASLR 关闭) | 描述 |
|--------|--------------------------|-------------|
| `HEAP_BASE` | `0x555555659000` | nginx 堆的基址 |
| `system@libc` | `0x7ffff6f6e420` | glibc 中的 `system()` |
| `NGX_CYCLES_POOL` | `0x5555556a4040` | 指向 cycles 池的指针 |
| 伪造清理地址 | `0x5555556a4030` | 喷射目标地址 |
### ASLR 绕过
即使不禁用 ASLR,**DoS**(崩溃)仍能确定性触发。对于启用 ASLR 的 RCE,有两种方法:
1. **部分覆盖**:使用 1 字节或 2 字节覆盖,在同一页内移动指针,并暴力破解剩余的半字节(16–256 次尝试)。
2. **信息泄露**:读取 `/proc/self/maps`,或使用 `log_parser.py` 内存分析来确定内存布局。
---
## 4. 修复分析
### 官方修复
**提交**:`524977e7c534e87e5b55739fa74601c9f1102686`
**文件**:`src/http/ngx_http_script.c`
**行**:~1205(位于 `ngx_http_script_regex_end_code` 中)```diff
void
ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)
{
ngx_http_script_regex_code_t *code;
code = (ngx_http_script_regex_code_t *) e->ip;
+ e->is_args = 0; /* ← THE FIX */
e->ip += sizeof(ngx_http_script_regex_code_t);
// ...
}
ngx_http_script_regex_end_code 在长度传递和复制传递期间每次正则表达式求值之后运行。在此处重置 e->is_args = 0 可确保:
set、if、rewrite)以干净的 is_args = 0 开始ngx_http_script_start_args_code 在替换字符串中遇到 ? 时仍然可以设置 is_args = 1 —— 该修复不会破坏这一功能patches/0002-hardening-bounds-check.patch 在 ngx_http_script_copy_capture_code 中添加了边界检查:```c
if (e->pos + len > e->buf.data + e->buf.len) {
return; /* gracefully truncate instead of overflowing */
}
### 向后移植补丁
| 补丁 | Nginx 版本 |
|-------|---------------|
| `patches/0001-fix-is_args.patch` | 1.22.x, 1.24.x, 1.26.x, 1.30.0 |
| `patches/backport-1.22.x.patch` | 1.22.0–1.22.1 |
| `patches/backport-1.24.x.patch` | 1.24.0–1.24.1 |
| `patches/backport-1.26.x.patch` | 1.26.0–1.26.1 |
---
## 5. 受影响版本
### NGINX 开源版
| 范围 | 状态 |
|-------|--------|
| **0.1.0 – 0.6.26** | 不受影响(重写模块早于未命名捕获) |
| **0.6.27 – 1.30.0** | **易受影响**(18 年窗口期) |
| **1.30.1** | 首个修复版本 |
| **1.31.0+** | 已修复(主线版) |
### NGINX Plus
| 版本 | 受影响 | 已修复 |
|---------|----------|-------|
| R32 | R32–R32 P5 | R32 P6 |
| R33 | R33–R33 P5 | R33 P6 |
| R34 | R34–R34 P4 | R34 P5 |
| R35 | R35–R35 P1 | R35 P2 |
| R36 | R36–R36 P3 | R36 P4 |
### NGINX 生态
| 产品 | 受影响版本 | 状态 |
|---------|----------|--------|
| NGINX Instance Manager | 2.16.0–2.21.1 | 安全公告待发布 |
| F5 NGINX WAF | 5.9.0–5.12.1 | 安全公告待发布 |
| NGINX Ingress Controller | 3.5.0–3.7.2, 4.0.0–4.0.1, 5.0.0–5.4.1 | 安全公告待发布 |
| NGINX Gateway Fabric | 1.3.0–1.6.2, 2.0.0–2.5.1 | 安全公告待发布 |
| NGINX Service Mesh | 1.6.0–1.6.2, 2.0.0–2.1.0 | 安全公告待发布 |
| NGINX Agent | 2.0.0–2.35.0 | 安全公告待发布 |
---
## 6. 检测