Skip to content

RPC: Add job tracking for all module types#21653

Open
cdelafuente-r7 wants to merge 1 commit into
rapid7:masterfrom
cdelafuente-r7:feature/msf-job-tracking-rpc
Open

RPC: Add job tracking for all module types#21653
cdelafuente-r7 wants to merge 1 commit into
rapid7:masterfrom
cdelafuente-r7:feature/msf-job-tracking-rpc

Conversation

@cdelafuente-r7

@cdelafuente-r7 cdelafuente-r7 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds support to job tracking for all module types when run through the RPC endpoints. This is required for the new MCP tools that were added and submitted in a separate PR. Particularly:

  • adds a job_listener: kwarg to run_simple / check_simple for exploit, auxiliary, post and evasion modules
  • exposes Msf::Module#run_uuid
  • threads the resulting UUID through Msf::ExploitDriver and Msf::EvasionDriver so RunAsJob paths return [uuid, job_id] uniformly.

This also fixes two issues:

  • fix RPC module.check NotImplementedError handling and JSON-RPC regression
  • fix RPC session.interactive_read/write for non-meterpreter sessions

Related Issue:
Orignial PR: #21632

Breaking Changes

  • module.execute / module.check RPC endpoints: uuid field now returns run_uuid, not mod.uuid
  • module.execute / module.check RPC endpoints: new input validation on opts
  • module.results: status code for unknown UUID changed from 404 to 500
  • module.results result payload is now JSON-sanitised
  • service.rb: ScriptError (incl. NotImplementedError) is now caught at the RPC entry point

Reviewer Notes

This comes from this PR. I have extracted the RPC/Framework changes here, as requested. The MCP-related code is in a separate PR.

Verification Steps

Setup a simple target with docker:

  • Dockerfile
FROM php:7.2-apache

LABEL MAINTAINER="phithon <root@leavesongs.com>"

RUN set -ex \
        && echo 'deb http://archive.debian.org/debian/ buster main contrib non-free' > /etc/apt/sources.list \
        && echo 'deb http://archive.debian.org/debian-security buster/updates main contrib non-free' >> /etc/apt/sources.list \
        && echo 'deb http://archive.debian.org/debian/ buster-updates main contrib non-free' >> /etc/apt/sources.list \
        && apt-get update \
        && apt-get install -y --no-install-recommends unzip \
        && rm -rf /var/lib/apt/lists/*

ENV APACHE_DOCUMENT_ROOT=/var/www/public

RUN set -ex \
    && cd /var/www \
    && rm -rf * \
    && curl -#SL https://github.com/top-think/think/archive/v5.0.23.tar.gz | tar zx --strip-components=1 \
    && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
    && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

COPY composer.json /var/www/

RUN set -ex \
    && cd /var/www \
    && curl -#sSL https://getcomposer.org/download/2.3.10/composer.phar -o composer.phar \
    && php composer.phar install \
    && php composer.phar require "topthink/think-captcha:^1.0" \
    && chown www-data:www-data -R .

WORKDIR /var/www/public
  • docker-compose.yml
services:
 web:
   build: .
   image: thinkphp:5.0.23
   ports:
    - "8080:80"
  • Start it with docker compose up
    • Start the RPC server: ./msfrpcd -f -U msfuser -P 123456 -p 55553 -a 127.0.0.1
    • Start the RPC client: ./msfrpc -a 127.0.0.1 -U msfuser -P 123456
    • Run an exploit module from the client: rpc.call('module.execute', 'exploit', 'unix/webapp/thinkphp_rce', {'LHOST' => '192.168.4.4', 'RPORT'=>8080, 'RHOSTS'=>'127.0.0.1', 'SRVPORT'=>8888})
    • Verify the 24-character UUID is returned.
    • Check the status with module.running_stats: rpc.call('module.running_stats')
    • Verify the 24-character UUID is part of waiting, running or results arrays
    • Check the results: rpc.call('module.results', <UUID>)
    • List sessions: rpc.call('session.list')
    • Send command to the interactive session: rpc.call('session.interactive_write', <sessionID>, "sysinfo")
    • Read the result: rpc.call('session.interactive_read', 1)
    • Redo these tests with other module types (auxiliary, post, evasion, local)
    • Test other session types (SMB, LDAP, shell, etc.)
    • From a Meterpreter session, open a shell interactive channel: rpc.call('session.interactive_write', , "shell")` and run shell commands.
    • Test the check methods: rpc.call('module.check', 'exploit', 'unix/webapp/thinkphp_rce', {'LHOST' => '192.168.4.4', 'RPORT'=>8080, 'RHOSTS'=>'127.0.0.1', 'SRVPORT'=>8888})

Test Evidence

✗ ./msfrpc -a 127.0.0.1 -U msfuser -P 123456
[*] The 'rpc' object holds the RPC client interface
[*] Use rpc.call('group.command') to make RPC calls

>> rpc.call('module.execute', 'exploit', 'unix/webapp/thinkphp_rce', {'LHOST' => '192.168.4.4', 'RPORT'=>8080, 'RHOSTS'=>'127.0.0.1', 'SRVPORT'=>8888})
=> {"job_id"=>0, "uuid"=>"WWQ8hMcMz34PITDJaitmodB2"}
>> rpc.call('module.running_stats')
=> {"waiting"=>[], "running"=>[], "results"=>["WWQ8hMcMz34PITDJaitmodB2"]}
>> rpc.call('module.results', 'WWQ8hMcMz34PITDJaitmodB2')
=>
{"status"=>"completed",
 "result"=>"Meterpreter session 1 opened (192.168.4.4:4444 -> 192.168.4.4:62031) at 2026-07-07 14:56:54 +0200"}
>> rpc.call('session.list')
=>
{1=>
  {"type"=>"meterpreter",
   "tunnel_local"=>"192.168.4.4:4444",
   "tunnel_peer"=>"192.168.4.4:62031",
   "via_exploit"=>"exploit/unix/webapp/thinkphp_rce",
   "via_payload"=>"payload/linux/x64/meterpreter/reverse_tcp",
   "desc"=>"Meterpreter",
   "info"=>"www-data @ 346322c4470a",
   "workspace"=>"default",
   "session_host"=>"127.0.0.1",
   "session_port"=>8080,
   "target_host"=>"127.0.0.1",
   "username"=>"cdelafuente",
   "uuid"=>"sv3slrjf",
   "exploit_uuid"=>"vk6x5j81",
   "routes"=>"",
   "arch"=>"x64",
   "platform"=>"linux"}}
>> rpc.call('session.interactive_write', 1, "sysinfo")
=> {"result"=>"success"}
>> rpc.call('session.interactive_read', 1)
=>
{"data"=>
  "Computer     : 346322c4470a\nOS           : Debian 10.7 (Linux 6.12.76-linuxkit)\nArchitecture : x64\nBuildTuple   : x86_64-linux-musl\nMeterpreter  : x64/linux\n"}

Environment

Field Details
Operating System OSX
Target Software/Hardware Vulnerable ThinkPHP docker instance
Docker Image / Vagrant Setup See the Verification Steps

AI Usage Disclosure

Copilot has been used to assist.

Pre-Submission Checklist

  • [] Included a corresponding documentation markdown file in documentation/modules (new modules only)
  • No sensitive information (IP addresses, credentials, API keys, hashes) in code or documentation
  • Tested on the target environment specified in the Environment section above
  • Included RSpec tests for library changes (encouraged for lib/ changes)
  • Read the CONTRIBUTING.md and module acceptance guidelines
Hardware and Complex Software Module Guidance

If your module targets specialized hardware (routers, IoT, PLCs, etc.) or complex software (licensed, multi-service, or multi-version), provide a pcap, screen recording, or video showing successful execution.

Email sanitized pcaps/recordings to msfdev@metasploit.com — remove real IPs, credentials, and hostnames before sending. If hardware/software is unavailable, explain in the PR description.

Responsiveness and PR Takeover Policy

We want every contribution to make it into the project. If approximately 2 weeks pass after a review request without a comment or code update from you, the team may take over the PR and complete the work on your behalf.

If this happens, you will remain credited as a co-author on the final commit — your contribution is always recognized.

This policy exists to keep the project moving forward. It is not a reflection on the quality of your work or your involvement. Life happens, and we would rather finish the work together than let a good contribution go stale.

Adds a job_listener: kwarg to run_simple / check_simple for exploit,
auxiliary, post and evasion modules, exposes Msf::Module#run_uuid, and
threads the resulting UUID through Msf::ExploitDriver and
Msf::EvasionDriver so RunAsJob paths return [uuid, job_id] uniformly.

Fix RPC module.check NotImplementedError handling and JSON-RPC regression

Fix RPC session.interactive_read/write for non-meterpreter sessions

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Metasploit’s RPC execution/check pathways to uniformly track module runs as “jobs” across exploit/aux/post/evasion by introducing a job_listener: kwarg through the Simple wrappers and drivers, exposing Msf::Module#run_uuid, and ensuring RPC returns the run UUID consistently while also improving RPC session interactivity for shell-like sessions.

Changes:

  • Add job_listener: plumbing through run_simple / check_simple and through Msf::ExploitDriver / Msf::EvasionDriver, generating and propagating a 24-char run_uuid.
  • Update RPC module.execute/module.check behavior to validate options structurally and to return uuid == run_uuid, plus adjust module.results missing-uuid status code.
  • Fix RPC session.interactive_read/write to support non-meterpreter interactive shell/powershell sessions and add extensive RSpec coverage for the new behaviors.

Impact Analysis:

  • Blast radius: high — affects RPC consumers (JSON-RPC/Msgpack RPC clients), module execution orchestration, and job/result polling; also touches console and session scripting entrypoints that call Simple wrappers.
  • Data and contract effects: breaking RPC contract changes (uuid semantics now map to run_uuid; options validation may reject previously accepted shapes; module.results missing UUID status code changes); introduces new persisted-in-memory job result serialization behavior (JSON sanitization).
  • Rollback and test focus: rollback is straightforward (code-only) but verify RPC compatibility carefully; focus testing on module.execute/module.check return payloads, module.results polling across module types, and interactive read/write for shell + meterpreter sessions.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
spec/lib/msf/exploit/local_spec.rb Adds coverage for Exploit::Local#run_simple accepting/forwarding job_listener:.
spec/lib/msf/core/rpc/v10/rpc_session_spec.rb Expands specs to validate interactive read/write for shell sessions.
spec/lib/msf/core/rpc/v10/rpc_module_execute_spec.rb New specs ensuring module.execute returns run_uuid + job id across module types and validates opts.
spec/lib/msf/core/rpc/v10/rpc_module_check_spec.rb New specs for module.check NotImplementedError behavior + listener wiring.
spec/lib/msf/core/rpc/v10/rpc_job_status_tracker_spec.rb Adds specs for JSON-safe result storage with framework objects and primitives.
spec/lib/msf/base/simple/post_job_tracking_spec.rb Adds job listener lifecycle tests for post modules + run_uuid assignment.
spec/lib/msf/base/simple/exploit_job_tracking_spec.rb Adds exploit driver job listener lifecycle tests + signature/forwarding checks.
spec/lib/msf/base/simple/evasion_job_tracking_spec.rb Adds evasion driver job listener lifecycle tests + signature checks.
spec/lib/msf/base/simple/auxiliary_job_tracking_spec.rb Adds auxiliary job listener lifecycle tests + signature/forwarding checks.
lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb Updates run_simple call sites to pass an explicit options Hash (keyword-arg safety).
lib/rex/post/hwbridge/ui/console/command_dispatcher/core.rb Same: wraps run_simple args in {} to avoid keyword ambiguity.
lib/msf/ui/console/module_command_dispatcher.rb Same: wraps check_simple args in {} for keyword-arg safety.
lib/msf/ui/console/command_dispatcher/post.rb Same: wraps post run_simple options in {}.
lib/msf/ui/console/command_dispatcher/core.rb Same: wraps post module run_simple options in {}.
lib/msf/ui/console/command_dispatcher/auxiliary.rb Same: wraps auxiliary run_simple options in {} for both single and multi-target paths.
lib/msf/core/rpc/v10/service.rb Broadens top-level RPC exception handling to include ScriptError.
lib/msf/core/rpc/v10/rpc_session.rb Adds interactive shell/powershell read/write support and compatible module listing updates.
lib/msf/core/rpc/v10/rpc_module.rb Adds options validation, threads job listener through RunAsJob paths, returns run_uuid, adjusts results missing-uuid status.
lib/msf/core/rpc/v10/rpc_job_status_tracker.rb Sanitizes stored job results to be JSON-safe (handles cyclic framework objects).
lib/msf/core/post/session_upgrade.rb Wraps exploit_simple args in {} to avoid keyword ambiguity.
lib/msf/core/module.rb Introduces attr_accessor :run_uuid on modules.
lib/msf/core/exploit/remote/check_module.rb Wraps run_simple args in {} to avoid keyword ambiguity.
lib/msf/core/exploit/remote/browser_autopwn2.rb Wraps exploit_simple args in {} to avoid keyword ambiguity.
lib/msf/core/exploit/local.rb Adds job_listener: kwarg to local exploit run_simple and forwards it.
lib/msf/core/exploit_driver.rb Adds job listener + run_uuid generation and reports lifecycle events.
lib/msf/core/evasion_driver.rb Adds job listener + run_uuid generation and reports lifecycle events.
lib/msf/base/simple/post.rb Adds job_listener: to post simple runner and emits lifecycle events + run_uuid propagation.
lib/msf/base/simple/exploit.rb Adds job_listener: to exploit simple wrappers and propagates run_uuid.
lib/msf/base/simple/evasion.rb Adds job_listener: to evasion simple runner and propagates run_uuid.
lib/msf/base/simple/auxiliary.rb Adds job_listener: to auxiliary simple wrappers and propagates run_uuid.
lib/msf/base/sessions/scriptable.rb Wraps run_simple / exploit_simple args in {} to avoid keyword ambiguity.

end

iout = StringIO.new
rex_out = Rex::Ui::Text::Output::Stdio.new(io: iout)
Comment on lines +332 to +340
if SHELL_SESSION_TYPES.include?(session.type)
begin
payload = data.end_with?("\n") ? data : "#{data}\n"
session.shell_write(payload)
return { 'result' => 'success' }
rescue ::Exception => e
error(500, "Session Disconnected: #{e.class} #{e}")
end
end
@cdelafuente-r7 cdelafuente-r7 moved this from Todo to Ready in Metasploit Kanban Jul 13, 2026
@bwatters-r7 bwatters-r7 added this to the Version 6.5 milestone Jul 13, 2026
@jheysel-r7 jheysel-r7 self-assigned this Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rn-enhancement release notes enhancement

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

4 participants