Skip to content

Commit d121ff6

Browse files
authored
Merge pull request rapid7#21307 from adfoster-r7/improve-vuln-and-vuln-attempt-tracking
Improve vuln and vuln attempt tracking
2 parents 7c4f15a + e00515c commit d121ff6

20 files changed

Lines changed: 1499 additions & 38 deletions

File tree

Gemfile.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,17 +353,17 @@ GEM
353353
mutex_m
354354
railties (~> 7.0)
355355
metasploit-payloads (2.0.245)
356-
metasploit_data_models (6.0.15)
357-
activerecord (~> 7.0)
358-
activesupport (~> 7.0)
356+
metasploit_data_models (6.0.18)
357+
activerecord (>= 7.0, < 8.1)
358+
activesupport (>= 7.0, < 8.1)
359359
arel-helpers
360360
bigdecimal
361361
drb
362362
metasploit-concern
363-
metasploit-model (~> 5.0.4)
363+
metasploit-model (>= 5.0.4)
364364
mutex_m
365365
pg
366-
railties (~> 7.0)
366+
railties (>= 7.0, < 8.1)
367367
recog
368368
webrick
369369
metasploit_payloads-mettle (1.0.46)

db/schema.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[7.2].define(version: 2026_01_30_124052) do
13+
ActiveRecord::Schema[7.2].define(version: 2026_04_11_000000) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "plpgsql"
1616

@@ -665,6 +665,8 @@
665665
t.integer "session_id"
666666
t.integer "loot_id"
667667
t.text "fail_detail"
668+
t.string "check_code"
669+
t.text "check_detail"
668670
end
669671

670672
create_table "vuln_details", id: :serial, force: :cascade do |t|

lib/msf/base/simple/auxiliary.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,13 @@ def self.job_run_proc(ctx, &block)
175175
begin
176176
begin
177177
job_listener.start run_uuid
178+
mod.check_code = nil if mod.respond_to?(:check_code=)
179+
mod.last_vuln_attempt = nil if mod.respond_to?(:last_vuln_attempt=)
178180
mod.setup
179181
mod.framework.events.on_module_run(mod)
180182
result = block.call(mod)
183+
# Store the check result if the block returned a CheckCode
184+
mod.check_code = result if result.is_a?(Msf::Exploit::CheckCode)
181185
job_listener.completed(run_uuid, result, mod)
182186
rescue ::Exception => e
183187
job_listener.failed(run_uuid, e, mod)

lib/msf/core/auxiliary.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ def fail_with(reason, msg = nil)
181181
#
182182
attr_accessor :fail_detail
183183

184+
#
185+
# The result of the last check invocation (a Msf::Exploit::CheckCode), if any
186+
#
187+
attr_accessor :check_code
188+
189+
#
190+
# The VulnAttempt object created during this run, or nil/false if none
191+
# was recorded. Used to prevent duplicate attempts when report_failure
192+
# is called later and to enrich the attempt with check code details.
193+
#
194+
attr_accessor :last_vuln_attempt
195+
184196
attr_accessor :queue
185197

186198
protected

lib/msf/core/auxiliary/multiple_target_hosts.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,17 @@ def check
2020
return Exploit::CheckCode::Unsupported unless has_check?
2121

2222
nmod = replicant
23-
nmod.check_host(datastore['RHOST'])
23+
result = nmod.check_host(datastore['RHOST'])
24+
25+
# Propagate the last_vuln_attempt (which may be the actual VulnAttempt
26+
# object) back from the replicant so that the ensure block in
27+
# job_run_proc (which calls report_failure on the *original* instance)
28+
# knows a vuln attempt was already created and can enrich it directly.
29+
if nmod.respond_to?(:last_vuln_attempt) && nmod.last_vuln_attempt && respond_to?(:last_vuln_attempt=)
30+
self.last_vuln_attempt = nmod.last_vuln_attempt
31+
end
32+
33+
result
2434
end
2535

2636
end

lib/msf/core/auxiliary/report.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,31 @@ def report_vuln(opts={})
314314
:fail_detail => 'vulnerability identified',
315315
:fail_reason => 'Untried', # Mdm::VulnAttempt::Status::UNTRIED, avoiding direct dependency on Mdm, used elsewhere in this module
316316
:module => mname,
317-
:username => username || "unknown"
317+
:username => username || self.owner || "unknown"
318318
}
319319

320+
# Enrich attempt with check code details when available.
321+
# Accept an explicit check_code in opts (useful when the module knows the
322+
# result before the framework sets self.check_code), falling back to the
323+
# module-level accessor.
324+
check_code = opts[:check_code]
325+
check_code = self.check_code if check_code.nil? && self.respond_to?(:check_code)
326+
if check_code.is_a?(Msf::Exploit::CheckCode)
327+
attempt_info[:check_code] = check_code.code
328+
attempt_info[:check_detail] = check_code.reason || check_code.message
329+
attempt_info[:fail_detail] = nil
330+
mapped_reason = Msf::Module::Failure.fail_reason_from_check_code(check_code)
331+
attempt_info[:fail_reason] = mapped_reason if mapped_reason
332+
end
333+
320334
# TODO: figure out what opts are required and why the above logic doesn't match that of the db_manager method
321-
framework.db.report_vuln_attempt(vuln, attempt_info)
335+
attempt = framework.db.report_vuln_attempt(vuln, attempt_info)
336+
337+
# Store the attempt object so that report_failure (called later by the
338+
# job wrapper) can enrich it directly without re-querying the DB.
339+
if self.respond_to?(:last_vuln_attempt=)
340+
self.last_vuln_attempt = attempt || true
341+
end
322342

323343
vuln
324344
end

lib/msf/core/auxiliary/scanner.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ module Auxiliary::Scanner
1515
class AttemptFailed < Msf::Auxiliary::Failed
1616
end
1717

18+
# Scanner modules handle per-host failure reporting through replicants
19+
# inside their run_host/run_batch threads. Override the default
20+
# report_failure so that the parent-level call from job_run_proc's
21+
# ensure block does not create a duplicate or misattributed attempt
22+
# after a scan. The check path (check_simple) still needs the
23+
# default report_failure behaviour, so we only skip when the scanner's
24+
# run method has executed.
25+
def report_failure
26+
return if @scanner_run_completed
27+
28+
super
29+
end
30+
1831
#
1932
# Initializes an instance of a recon auxiliary module
2033
#
@@ -42,6 +55,7 @@ def peer
4255
# The command handler when launched from the console
4356
#
4457
def run
58+
@scanner_run_completed = false
4559
@show_progress = datastore['ShowProgress']
4660
@show_percent = datastore['ShowProgressPercent'].to_i
4761

@@ -260,6 +274,7 @@ def run
260274
print_status("Caught interrupt from the console...")
261275
return
262276
ensure
277+
@scanner_run_completed = true
263278
seppuko!()
264279
end
265280
end

lib/msf/core/db_manager/exploit_attempt.rb

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,25 @@ def report_exploit_failure(opts)
7979

8080
vuln = nil
8181
if rids.present?
82-
# Try to find an existing vulnerability with the same service & references
83-
# or, if svc is nil, with the same host & references
84-
vuln = find_vuln_by_refs(rids, host, svc, false)
82+
# Only perform vuln lookup when no check_code is present (normal
83+
# exploit flow) or the check result positively indicates vulnerability.
84+
# Safe, Unknown, and Detected results should not associate this attempt
85+
# with an existing vuln. Only key off check_code — fail_reason alone
86+
# is too broad (e.g. Failure::Unknown covers real exploit failures too).
87+
vuln_check_codes = [Msf::Exploit::CheckCode::Appears.code, Msf::Exploit::CheckCode::Vulnerable.code]
88+
if opts[:check_code].nil? || vuln_check_codes.include?(opts[:check_code])
89+
# Try to find an existing vulnerability with the same service & references
90+
# or, if svc is nil, with the same host & references
91+
vuln = find_vuln_by_refs(rids, host, svc, false)
92+
93+
# Fall back to a host-only lookup when the service-scoped query found
94+
# nothing. Only match vulns with no associated service to avoid
95+
# misattributing attempts to a vuln on a different service.
96+
if svc && vuln.nil?
97+
fallback_vuln = find_vuln_by_refs(rids, host, nil, false)
98+
vuln = fallback_vuln if fallback_vuln && fallback_vuln.service_id.nil?
99+
end
100+
end
85101
end
86102

87103
opts[:service] = svc
@@ -158,8 +174,20 @@ def do_report_failure_or_success(opts)
158174
# Create a references map from the module list
159175
ref_objs = ::Mdm::Ref.where(name: ref_names)
160176

161-
# Try find a matching vulnerability
162-
vuln = find_vuln_by_refs(ref_objs, host, svc, false)
177+
# Only perform vuln lookup when no check_code is present (normal
178+
# exploit flow) or the check result positively indicates vulnerability.
179+
# Safe, Unknown, and Detected results should not associate this attempt
180+
# with an existing vuln. Only key off check_code — fail_reason alone
181+
# is too broad (e.g. Failure::Unknown covers real exploit failures too).
182+
vuln_check_codes = [Msf::Exploit::CheckCode::Appears.code, Msf::Exploit::CheckCode::Vulnerable.code]
183+
if opts[:check_code].nil? || vuln_check_codes.include?(opts[:check_code])
184+
# Try find a matching vulnerability
185+
vuln = find_vuln_by_refs(ref_objs, host, svc, false)
186+
if svc && vuln.nil?
187+
fallback_vuln = find_vuln_by_refs(ref_objs, host, nil, false)
188+
vuln = fallback_vuln if fallback_vuln && fallback_vuln.service_id.nil?
189+
end
190+
end
163191
end
164192

165193
attempt_info = {
@@ -170,12 +198,17 @@ def do_report_failure_or_success(opts)
170198
:module => mname,
171199
:username => username || "unknown",
172200
}
201+
attempt_info[:check_code] = opts[:check_code] if opts[:check_code]
202+
attempt_info[:check_detail] = opts[:check_detail] if opts[:check_detail]
173203

174204
attempt_info[:session_id] = opts[:session_id] if opts[:session_id]
175205
attempt_info[:loot_id] = opts[:loot_id] if opts[:loot_id]
176206

177-
# We have match, lets create a vuln_attempt record
178-
if vuln
207+
# We have match, lets create a vuln_attempt record.
208+
# Skip if the caller already recorded a vuln attempt for this run
209+
# (e.g. Auxiliary::Report#report_vuln sets skip_vuln_attempt via
210+
# the last_vuln_attempt flag on the module).
211+
if vuln && !opts[:skip_vuln_attempt]
179212
attempt_info[:vuln_id] = vuln.id
180213
vuln.vuln_attempts.create(attempt_info)
181214

@@ -200,7 +233,8 @@ def do_report_failure_or_success(opts)
200233
attempt_info[:proto] = prot || Msf::DBManager::DEFAULT_SERVICE_PROTO
201234
end
202235

203-
host.exploit_attempts.create(attempt_info)
236+
# check_code and check_detail are valid for VulnAttempt but not ExploitAttempt
237+
host.exploit_attempts.create(attempt_info.except(:check_code, :check_detail))
204238
}
205239

206240
end

lib/msf/core/exploit.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,13 @@ def handle_exception e
14931493
#
14941494
attr_accessor :fail_detail
14951495

1496+
#
1497+
# The VulnAttempt object created during this run, or nil/false if none
1498+
# was recorded. Used to prevent duplicate attempts when report_failure
1499+
# is called later and to enrich the attempt with check code details.
1500+
#
1501+
attr_accessor :last_vuln_attempt
1502+
14961503
#
14971504
# The list of targets.
14981505
#

lib/msf/core/exploit/remote/auto_check.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def with_prepended_auto_check
5151
name: fullname,
5252
username: respond_to?(:owner) ? owner : nil,
5353
refs: references,
54-
info: description.strip
54+
info: description.strip,
55+
check_code: check_code
5556
}
5657

5758
if respond_to?(:session) && session.respond_to?(:session_host)

0 commit comments

Comments
 (0)