
Exploit de prueba de concepto para CVE-2026-9256, un desbordamiento de búfer en el montón en ngx_http_rewrite_module de NGINX. Valida el bloqueo remoto del worker mediante una URI manipulada con caracteres especiales, con una sonda keep-alive de múltiples etapas para una detección fiable.
Ámbito de aplicación: solo para laboratorios locales, entornos de reproducción autorizados, verificación de vulnerabilidades y análisis de reglas de protección. No lo utilice en objetivos no autorizados. Las ideas del PoC en este artículo solo verifican el comportamiento de caída del worker de NGINX visible de forma remota, no incluyen RCE, elusión de ASLR ni cadenas de explotación estables.
CVE-2026-9256 es una vulnerabilidad de desbordamiento de búfer en el montón (heap buffer overflow) en el módulo ngx_http_rewrite_module de NGINX. El desencadenamiento de la vulnerabilidad no se produce simplemente accediendo a una URI fija, sino que depende de un patrón de configuración de rewrite específico: grupos de captura PCRE superpuestos en la expresión regular de rewrite, y la parte de reemplazo referencia múltiples variables de captura, por ejemplo $1, $2.
Cuando un atacante construye una URI especial que hace que la lógica de rewrite entre en la ruta relevante, NGINX puede presentar una inconsistencia entre el cálculo de longitud y la escritura real al procesar el contenido capturado, concatenar el resultado de rewrite o realizar el escape de URI/parámetros, lo que finalmente provoca la corrupción de la memoria dinámica del proceso worker.
Por lo tanto, la clave de esta vulnerabilidad no es la ruta /api en sí, sino si existe en la configuración de NGINX objetivo una regla de rewrite vulnerable que pueda ser alcanzada por una solicitud. La ruta /api utilizada por defecto en el PoC es solo una ruta de ejemplo en el entorno de reproducción actual. En las pruebas reales, es necesario ajustar la ruta de la solicitud según la regla de rewrite que contenga grupos de captura superpuestos y referencias a múltiples variables de captura en la configuración de NGINX.
El objetivo actual del PoC es verificar el comportamiento de caída del worker / denegación de servicio. No intenta construir un diseño de montón preciso, no intenta sobrescribir la dirección de retorno o punteros de función, ni demuestra la ejecución remota de código. La evidencia estable que se puede observar en el lado remoto es principalmente: la conexión de la solicitud desencadenante se interrumpe anormalmente, luego el servicio NGINX se recupera y responde, y las conexiones keep-alive se interrumpen después del desencadenamiento debido a la caída del worker.
Después de desencadenar esta vulnerabilidad, no siempre se manifiesta como un código de estado HTTP fijo 500, 502 o 400. La razón es que el modelo master-worker de NGINX permite que el proceso worker se cuelgue y luego el master lo reinicie. El fenómeno que ve el atacante en el lado remoto generalmente no es que todo el servicio deje de estar disponible por completo, sino que una conexión se interrumpe repentinamente, se produce un timeout de lectura, la conexión se reinicia, y luego al acceder nuevamente a / se obtiene una respuesta normal.
Por lo tanto, el PoC no puede determinar la existencia de la vulnerabilidad basándose únicamente en el código de estado HTTP de una sola solicitud. Si solo se envía una URI larga y luego se ve que la conexión se interrumpe y se concluye directamente que "la vulnerabilidad existe", el riesgo de falsos positivos es alto. La interrupción de la conexión también puede deberse a fluctuaciones de la red, timeout de proxy, interceptación por dispositivos intermedios, limitación de velocidad del backend o cierre activo de la conexión por parte del servidor.
Por lo tanto, el PoC debe diseñarse como una verificación en múltiples etapas:
Solo cuando aparecen simultáneamente "conexión desencadenante anormal + recuperación del servicio posterior + múltiples caídas de keep-alive", se puede determinar con mayor seguridad la existencia de un comportamiento de caída del worker estilo CVE-2026-9256.
La ruta central de desencadenamiento del PoC actual es:
GET /api/++++++++++++++++++++++++++++++++... HTTP/1.1
Host: 127.0.0.1:19321
Donde /api/ es la ruta de ejemplo utilizada en el laboratorio actual para alcanzar la regla de rewrite, y luego se concatenan una gran cantidad de caracteres +. La cantidad predeterminada es 4096.
Las razones para elegir + son principalmente tres.
Primero, + es un carácter URI合法, y al usar un cliente HTTP normal generalmente no se trunca o reescribe forzosamente como espacios, #, etc. Por lo tanto, el PoC actual no necesita usar raw socket para construir líneas de solicitud ilegales como en algunas vulnerabilidades de elusión de request-target.
Segundo, una gran cantidad de caracteres repetidos permite que los grupos de captura de rewrite tengan una entrada suficientemente larga, ampliando la escala de salida de la concatenación posterior del reemplazo o del procesamiento de escape, lo que facilita desencadenar la inconsistencia entre el cálculo de longitud y la escritura real.
Tercero, la estructura de payload de + repetidos es simple, fácil de observar en capturas de paquetes, registros y reglas IDS, y también facilita el ajuste de longitud para pruebas de umbral.
Sin embargo, hay que tener en cuenta que + no es el único carácter teóricamente capaz de desencadenar la vulnerabilidad. La condición real de desencadenamiento sigue siendo "alcanzar una configuración de rewrite vulnerable + entrada que pueda entrar en los grupos de captura relevantes + el procesamiento de salida de rewrite que desencadene un desbordamiento de montón". En diferentes entornos, la ruta de desencadenamiento, el tipo de carácter y el umbral de longitud pueden necesitar ajustes.
El PoC actual elige por defecto una gran cantidad de + como caracteres desencadenantes, pero esto no significa que solo + pueda desencadenar el problema. + es simplemente el carácter más adecuado para escribir en un PoC genérico, porque es relativamente estable en una URI, fácil de enviar con un cliente HTTP normal y con características claras en la captura de paquetes.
Desde el principio de la vulnerabilidad, siempre que un carácter entre en la lógica de escape NGX_ESCAPE_ARGS durante el procesamiento de rewrite de NGINX y se expanda de 1 byte original a 3 bytes en forma %XX, puede causar una diferencia entre el "valor calculado de longitud" y el "valor real escrito". Es decir, el punto de desencadenamiento no es esencialmente + en sí mismo, sino la "aparición densa de caracteres que pueden ser escapados en modo args".
Además de +, los caracteres que teóricamente deben considerarse con énfasis incluyen:
Espacio: 0x20
#: 0x23
%: 0x25
&: 0x26
?: 0x3F
Caracteres de control: 0x00-0x1F
Bytes altos: 0x7F-0xFF
Si estos caracteres entran en la captura relevante y son tratados como contenido args para escape en el reemplazo de rewrite, todos producirán un efecto de expansión similar. Por ejemplo:
+ -> %2B
& -> %26
% -> %25
# -> %23
? -> %3F
Espacio -> %20
Cada aparición de un carácter de este tipo expandirá teóricamente 1 byte a 3 bytes, aumentando la longitud real de escritura en 2 bytes. Si la entrada contiene una gran cantidad de estos caracteres, la longitud real de escritura puede superar significativamente la longitud del búfer calculada incorrectamente, facilitando así la activación del desbordamiento del búfer en el montón.
Sin embargo, la usabilidad de diferentes caracteres en el PoC no es completamente igual.
+ es el más estable. Generalmente puede aparecer directamente en el request-target HTTP, no es fácilmente truncado por navegadores o herramientas de línea de comandos, y no cambia de forma natural la estructura path/query de la URI. Por lo tanto, el PoC actual utiliza 4096 + como payload predeterminado.
& también puede ser un carácter candidato, porque en modo args se escapará a %26. Pero en shell, & tiene el significado de ejecución en segundo plano, y en URL también suele usarse como separador de parámetros de query, por lo que se debe tener cuidado con las referencias y la posición al probar; de lo contrario, la solicitud puede no enviarse como se espera.
% también puede ser un carácter candidato, porque se escapará a %25. Pero % en sí mismo es un prefijo de codificación URL, y algunos clientes, proxies o frameworks pueden intentar interpretar secuencias %XX. Si no se construye adecuadamente, el destino puede no recibir el carácter % original, sino contenido preprocesado por el cliente.
? teóricamente también pertenece a los caracteres escapables, pero en el request-target HTTP separa path y query. Si se coloca directamente en la ruta, el contenido posterior puede analizarse como query string, cambiando así el alcance de captura de rewrite. Por lo tanto, es más adecuado como carácter complementario para pruebas, no como carácter principal predeterminado del PoC.
# teóricamente puede desencadenar escape, pero el navegador no envía # ni su fragmento posterior al servidor, y muchos clientes HTTP avanzados también lo codificarán o truncarán. Por lo tanto, si se quiere probar el # literal, generalmente se necesita raw socket, Burp Repeater o una herramienta que pueda conservar el request-target original; no se puede confiar directamente en la barra de direcciones del navegador.
El espacio 0x20 también pertenece a los caracteres escapables, pero en una línea de solicitud HTTP/1.1 normal, el espacio en sí es un delimitador. Colocarlo directamente en el request-target rompería la estructura de la línea de solicitud. En pruebas reales, si se escribe como %20, si el servidor ve la forma codificada o el espacio decodificado depende del proceso de análisis específico y de la posición de rewrite. Por lo tanto, el espacio es más adecuado para explicaciones de principio y pruebas auxiliares, no como payload predeterminado.
Los caracteres de control 0x00-0x1F y los bytes altos 0x7F-0xFF también están dentro del rango de escape, pero en enlaces HTTP reales son más fácilmente interceptados, normalizados o rechazados por clientes, proxies, WAF o el parser HTTP de NGINX. Pueden usarse como explicación de objetos de escape a nivel de código fuente, pero no se recomiendan como caracteres desencadenantes predeterminados para un PoC regular.
Por lo tanto, la razón por la que el PoC actual usa + no es porque la vulnerabilidad solo pueda ser desencadenada por +, sino porque + cumple tres condiciones: puede desencadenar la expansión de escape args, es fácil de enviar de manera estable y no cambia significativamente la estructura de la URI. Las reglas de protección o el análisis de tráfico no deben coincidir solo con + consecutivos, sino que también deben considerar combinaciones de alta densidad de otros caracteres escapables, especialmente la aparición masiva de caracteres como +, &, %, ?, # en URI largas.
Desde la perspectiva de la detección, una generalización más razonable no es:
Después de /api/ aparecen muchos +
sino:
En una URI larga aparecen muchos caracteres especiales que se expandirían a %XX en modo NGX_ESCAPE_ARGS
Si solo se detecta ++++, la regla solo cubre la escritura predeterminada del PoC actual; si el atacante cambia el payload a &&&&, %%%%, ????, o usa una mezcla de +%&?#, la característica única de + podría pasar desapercibida. Un enfoque de detección más seguro es combinar la longitud de la URI, la densidad de caracteres especiales, el número de repeticiones consecutivas, las rutas de rewrite de riesgo y la superficie de exposición del servicio NGINX.
La función normalize_target del PoC se encarga de procesar la entrada de línea de comandos, soportando tres formas:
python3 CVE-2026-9256-poc.py 127.0.0.1:19321
python3 CVE-2026-9256-poc.py 127.0.0.1 19321
python3 CVE-2026-9256-poc.py http://127.0.0.1:19321
Si el usuario solo ingresa host:port sin escribir el esquema, el script agregará automáticamente http://host:port. Luego usa urllib.parse.urlparse para analizar el hostname y el puerto, y genera base, por ejemplo:
host = 127.0.0.1
port = 19321
base = http://127.0.0.1:19321
La implementación actual está orientada principalmente a laboratorios HTTP en texto claro. Aunque normalize_target acepta la forma https://, la detección keep-alive posterior usa un socket TCP normal sin capa TLS, por lo que en escenarios HTTPS el sondeo de caída será inexacto. Para soportar HTTPS, sería necesario agregar ssl.wrap_socket o ssl.create_default_context().wrap_socket() al socket.
El PoC primero llama a check_alive(base) para acceder a la ruta raíz /:
GET /
Si el objetivo puede devolver cualquier código de estado HTTP, significa que el servicio está básicamente vivo y se puede continuar con las pruebas. Si la conexión falla, se sale directamente para evitar interpretar un objetivo inalcanzable como un fallo en el desencadenamiento de la vulnerabilidad.
Luego llama a check_rewrite(base) para acceder a:
GET /api/test
Esta solicitud se utiliza para observar si /api/* puede alcanzar la lógica de rewrite en el laboratorio actual. Si devuelve códigos de estado de redirección como 301, 302, 303, 307, 308, indica que el comportamiento de rewrite redirect es bastante evidente, y el PoC imprimirá el encabezado Location como evidencia auxiliar.
Sin embargo, este paso no es una condición obligatoria de éxito. Porque en algunas configuraciones de reproducción, la ruta /api/* puede entrar directamente en la ruta de rewrite problemática, e incluso una solicitud de sondeo normal podría agotar el tiempo de espera o ser procesada de manera anómala. Por lo tanto, incluso si el script no obtiene una respuesta de rewrite normal, continuará con la fase de desencadenamiento.
La función de desencadenamiento es send_trigger(base, plus_count=4096), cuya lógica central es concatenar:
payload = "/api/" + ("+" * plus_count)
La ruta de solicitud final predeterminada es similar a:
/api/++++++++++++++++++++++++++++++++... 4096 + en total
Luego se envía la solicitud mediante requests.get(base + payload, timeout=10, allow_redirects=False).
Deshabilitar la redirección automática tiene dos razones.
Primero, el rewrite en sí mismo puede devolver una redirección. Si el cliente HTTP sigue la redirección automáticamente, la solicitud desencadenante original y las solicitudes de redirección posteriores se mezclarían, dificultando determinar qué sucedió exactamente con la primera solicitud.
Segundo, el PoC se centra en el estado de la conexión durante la fase de desencadenamiento, no en la página de negocio después de la redirección. Conservar la respuesta original facilita el análisis.
El resultado del desencadenamiento se clasifica en varios tipos:
Si se captura una ConnectionError, significa que la conexión se cerró de manera anómala durante la solicitud desencadenante, lo que podría ser una manifestación remota de la caída del worker.
Si se captura un ReadTimeout, significa que después de enviar la solicitud no se recibió una respuesta normal durante mucho tiempo, lo que también podría deberse a que el worker está atascado, se colgó antes de devolver una respuesta, o un timeout causado por el entorno de red.
Si se recibe una respuesta HTTP normal, se imprime el código de estado y la longitud del cuerpo de la respuesta, pero no se puede descartar la vulnerabilidad solo por una respuesta normal, porque en algunos entornos las condiciones de desencadenamiento pueden no cumplirse completamente, o la longitud del payload es insuficiente.
Después de la solicitud desencadenante, el PoC espera 1 segundo y luego llama a follow_up(base) para acceder nuevamente a la ruta raíz /.
El propósito de este paso no es demostrar el desbordamiento en sí, sino determinar si NGINX muestra las características de "caída del worker y reinicio por parte del master".
Si la conexión de la solicitud desencadenante se interrumpe de forma anómala, pero al acceder posteriormente a / se obtiene un 200 u otro código de estado HTTP normal, significa que el servicio no cayó por completo, sino que parece que un solo proceso worker se colgó y luego se recuperó.
Si el servicio permanece inalcanzable durante mucho tiempo después del desencadenamiento, podría deberse a que todo el servicio se detuvo, el contenedor se cayó o una anomalía de red; este resultado no puede equipararse directamente a un desencadenamiento exitoso de CVE-2026-9256.
Por lo tanto, el criterio de juicio del PoC actual es: caída del worker visible de forma remota, no simplemente "servicio no disponible".
La verificación de estabilidad más crítica en el PoC es keepalive_probe(host, port, rounds=5, plus_count=4096).
No usa requests, sino que establece una conexión TCP directamente con socket.create_connection y envía tres solicitudes consecutivas en la misma conexión keep-alive.
La primera solicitud es una solicitud normal:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Esta solicitud se utiliza para confirmar que la conexión actual está disponible y para intentar que la solicitud desencadenante posterior caiga en la misma conexión.
La segunda solicitud es la solicitud desencadenante:
GET /api/++++++++++++++++++++++++++++++++... HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Si esta solicitud desencadena la caída del worker, es muy probable que la conexión keep-alive mantenida por ese worker se interrumpa directamente.
La tercera solicitud sigue siendo una solicitud normal:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: close
Si aún se puede recibir una respuesta a la tercera solicitud, significa que la conexión no se interrumpió debido a la solicitud desencadenante, y en esta ronda no se considera caída del worker.
Si la tercera solicitud falla al enviarse, no se pueden leer datos, o la conexión ya se ha cerrado, se registra como:
keepalive connection dropped
El PoC repite 5 rondas por defecto. La importancia de la repetición múltiple es reducir los falsos positivos debidos a errores de red ocasionales. Si en 5 rondas se producen varias caídas de keep-alive y el servicio aún puede recuperar la respuesta, la evidencia remota es más sólida.
El juicio final del PoC se divide en tres niveles.
Primer nivel: Vulnerabilidad confirmada.
La condición es:
crash_count > 0 and recovered == True
Es decir, al menos una ronda del sondeo keep-alive detectó una caída de conexión, y la solicitud normal de seguimiento demuestra que el servicio ha recuperado la respuesta. El script genera:
VULNERABILITY CONFIRMED - CVE-2026-9256 style crash behavior
Impact confirmed: worker crash / denial of service
RCE is not proven by this script
Esto indica que el entorno actual muestra un comportamiento de caída del worker estilo CVE-2026-9256, pero no demuestra la ejecución remota de código.
Segundo nivel: Sospecha de existencia.
La condición es:
kind == "connection_error" and recovered == True
Es decir, la solicitud desencadenante principal provocó una interrupción de la conexión y el servicio posterior se recuperó, pero el sondeo keep-alive no confirmó de manera estable la caída del worker. El script genera VULNERABILITY SUSPECTED.
Esta situación indica que hay fenómenos anómalos, pero la evidencia no es lo suficientemente estable; se necesita confirmación adicional mediante error.log del servidor, core dump, registros del contenedor o depurador.
Tercer nivel: No confirmado.
Si no hay una caída confiable de keep-alive ni una combinación de interrupción de conexión desencadenante y recuperación del servicio, el script genera:
VULNERABILITY NOT CONFIRMED
Esto no significa necesariamente que el objetivo sea absolutamente invulnerable; también podría deberse a que la ruta no alcanzó el rewrite, la longitud del payload fue insuficiente, la selección de caracteres no fue la adecuada, la versión objetivo ya está parcheada, un proxy intermedio cambió la URI, o el script actual no está adaptado a HTTPS.
El flujo de ejecución del script se puede resumir como:
/, confirmar que el servicio objetivo está vivo./api/test, intentar determinar si la ruta de rewrite de ejemplo está activa./api/ más 4096 + como solicitud desencadenante./, confirmar si el worker se ha recuperado.Ejemplo de laboratorio local:
python3 CVE-2026-9256-poc.py http://127.0.0.1:19321
o:
python3 CVE-2026-9256-poc.py 127.0.0.1 19321
Cuando se desencadena con éxito, la salida típica se mostrará como:
[+] Connection dropped during trigger request
[+] Worker is responding after trigger (HTTP 200)
round 1: worker likely crashed (keepalive connection dropped)
round 2: worker likely crashed (keepalive connection dropped)
...
[+] VULNERABILITY CONFIRMED - CVE-2026-9256 style crash behavior
[+] Impact confirmed: worker crash / denial of service
[*] RCE is not proven by this script
Este tipo de salida indica que en el lado remoto se observó evidencia bastante estable de caída del worker.
Los límites de seguridad de este PoC son bastante claros:
Primero, solo hace verificación de caída, no explotación de RCE.
Segundo, no construye heap spray, ROP, elusión de ASLR, shellcode ni lógica de ejecución de comandos.
Tercero, su criterio de éxito es la caída de la conexión del worker y la recuperación del servicio, no obtener un shell o leer archivos.
Cuarto, es adecuado para reproducción local, verificación de vulnerabilidades, construcción de reglas IDS/IPS y pruebas comparativas antes y después de la corrección.
Para mejorar aún más la seguridad, se podrían agregar las siguientes restricciones:
127.0.0.1, localhost, direcciones privadas o segmentos de red de experimentación explícitamente autorizados.--plus-count para evitar enviar un payload demasiado grande por defecto.--route para que el usuario especifique explícitamente la ruta de desencadenamiento, en lugar de tener /api/ fijo.--rounds para controlar el número de sondeos keep-alive.--print-request para imprimir la solicitud HTTP real enviada, facilitando la comparación con los resultados de captura de paquetes.Desde la perspectiva de la derivación del PoC, las reglas de detección no deben centrarse solo en /api/, porque /api no es una ruta fija de la vulnerabilidad, sino solo un ejemplo del laboratorio actual. Los puntos de detección realmente valiosos deberían ser:
+, o una mezcla de caracteres especiales como +, &, %, ?, #.Si la regla solo tiene escrito /api/++++, solo cubre el PoC actual y el laboratorio actual; para cubrir tráfico de ataque más genérico, se deben extraer características en torno a "URI larga + gran cantidad de caracteres especiales de alta densidad + dirección de solicitud HTTP + rutas de riesgo de rewrite de NGINX".
Al mismo tiempo, dado que en el negocio legítimo también pueden existir URL largas o muchos caracteres codificados, las reglas deben reducir los falsos positivos mediante umbrales de longitud, densidad de caracteres, número de repeticiones y contexto de ruta. Una dirección de detección más segura es:
URI larga
+
Gran cantidad de caracteres especiales que pueden ser escapados/expandidos por NGX_ESCAPE_ARGS
+
Dirección de solicitud to_server
+
Superficie de exposición relacionada con rewrite de NGINX
En lugar de simplemente detectar:
/api/++++
Para reglas de Suricata / Snort, si solo se quiere cubrir el PoC público actual, se pueden usar + consecutivos como una de las características fuertes; si se desea cubrir variantes, es necesario incluir el rango de caracteres especiales en PCRE, por ejemplo +, %, #, &, ? y otros caracteres que puedan ser escapados/expandidos. Pero tales reglas también son más propensas a falsos positivos, por lo que deben usarse junto con urilen, umbral de repetición de caracteres, restricciones de ruta y el rango de activos NGINX.
El enfoque de la derivación del PoC de CVE-2026-9256 no es encontrar una ruta de vulnerabilidad fija, sino comprender primero las condiciones de desencadenamiento: configuración de rewrite vulnerable, grupos de captura superpuestos, referencia a múltiples variables de captura y una entrada URI especial que pueda hacer que el resultado del procesamiento de rewrite se expanda anormalmente.
El script actual elige /api/ más 4096 + porque esta ruta puede alcanzar la regla de rewrite en el entorno de reproducción actual, y una gran cantidad de + puede generar de manera estable una presión de entrada larga. El script no implementa RCE, sino que demuestra la caída del worker mediante interrupción de conexión, recuperación del servicio y múltiples caídas de keep-alive.
Al mismo tiempo, + es solo el carácter predeterminado más estable y fácil de enviar, no el único carácter que puede desencadenar el problema. Todos los caracteres que se expanden a %XX en modo NGX_ESCAPE_ARGS deben considerarse en el análisis de principios y en las reglas de protección. La comprensión más precisa debería ser: en una URI larga, la aparición densa de caracteres escapables/expandibles, al entrar en una captura vulnerable de rewrite y en el procesamiento de reemplazo, provoca una inconsistencia entre el cálculo de longitud y la escritura real, lo que finalmente causa una caída del worker o una corrupción de memoria más grave.