Skip to content

Commit ccfc94f

Browse files
committed
Update logic for aux modules having called report_vuln already
1 parent a4fb46e commit ccfc94f

13 files changed

Lines changed: 147 additions & 67 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)

lib/msf/base/simple/auxiliary.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def self.job_run_proc(ctx, &block)
176176
begin
177177
job_listener.start run_uuid
178178
mod.check_code = nil if mod.respond_to?(:check_code=)
179-
mod.vuln_attempt_recorded = false if mod.respond_to?(:vuln_attempt_recorded=)
179+
mod.last_vuln_attempt = nil if mod.respond_to?(:last_vuln_attempt=)
180180
mod.setup
181181
mod.framework.events.on_module_run(mod)
182182
result = block.call(mod)

lib/msf/core/auxiliary.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ def fail_with(reason, msg = nil)
187187
attr_accessor :check_code
188188

189189
#
190-
# Whether a vuln attempt was already recorded during this run (used to
191-
# prevent duplicate attempts when report_failure is called later).
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.
192193
#
193-
attr_accessor :vuln_attempt_recorded
194+
attr_accessor :last_vuln_attempt
194195

195196
attr_accessor :queue
196197

lib/msf/core/auxiliary/multiple_target_hosts.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ def check
2222
nmod = replicant
2323
result = nmod.check_host(datastore['RHOST'])
2424

25-
# Propagate the vuln_attempt_recorded flag back from the replicant so
26-
# that the ensure block in job_run_proc (which calls report_failure on
27-
# the *original* instance) knows a vuln attempt was already created and
28-
# can skip creating a duplicate.
29-
if nmod.respond_to?(:vuln_attempt_recorded) && nmod.vuln_attempt_recorded && respond_to?(:vuln_attempt_recorded=)
30-
self.vuln_attempt_recorded = true
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
3131
end
3232

3333
result

lib/msf/core/auxiliary/report.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,17 +326,19 @@ def report_vuln(opts={})
326326
if check_code.is_a?(Msf::Exploit::CheckCode)
327327
attempt_info[:check_code] = check_code.code
328328
attempt_info[:check_detail] = check_code.reason || check_code.message
329+
attempt_info[:fail_detail] = nil
329330
mapped_reason = Msf::Module::Failure.fail_reason_from_check_code(check_code)
330331
attempt_info[:fail_reason] = mapped_reason if mapped_reason
331332
end
332333

333334
# TODO: figure out what opts are required and why the above logic doesn't match that of the db_manager method
334-
framework.db.report_vuln_attempt(vuln, attempt_info)
335+
attempt = framework.db.report_vuln_attempt(vuln, attempt_info)
335336

336-
# Signal that a vuln attempt was already recorded for this run so that
337-
# report_failure (called later by the job wrapper) can skip creating a
338-
# duplicate attempt in do_report_failure_or_success.
339-
self.vuln_attempt_recorded = true if self.respond_to?(:vuln_attempt_recorded=)
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
340342

341343
vuln
342344
end

lib/msf/core/auxiliary/scanner.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def peer
5555
# The command handler when launched from the console
5656
#
5757
def run
58-
@scanner_run_completed = true
58+
@scanner_run_completed = false
5959
@show_progress = datastore['ShowProgress']
6060
@show_percent = datastore['ShowProgressPercent'].to_i
6161

@@ -141,7 +141,6 @@ def run
141141
print_status("Error: #{targ}: #{e.class} #{e.message}")
142142
elog("Error running against host #{targ}", error: e)
143143
ensure
144-
nmod.report_failure
145144
nmod.cleanup
146145
end
147146
end
@@ -230,7 +229,6 @@ def run
230229
rescue ::Exception => e
231230
print_status("Error: #{mybatch[0]}-#{mybatch[-1]}: #{e}")
232231
ensure
233-
nmod.report_failure
234232
nmod.cleanup
235233
end
236234
end
@@ -276,6 +274,7 @@ def run
276274
print_status("Caught interrupt from the console...")
277275
return
278276
ensure
277+
@scanner_run_completed = true
279278
seppuko!()
280279
end
281280
end

lib/msf/core/db_manager/exploit_attempt.rb

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ def report_exploit_failure(opts)
7979

8080
vuln = nil
8181
if rids.present?
82-
# Skip vuln lookup entirely when the check result indicates the target
83-
# is not vulnerable. A Safe/Unknown check on port 80 should not
84-
# attach a VulnAttempt to an unrelated vuln on port 9200 just because
85-
# the module refs happen to match.
86-
safe_fail_reasons = [Msf::Module::Failure::NotVulnerable, Msf::Module::Failure::Unknown]
87-
unless safe_fail_reasons.include?(opts[:fail_reason])
82+
# Skip vuln lookup when an explicit check result says the target is
83+
# safe or the check could not establish vulnerability (Unknown).
84+
# Only key off check_code — fail_reason alone is too broad
85+
# (e.g. Failure::Unknown covers real exploit failures too).
86+
safe_check_codes = [Msf::Exploit::CheckCode::Safe.code, Msf::Exploit::CheckCode::Unknown.code]
87+
unless safe_check_codes.include?(opts[:check_code])
8888
# Try to find an existing vulnerability with the same service & references
8989
# or, if svc is nil, with the same host & references
9090
vuln = find_vuln_by_refs(rids, host, svc, false)
@@ -173,11 +173,12 @@ def do_report_failure_or_success(opts)
173173
# Create a references map from the module list
174174
ref_objs = ::Mdm::Ref.where(name: ref_names)
175175

176-
# Skip vuln lookup when the result indicates the target is not
177-
# vulnerable — avoids attaching a VulnAttempt to an unrelated vuln
178-
# on a different port that happens to share the same refs.
179-
safe_fail_reasons = [Msf::Module::Failure::NotVulnerable, Msf::Module::Failure::Unknown]
180-
unless safe_fail_reasons.include?(freason)
176+
# Skip vuln lookup when an explicit check result says the target is
177+
# safe or the check could not establish vulnerability (Unknown).
178+
# Only key off check_code — fail_reason alone is too broad
179+
# (e.g. Failure::Unknown covers real exploit failures too).
180+
safe_check_codes = [Msf::Exploit::CheckCode::Safe.code, Msf::Exploit::CheckCode::Unknown.code]
181+
unless safe_check_codes.include?(opts[:check_code])
181182
# Try find a matching vulnerability
182183
vuln = find_vuln_by_refs(ref_objs, host, svc, false)
183184
vuln ||= find_vuln_by_refs(ref_objs, host, nil, false) if svc && vuln.nil?
@@ -201,7 +202,7 @@ def do_report_failure_or_success(opts)
201202
# We have match, lets create a vuln_attempt record.
202203
# Skip if the caller already recorded a vuln attempt for this run
203204
# (e.g. Auxiliary::Report#report_vuln sets skip_vuln_attempt via
204-
# the vuln_attempt_recorded flag on the module).
205+
# the last_vuln_attempt flag on the module).
205206
if vuln && !opts[:skip_vuln_attempt]
206207
attempt_info[:vuln_id] = vuln.id
207208
vuln.vuln_attempts.create(attempt_info)

lib/msf/core/exploit.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,10 +1494,11 @@ def handle_exception e
14941494
attr_accessor :fail_detail
14951495

14961496
#
1497-
# Whether a vuln attempt was already recorded during this run (used to
1498-
# prevent duplicate attempts when report_failure is called later).
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.
14991500
#
1500-
attr_accessor :vuln_attempt_recorded
1501+
attr_accessor :last_vuln_attempt
15011502

15021503
#
15031504
# The list of targets.

lib/msf/core/module/failure.rb

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ def report_failure
9393
info[:port] = self.datastore['RPORT']
9494
if self.class.ancestors.include?(Msf::Exploit::Remote::Tcp)
9595
info[:proto] = 'tcp'
96+
elsif self.class.ancestors.include?(Msf::Exploit::Remote::Udp)
97+
info[:proto] = 'udp'
9698
end
9799
end
98100

@@ -127,9 +129,9 @@ def report_failure
127129
# check_code is available, update the existing attempt so it carries the
128130
# check result details (the attempt created by report_vuln may not have
129131
# had the check_code yet because it runs before job_run_proc stores it).
130-
if self.respond_to?(:vuln_attempt_recorded) && self.vuln_attempt_recorded
132+
if self.respond_to?(:last_vuln_attempt) && self.last_vuln_attempt
131133
if self.respond_to?(:check_code) && self.check_code.is_a?(Msf::Exploit::CheckCode)
132-
_enrich_existing_vuln_attempt(info)
134+
_enrich_existing_vuln_attempt(info, self.last_vuln_attempt)
133135
end
134136
info[:skip_vuln_attempt] = true
135137
end
@@ -139,28 +141,47 @@ def report_failure
139141

140142
private
141143

142-
# Update the most recent VulnAttempt for this module/host with check code
143-
# details that were not available when report_vuln originally created it.
144-
def _enrich_existing_vuln_attempt(info)
144+
# Update the VulnAttempt for this module/host with check code details that
145+
# were not available when report_vuln originally created it.
146+
#
147+
# @param info [Hash] enrichment data built by report_failure
148+
# @param recorded_attempt [Mdm::VulnAttempt, true] the attempt object stored
149+
# by report_vuln, or +true+ if only the flag was propagated (legacy/fallback).
150+
def _enrich_existing_vuln_attempt(info, recorded_attempt = nil)
145151
return unless framework.db&.active
146152

147-
host = info[:host]
148-
return unless host
149-
150-
host_obj = if host.is_a?(::Mdm::Host)
151-
host
152-
else
153-
wspace = info[:workspace] || framework.db.find_workspace(workspace)
154-
framework.db.get_host(workspace: wspace, address: host.to_s)
155-
end
156-
return unless host_obj
157-
158-
# Find the most recent vuln attempt for this module on this host
159-
attempt = ::Mdm::VulnAttempt
153+
# Use the stored attempt directly when available — avoids a racy
154+
# re-query that could match the wrong row under concurrency.
155+
attempt = recorded_attempt if recorded_attempt.is_a?(::Mdm::VulnAttempt)
156+
157+
# Fallback: re-query if we only have the boolean flag (e.g. propagated
158+
# through a replicant that only forwarded +true+).
159+
if attempt.nil?
160+
host = info[:host]
161+
return unless host
162+
163+
host_obj = if host.is_a?(::Mdm::Host)
164+
host
165+
else
166+
wspace = info[:workspace] || framework.db.find_workspace(workspace)
167+
framework.db.get_host(workspace: wspace, address: host.to_s)
168+
end
169+
return unless host_obj
170+
171+
scope = ::Mdm::VulnAttempt
160172
.joins(:vuln)
161173
.where(module: fullname, vulns: { host_id: host_obj.id })
162-
.order(attempted_at: :desc)
163-
.first
174+
175+
# Narrow by port when available so we don't match an attempt against
176+
# a different service on the same host (e.g. port 80 vs 9200).
177+
if info[:port]
178+
scope = scope.joins(vuln: :service)
179+
.where(services: { port: info[:port] })
180+
end
181+
182+
attempt = scope.order(attempted_at: :desc).first
183+
end
184+
164185
return unless attempt
165186

166187
updates = {}

lib/msf/ui/console/module_command_dispatcher.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,17 @@ def report_vuln(instance, checkcode = nil)
178178
opts = {
179179
workspace: instance.workspace,
180180
host: instance.respond_to?(:target_host) && instance.target_host ? instance.target_host : instance.datastore['RHOST'],
181-
proto: instance.datastore['PROTO'] || 'tcp',
181+
proto: if instance.class.ancestors.include?(Msf::Exploit::Remote::Udp)
182+
'udp'
183+
else
184+
'tcp'
185+
end,
182186
name: instance.name,
183-
info: "This was flagged as vulnerable by the explicit check of #{instance.fullname}.",
187+
info: if checkcode == Msf::Exploit::CheckCode::Appears
188+
"Target appears vulnerable based on the explicit check of #{instance.fullname}."
189+
else
190+
"Vulnerability confirmed by the explicit check of #{instance.fullname}."
191+
end,
184192
refs: instance.references
185193
}
186194

0 commit comments

Comments
 (0)