
CVE-2026-42533: desbordamiento de montón (heap overflow) pre-autenticación y fuga de información en nginx por la corrupción de capturas de PCRE en el motor de map/script, encadenado a RCE.
Desbordamiento de búfer en el heap y fuga de información del heap antes de la autenticación en nginx, causados por la falta de guardado/restauración del estado de las capturas de PCRE entre las dos pasadas del motor de scripts. Una variable map con regex evaluada entre dos referencias a capturas sobrescribe r->captures, por lo que la pasada LEN y la pasada VALUE no se ponen de acuerdo sobre el tamaño de una captura. El búfer se dimensiona para una captura y se rellena con otra. Una sobrescritura mayor produce un desbordamiento del heap con contenido y longitud controlados por el atacante. Una sobrescritura menor produce un búfer sobredimensionado cuya cola sin inicializar se devuelve al cliente, filtrando punteros de libc y del heap.
Las dos primitivas se encadenan para lograr una ejecución remota de código fiable antes de la autenticación. La fuga neutraliza ASLR en una sola petición GET, por lo que el desbordamiento no necesita que ASLR esté desactivado.
Informe: https://cyberstan.co.uk/nginx-rce/ Aviso: F5 K000162097 Reportado por: Stan Shaw (cyberstan)
nginx 0.9.6 hasta 1.30.3 (estable) y 1.31.2 (mainline), explotable desde que la directiva map incorporó soporte de regex en 2011. Afecta tanto al módulo http como al stream. Unas 50 directivas repartidas en 13 puntos de llamada, más una segunda vía a través de capturas con nombre (r->variables[]). Corregido en 1.30.4 y 1.31.3.
CVE-2026-42533-PoC/
├── exploits/ exploit and validator scripts
│ ├── poc.py numbered-capture proxy_method chain (crash/leak/rce/rce-det)
│ ├── calibrate.py find PL_OFF / HEAP_PAGE_OFF for --rce-det on your build
│ ├── leak_multi.py info leak on the return and set sinks
│ ├── test_all_sites.py ASan validator, all 13 overflow sites
│ └── named_capture_poc.py named-capture r->variables[] variant
├── configs/ nginx configs the exploits run against
│ ├── nginx_poc.conf crash / leak / rce
│ └── nginx_det.conf deterministic rce-det
├── docs/
│ └── EXPLOITATION.md full exploitation writeup
└── README.md
Ejecuta todos los comandos de abajo desde la raíz del repositorio, para que exploits/, configs/ y el árbol de compilación ../nginx-1.30.1 se resuelvan correctamente.
exploits/poc.py es el exploit principal (captura numerada, sumidero proxy_method). Sus modos:
Scripts independientes que cubren el resto de la superficie del fallo:
Las configuraciones están en configs/: nginx_poc.conf (crash/leak/rce) y nginx_det.conf (rce-det). El informe completo está en docs/EXPLOITATION.md.
Linux, gcc, python3 y el código fuente de nginx 1.30.1. Desarrollado y probado en Ubuntu 24.04.4, glibc 2.39, PCRE2 10.42, python 3.12, ASLR completo.
Dos compilaciones. Una compilación limpia para la fuga y el RCE, para que el residuo del heap sea real. Una compilación AddressSanitizer para el crash y los validadores de puntos, para que el desbordamiento se informe con el tamaño exacto de la escritura y la pila.
tar xf nginx-1.30.1.tar.gz
cd nginx-1.30.1
# clean build -> objs.dbg/nginx (leak, rce)
./configure --with-pcre --with-http_ssl_module --with-debug --builddir=objs.dbg
make -j"$(nproc)"
# ASan build with all the modules the site validator needs -> objs/nginx
./configure --with-pcre --with-http_ssl_module --with-http_v2_module \
--with-stream --with-stream_ssl_preread_module --with-stream_ssl_module \
--with-debug \
--with-cc-opt='-g -O0 -fsanitize=address -fno-omit-frame-pointer' \
--with-ld-opt=-fsanitize=address --builddir=objs
make -j"$(nproc)"
exploits/poc.py se comunica con un nginx en ejecución en 127.0.0.1:8950. Inícialo en una terminal con configs/nginx_poc.conf y luego ejecuta en otra el modo que quieras. Los otros tres scripts inician y detienen su propio nginx, por lo que solo necesitan NGINX_BIN.
mkdir -p run/logs
../nginx-1.30.1/objs/nginx -p run -c "$PWD/configs/nginx_poc.conf" # ASan build, foreground
python3 exploits/poc.py --crash
Esperado: un heap-buffer-overflow, WRITE of size 200 en ngx_http_script_copy_capture_code en ngx_http_script.c:1404, llamado desde ngx_http_complex_value en ngx_http_proxy_create_request.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --leak
Esperado: un cuerpo de 8161 bytes con 2 bytes escritos y el resto residuo del heap. Un puntero de libc en el offset 0x08 y un puntero del heap en 0x10.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --rce # writes /tmp/PWNED via system()
Esperado: una fuga, unas 40 conexiones de spray, el disparo del desbordamiento y luego /tmp/PWNED con la salida de id. Es un disparo único y acierta aproximadamente dos de cada tres veces en la compilación de desarrollo; si falla, el worker crashea y vuelves a ejecutarlo. Consulta la sección "Fiabilidad" de docs/EXPLOITATION.md.
Contra una configuración controlada, el mismo fallo es un disparo único determinista. Recupera la base absoluta del heap a partir de la fuga en una sola línea (heap_base = (leaked_ptr & ~0xfff) - 0x22000), deposita un único cleanup de pool falsificado en una conexión retenida en una dirección conocida y apunta el cleanup del pool víctima a él, en lugar de al cuerpo de disparo transitorio, que nginx libera antes del teardown.
mkdir -p run/logs
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_det.conf" # controlled config
python3 exploits/poc.py --rce-det
nginx_det.conf es una configuración de laboratorio (un solo worker, búferes fijos) cuyo diseño del heap es reproducible, y eso es lo que hace que los offsets de poc.py (PL_OFF, HEAP_PAGE_OFF) sigan siendo válidos. PL_OFF es el offset desde la base del heap en el que aterriza el cuerpo de cleanup retenido de POST /b/; depende de la secuencia exacta de asignaciones, por lo que varía entre compilaciones, versiones de glibc y configuraciones. Si --rce-det informa Recalibrate, lee los valores correctos de un worker en vivo con calibrate.py:
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/calibrate.py
# prints e.g. set PL_OFF = 0x14426 , then edit it in exploits/poc.py
La única otra posibilidad de fallo es una tirada de ASLR que coloca un 0x0a dentro de la dirección del cleanup o del cuerpo del desbordamiento, algo que la regex de map no puede transportar; la herramienta lo informa y vuelves a ejecutar. Un despliegue estándar no es tan predecible, así que usa --rce en ese caso. Consulta "Una compilación determinista" en docs/EXPLOITATION.md.
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py 1 7 12 # a subset
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/named_capture_poc.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/leak_multi.py # clean build
Esperado: return y set filtran cada uno un puntero de libc y uno del heap, ambos confirmados contra /proc/<pid>/maps del worker.
Las dos cosas que más confunden son: usar la compilación equivocada (ASan frente a limpia) y que los offsets de --rce-det no coincidan con tu entorno. Ambas se tratan a continuación.
--rce recupera la base de libc y un puntero del heap en tiempo de ejecución a partir de la fuga, por lo que no codifica ninguna dirección de forma fija. Lo que sí codifica son offsets específicos de la compilación y la libc con las que se desarrolló:
LIBC_LEAK_OFFSET libc base to the leaked arena pointer
SYSTEM_OFFSET libc base to system()
BODY_DELTA_* leaked heap pointer to the overflow body buffer
POOL_OFF_FROM_BUF, D_LAST_OFF, D_END_OFF, LOG_OFF forged pool geometry
En una distro, glibc o compilación de nginx diferentes, estos valores requieren recalibración. Lee el offset real de system() con readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep '\bsystem\b', y lee los offsets del pool de la ngx_pool_t del objetivo. Los modos --crash y --leak no llevan esos offsets y se reproducen en cualquier compilación de una versión afectada.
La fuga no requiere ningún ajuste especial. Funciona con una configuración estándar events {}, cuyo worker_connections por defecto (512) dimensiona la arena de modo que el chunk de la petición liberado cae en un bin de glibc que aún contiene punteros de la arena y del heap que el búfer de fuga sobredimensionado reutiliza. Solo un worker_connections inusualmente bajo (por debajo de ~256) lo evita; todos los valores de producción (512-1024) filtran.
Un escáner estático de configuración que señala el patrón vulnerable sin explotar nada está en https://github.com/0xCyberstan/CVE-2026-42533-Config-Scanner.
Esta herramienta apunta a una vulnerabilidad corregida y divulgada públicamente. Existe para que los defensores verifiquen la exposición y para la reproducción en investigación. Ejecútala solo contra nginx que sea tuyo o sobre el que tengas autorización explícita para hacer pruebas. Actualiza a 1.30.4 o 1.31.3.
| Modo | Propósito |
|---|
poc.py --crash | Dispara el desbordamiento del heap; en una compilación ASan imprime el tamaño de la escritura y la pila en ngx_http_script_copy_capture_code. |
poc.py --leak | Fuga de información: extrae los punteros de libc y del heap del cuerpo de la respuesta sobredimensionada. |
poc.py --rce | RCE completo antes de la autenticación. Disparo único general (configs/nginx_poc.conf), ~66% por intento; repítelo si falla. |
poc.py --rce-det | RCE completo antes de la autenticación, determinista contra la configuración controlada configs/nginx_det.conf. |
| Script | Propósito |
|---|
exploits/leak_multi.py | Fuga de información a través de otros dos evaluadores (return, set); cada puntero libc+heap se confirma contra /proc/<pid>/maps. Config estándar. |
exploits/test_all_sites.py | Validador AddressSanitizer que dispara los 13 puntos de llamada del desbordamiento (http + stream). |
exploits/named_capture_poc.py | La variante de captura con nombre (?P<name>...) a través de r->variables[] / copy_var_code, una segunda causa raíz. |
| Síntoma | Por qué | Qué hacer |
|---|
--leak no muestra punteros, o --rce / --rce-det nunca aciertan | Estás en la compilación ASan; AddressSanitizer envenena la memoria liberada, por lo que el residuo no contiene punteros reales | Usa la compilación limpia objs.dbg para --leak, --rce, --rce-det y leak_multi.py. La compilación ASan objs es solo para --crash, test_all_sites.py y named_capture_poc.py. |
--rce acierta solo unas 2 de cada 3 veces | Es un disparo único contra una configuración general; la ubicación del cuerpo de disparo liberado varía | Es lo esperado. Si falla, el worker crashea y se reinicia, así que simplemente vuelve a ejecutarlo. Para un disparo único determinista usa --rce-det. |
--rce-det imprime No RCE. Recalibrate PL_OFF/HEAP_PAGE_OFF en cada ejecución | PL_OFF es específico de tu compilación, glibc, configuración y de la longitud de la ruta del prefijo -p de nginx. El valor incluido es para la compilación de desarrollo de este repositorio lanzada con -p run. | Ejecuta exploits/calibrate.py, pega el PL_OFF que imprime en exploits/poc.py e inicia nginx con el mismo prefijo -p que le diste a calibrate.py (el README usa -p run). |
--rce-det a veces imprime 0x0a (regex-hostile) ... retry | Una tirada de ASLR colocó un byte 0x0a (nueva línea) en una dirección, que la regex de map no puede transportar | No es un fallo. Vuelve a ejecutar; casi siempre la siguiente tirada lo evita. |
nginx o calibrate.py: bind() to 127.0.0.1:8950 failed (Address already in use) | Un nginx anterior sigue ocupando el puerto | pkill -x nginx, espera un segundo y reintenta. Mantén solo un nginx en el 8950. |
Un script independiente imprime nginx not found | NGINX_BIN sin definir o apuntando a la compilación equivocada | Define NGINX_BIN (compilación limpia para leak_multi.py, compilación ASan para los validadores). |
No such file para una configuración, un script o el binario de nginx | No estás en la raíz del repositorio | Haz cd a la raíz del repositorio primero; todos los comandos la asumen (exploits/..., configs/..., ../nginx-1.30.1/...). |
| La fuga funciona con una configuración pero no con otra | Un worker_connections por debajo de ~256 reduce la arena, por lo que el chunk liberado no contiene punteros | Usa un worker_connections normal (512 a 1024). Todos los valores de producción filtran. |