Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ source 'https://rubygems.org'
# spec.add_runtime_dependency '<name>', [<version requirements>]
gemspec name: 'metasploit-framework'

gem 'metasploit_data_models', git: 'https://github.com/cdelafuente-r7/metasploit_data_models/', branch: 'module-execution-data-models'

# separate from test as simplecov is not run on travis-ci
group :coverage do
# code coverage for tests
Expand Down
33 changes: 20 additions & 13 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
GIT
remote: https://github.com/cdelafuente-r7/metasploit_data_models
revision: 7bafa640b845bfe2b3adf39351fd27986ba65385
branch: module-execution-data-models
specs:
metasploit_data_models (6.0.19)
activerecord (>= 7.0, < 8.1)
activesupport (>= 7.0, < 8.1)
arel-helpers
bigdecimal
drb
metasploit-concern
metasploit-model (>= 5.0.4)
mutex_m
pg
railties (>= 7.0, < 8.1)
recog
webrick

GIT
remote: https://github.com/rapid7/LicenseFinder
revision: 2b1aefcfd6d0745d17843050c1c89f973c1fa98c
Expand Down Expand Up @@ -370,19 +389,6 @@ GEM
mutex_m
railties (>= 7.0, < 8.1)
metasploit-payloads (2.0.245)
metasploit_data_models (6.0.18)
activerecord (>= 7.0, < 8.1)
activesupport (>= 7.0, < 8.1)
arel-helpers
bigdecimal
drb
metasploit-concern
metasploit-model (>= 5.0.4)
mutex_m
pg
railties (>= 7.0, < 8.1)
recog
webrick
metasploit_payloads-mettle (1.0.46)
method_source (1.1.0)
mime-types (3.7.0)
Expand Down Expand Up @@ -715,6 +721,7 @@ DEPENDENCIES
license_finder!
memory_profiler
metasploit-framework!
metasploit_data_models!
octokit
pry-byebug
rake
Expand Down
65 changes: 64 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2026_04_11_000000) do
ActiveRecord::Schema[8.0].define(version: 2026_06_08_120002) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"

Expand Down Expand Up @@ -383,6 +383,64 @@
t.index ["refname"], name: "index_module_details_on_refname"
end

create_table "module_execution_errors", force: :cascade do |t|
t.bigint "module_execution_id", null: false
t.text "exception_class"
t.text "message"
t.text "backtrace"
t.text "lifecycle_phase", null: false
t.text "failure_reason"
t.timestamptz "occurred_at", null: false
t.timestamptz "created_at", null: false
t.timestamptz "updated_at", null: false
t.index ["module_execution_id", "occurred_at"], name: "idx_module_execution_errors_on_execution_and_occurred_at"
t.check_constraint "lifecycle_phase = ANY (ARRAY['setup'::text, 'check'::text, 'exploit'::text, 'cleanup'::text, 'post'::text, 'run'::text])", name: "module_execution_errors_lifecycle_phase_check"
end

create_table "module_execution_events", force: :cascade do |t|
t.bigint "module_execution_id", null: false
t.text "name", null: false
t.jsonb "payload"
t.timestamptz "occurred_at", null: false
t.timestamptz "created_at", null: false
t.timestamptz "updated_at", null: false
t.index ["module_execution_id", "occurred_at"], name: "idx_module_execution_events_on_execution_and_occurred_at"
t.index ["name", "occurred_at"], name: "idx_module_execution_events_on_name_and_occurred_at"
end

create_table "module_executions", force: :cascade do |t|
t.bigint "workspace_id", null: false
t.text "module_reference_name", null: false
t.text "module_type", null: false
t.text "kind", default: "run", null: false
t.jsonb "options_snapshot"
t.text "originating_ui", null: false
t.bigint "originating_user_id"
t.text "originating_token_ref"
t.bigint "parent_execution_id"
t.timestamptz "started_at", null: false
t.timestamptz "ended_at"
t.text "terminal_status"
t.text "failure_reason"
t.text "failure_message"
t.integer "single_entity_failure_count", default: 0, null: false
t.jsonb "last_single_entity_errors"
t.timestamptz "created_at", null: false
t.timestamptz "updated_at", null: false
t.index ["kind", "originating_ui"], name: "idx_module_executions_on_kind_and_originating_ui"
t.index ["module_reference_name", "started_at"], name: "idx_module_executions_on_reference_name_and_started_at", order: { started_at: :desc }
t.index ["originating_user_id"], name: "index_module_executions_on_originating_user_id"
t.index ["parent_execution_id"], name: "idx_module_executions_on_parent_execution_id_not_null", where: "(parent_execution_id IS NOT NULL)"
t.index ["workspace_id", "started_at"], name: "idx_module_executions_on_workspace_and_started_at", order: { started_at: :desc }
t.index ["workspace_id"], name: "index_module_executions_on_workspace_id"
t.check_constraint "ended_at IS NULL AND (terminal_status IS NULL OR terminal_status = 'running'::text) OR ended_at IS NOT NULL AND terminal_status IS NOT NULL AND terminal_status <> 'running'::text", name: "module_executions_terminal_status_lifecycle_check"
t.check_constraint "ended_at IS NULL OR ended_at >= started_at", name: "module_executions_ended_at_after_started_at_check"
t.check_constraint "kind = ANY (ARRAY['run'::text, 'check'::text, 'import'::text, 'direct_write'::text])", name: "module_executions_kind_check"
t.check_constraint "module_type = ANY (ARRAY['exploit'::text, 'auxiliary'::text, 'post'::text, 'payload'::text, 'encoder'::text, 'evasion'::text, 'nop'::text, 'external'::text])", name: "module_executions_module_type_check"
t.check_constraint "originating_ui = ANY (ARRAY['console'::text, 'rpc'::text, 'json_rpc'::text, 'mcp'::text, 'external'::text, 'import'::text, 'plugin'::text, 'autocheck'::text])", name: "module_executions_originating_ui_check"
t.check_constraint "terminal_status IS NULL OR (terminal_status = ANY (ARRAY['running'::text, 'success'::text, 'neutral'::text, 'expected_failure'::text, 'unhandled_exception'::text]))", name: "module_executions_terminal_status_check"
end

create_table "module_mixins", id: :serial, force: :cascade do |t|
t.integer "detail_id"
t.text "name"
Expand Down Expand Up @@ -825,6 +883,11 @@
t.boolean "import_fingerprint", default: false
end

add_foreign_key "module_execution_errors", "module_executions", on_delete: :cascade
add_foreign_key "module_execution_events", "module_executions", on_delete: :cascade
add_foreign_key "module_executions", "module_executions", column: "parent_execution_id"
add_foreign_key "module_executions", "users", column: "originating_user_id"
add_foreign_key "module_executions", "workspaces"
add_foreign_key "service_links", "services", column: "child_id"
add_foreign_key "service_links", "services", column: "parent_id"
end
1 change: 1 addition & 0 deletions lib/msf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Msf

require 'msf/core/exception' # TODO: temporary require until we can split up the exceptions file and namespace properly
require 'msf/core/constants'
require 'msf/core/reporting/errors'
require 'msf/core/trace/certificate_trace_presenter'
require 'msf_autoload'

Expand Down
207 changes: 138 additions & 69 deletions lib/msf/base/simple/auxiliary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def self.run_simple(omod, opts = {}, job_listener: Msf::Simple::NoopJobListener.

run_uuid = Rex::Text.rand_text_alphanumeric(24)
job_listener.waiting run_uuid
ctx = [mod, run_uuid, job_listener]
originating_ui = opts['OriginatingUi'] || 'console'
ctx = [mod, run_uuid, job_listener, { originating_ui: originating_ui, kind: Msf::Reporting::Execution::KIND_RUN }]
run_as_job = opts['RunAsJob'].nil? ? mod.passive? : opts['RunAsJob']
if run_as_job
mod.job_id = mod.framework.jobs.start_bg_job(
Expand Down Expand Up @@ -129,7 +130,8 @@ def self.check_simple(mod, opts, job_listener: Msf::Simple::NoopJobListener.inst

run_uuid = Rex::Text.rand_text_alphanumeric(24)
job_listener.waiting run_uuid
ctx = [mod, run_uuid, job_listener]
originating_ui = opts['OriginatingUi'] || 'console'
ctx = [mod, run_uuid, job_listener, { originating_ui: originating_ui, kind: Msf::Reporting::Execution::KIND_CHECK }]

if opts['RunAsJob']
mod.job_id = mod.framework.jobs.start_bg_job(
Expand Down Expand Up @@ -172,89 +174,156 @@ def self.job_run_proc(ctx, &block)
mod = ctx[0]
run_uuid = ctx[1]
job_listener = ctx[2]
lifecycle_opts = ctx[3] || {}
execution_kind = lifecycle_opts[:kind] || Msf::Reporting::Execution::KIND_RUN
originating_ui = lifecycle_opts[:originating_ui] || 'console'
execution = Msf::Reporting::Execution.start!(
framework: mod.framework,
mod: mod,
originating_ui: originating_ui,
kind: execution_kind
)
Msf::Reporting::Execution.clear_module_unhandled_exception(mod)
last_check_code = nil
result = nil
failure_exception = nil
begin
begin
Msf::Reporting::CurrentExecution.with(execution) do
job_listener.start run_uuid
mod.check_code = nil if mod.respond_to?(:check_code=)
mod.last_vuln_attempt = nil if mod.respond_to?(:last_vuln_attempt=)
mod.setup
Msf::Reporting::Execution.with_phase_setup(mod) { mod.setup }
mod.framework.events.on_module_run(mod)
result = block.call(mod)
# Store the check result if the block returned a CheckCode
mod.check_code = result if result.is_a?(Msf::Exploit::CheckCode)
job_listener.completed(run_uuid, result, mod)
last_check_code = result if result.is_a?(Msf::Exploit::CheckCode)
rescue Msf::Auxiliary::Complete => e
failure_exception = e
Msf::Reporting::Execution.with_phase_cleanup(mod) { mod.cleanup }
return
rescue Msf::Auxiliary::Failed => e
mod.error = e
failure_exception = e
mod.print_error("Auxiliary aborted due to failure: #{e.message}")

# The caller should have already set mod.fail_reason
if mod.fail_reason == Msf::Module::Failure::None
mod.fail_reason = Msf::Module::Failure::Unknown
end
mod.fail_detail ||= e.to_s
Msf::Reporting::Execution.with_phase_cleanup(mod) { mod.cleanup }
return
rescue ::Timeout::Error => e
mod.error = e
failure_exception = e
mod.fail_reason = Msf::Module::Failure::TimeoutExpired
mod.fail_detail ||= e.to_s
mod.print_error("Auxiliary triggered a timeout exception")
Msf::Reporting::Execution.with_phase_cleanup(mod) { mod.cleanup }
return
rescue ::Interrupt => e
mod.error = e
failure_exception = e
mod.fail_reason = Msf::Module::Failure::UserInterrupt
mod.fail_detail ||= e.to_s
mod.print_error("Stopping running against current target...")
Msf::Reporting::Execution.with_phase_cleanup(mod) { mod.cleanup }
mod.print_status("Control-C again to force quit all targets.")
begin
Rex.sleep(0.5)
rescue ::Interrupt
raise $!
end
return
rescue ::Msf::OptionValidateError => e
mod.error = e
failure_exception = e
mod.fail_reason = Msf::Module::Failure::BadConfig
mod.fail_detail ||= e.to_s
::Msf::Ui::Formatter::OptionValidateError.print_error(mod, e)
rescue ::Exception => e
job_listener.failed(run_uuid, e, mod)
raise
end
rescue Msf::Auxiliary::Complete
mod.cleanup
return
rescue Msf::Auxiliary::Failed => e
mod.error = e
mod.print_error("Auxiliary aborted due to failure: #{e.message}")

# The caller should have already set mod.fail_reason
if mod.fail_reason == Msf::Module::Failure::None
mod.error = e
failure_exception = e
mod.fail_reason = Msf::Module::Failure::Unknown
end
mod.fail_detail ||= e.to_s

mod.cleanup
return
rescue ::Timeout::Error => e
mod.error = e
mod.fail_reason = Msf::Module::Failure::TimeoutExpired
mod.fail_detail ||= e.to_s
mod.print_error("Auxiliary triggered a timeout exception")
mod.cleanup
return
rescue ::Interrupt => e
mod.error = e
mod.fail_reason = Msf::Module::Failure::UserInterrupt
mod.fail_detail ||= e.to_s
mod.print_error("Stopping running against current target...")
mod.cleanup
mod.print_status("Control-C again to force quit all targets.")
begin
Rex.sleep(0.5)
rescue ::Interrupt
raise $!
end
return
rescue ::Msf::OptionValidateError => e
mod.error = e
mod.fail_reason = Msf::Module::Failure::BadConfig
mod.fail_detail ||= e.to_s
::Msf::Ui::Formatter::OptionValidateError.print_error(mod, e)
rescue ::Exception => e
mod.error = e
mod.fail_reason = Msf::Module::Failure::Unknown
mod.fail_detail ||= e.to_s
mod.print_error("Auxiliary failed: #{e.class} #{e}")
if(e.class.to_s != 'Msf::OptionValidateError')
mod.print_error("Call stack:")
e.backtrace.each do |line|
break if line =~ /lib.msf.base.simple.auxiliary.rb/
mod.print_error(" #{line}")
mod.fail_detail ||= e.to_s
Msf::Reporting::Execution.mark_module_unhandled_exception(mod)
mod.print_error("Auxiliary failed: #{e.class} #{e}")
if(e.class.to_s != 'Msf::OptionValidateError')
mod.print_error("Call stack:")
e.backtrace.each do |line|
break if line =~ /lib.msf.base.simple.auxiliary.rb/
mod.print_error(" #{line}")
end
end

elog('Auxiliary failed', error: e)
Msf::Reporting::Execution.capture_exception!(mod, e, failure_reason: mod.fail_reason)
Msf::Reporting::Execution.with_phase_cleanup(mod) { mod.cleanup }
end
ensure
# Notify the job listener exactly once. +failure_exception+ is
# populated by every rescue branch (including +Msf::Auxiliary::Complete+,
# which the listener historically receives via +failed+); a +nil+
# value indicates the success path completed without raising.
if failure_exception
job_listener.failed(run_uuid, failure_exception, mod)
else
job_listener.completed(run_uuid, result, mod)
end

elog('Auxiliary failed', error: e)
mod.cleanup
# Register an attempt in the database (an `Mdm::ExploitAttempt` (and
# possibly an `Mdm::VulnAttempt`).
#
# Since auxiliary modules don't report clearly when it is a success or a
# failure, we are calling #report_failure keeping the `mod.fail_reason`
# value unchanged. This value is set to `Msf::Module::Failure::None` when
# no error was reported. It should be set to another
# `Msf::Module::Failure::*` value otherwise.
mod.report_failure

if execution
terminal_status, failure_reason, failure_message =
derive_terminal_status(mod, execution_kind, last_check_code)
Msf::Reporting::Execution.finalize!(
execution,
terminal_status: terminal_status,
failure_reason: failure_reason,
failure_message: failure_message
)
end
end
return result
ensure
# Register an attempt in the database (an `Mdm::ExploitAttempt` (and
# possibly an `Mdm::VulnAttempt`).
#
# Since auxiliary modules don't report clearly when it is a success or a
# failure, we are calling #report_failure keeping the `mod.fail_reason`
# value unchanged. This value is set to `Msf::Module::Failure::None` when
# no error was reported. It should be set to another
# `Msf::Module::Failure::*` value otherwise.
mod.report_failure
end

# Translate the auxiliary module's post-run state into a terminal
# status string.
#
# @api private
def self.derive_terminal_status(mod, execution_kind, last_check_code)
unhandled = Msf::Reporting::Execution.module_unhandled_exception?(mod)

if execution_kind == Msf::Reporting::Execution::KIND_CHECK
if unhandled
return [Msf::Reporting::Execution::TERMINAL_UNHANDLED_EXCEPTION, nil, mod.error&.message]
end

return [Msf::Reporting::Execution.terminal_status_for_check_code(last_check_code), nil, nil]
end

if unhandled
reason = mod.fail_reason if mod.respond_to?(:fail_reason)
reason = nil if reason == Msf::Module::Failure::None
return [Msf::Reporting::Execution::TERMINAL_UNHANDLED_EXCEPTION, reason, mod.error&.message]
end

reason = mod.fail_reason if mod.respond_to?(:fail_reason)
if reason && reason != Msf::Module::Failure::None
detail = mod.respond_to?(:fail_detail) ? mod.fail_detail : nil
[Msf::Reporting::Execution::TERMINAL_EXPECTED_FAILURE, reason, detail]
else
[Msf::Reporting::Execution::TERMINAL_SUCCESS, nil, nil]
end
end

#
Expand Down
Loading
Loading