Skip to content

Commit 0b71adf

Browse files
Vulnerability Report enhancement
- update `#report_service` and `#report_vuln` - update vulnerability report when a session is established - update CheckCode and `#cmd_check` to report a vulnerability when Vulnerable checkcode is returned - update `vulns` and `services` commands to display the `resource` and parent services - specs
1 parent c2971d5 commit 0b71adf

File tree

15 files changed

+1340
-103
lines changed

15 files changed

+1340
-103
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ source 'https://rubygems.org'
33
# spec.add_runtime_dependency '<name>', [<version requirements>]
44
gemspec name: 'metasploit-framework'
55

6+
gem 'metasploit_data_models', git: '[email protected]:cdelafuente-r7/metasploit_data_models.git', branch: 'MS-9930_resource_layered_services'
7+
68
# separate from test as simplecov is not run on travis-ci
79
group :coverage do
810
# code coverage for tests

Gemfile.lock

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
GIT
2+
remote: [email protected]:cdelafuente-r7/metasploit_data_models.git
3+
revision: fdefdfa6e99ff8aa47f3e9144a18cbe642f1d315
4+
branch: MS-9930_resource_layered_services
5+
specs:
6+
metasploit_data_models (6.0.11)
7+
activerecord (~> 7.0)
8+
activesupport (~> 7.0)
9+
arel-helpers
10+
bigdecimal
11+
drb
12+
metasploit-concern
13+
metasploit-model (>= 3.1)
14+
mutex_m
15+
pg
16+
railties (~> 7.0)
17+
recog
18+
webrick
19+
120
PATH
221
remote: .
322
specs:
@@ -348,16 +367,6 @@ GEM
348367
mutex_m
349368
railties (~> 7.0)
350369
metasploit-payloads (2.0.221)
351-
metasploit_data_models (6.0.9)
352-
activerecord (~> 7.0)
353-
activesupport (~> 7.0)
354-
arel-helpers
355-
metasploit-concern
356-
metasploit-model (>= 3.1)
357-
pg
358-
railties (~> 7.0)
359-
recog
360-
webrick
361370
metasploit_payloads-mettle (1.0.42)
362371
method_source (1.1.0)
363372
mime-types (3.6.0)
@@ -668,6 +677,7 @@ DEPENDENCIES
668677
license_finder (= 5.11.1)
669678
memory_profiler
670679
metasploit-framework!
680+
metasploit_data_models!
671681
octokit
672682
pry-byebug
673683
rake

db/schema.rb

Lines changed: 17 additions & 2 deletions
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: 2025_02_04_172657) do
13+
ActiveRecord::Schema[7.2].define(version: 2025_07_21_114306) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "plpgsql"
1616

@@ -521,6 +521,16 @@
521521
t.string "netmask"
522522
end
523523

524+
create_table "service_links", force: :cascade do |t|
525+
t.bigint "parent_id", null: false
526+
t.bigint "child_id", null: false
527+
t.datetime "created_at", null: false
528+
t.datetime "updated_at", null: false
529+
t.index ["child_id"], name: "index_service_links_on_child_id"
530+
t.index ["parent_id", "child_id"], name: "index_service_links_on_parent_id_and_child_id", unique: true
531+
t.index ["parent_id"], name: "index_service_links_on_parent_id"
532+
end
533+
524534
create_table "services", id: :serial, force: :cascade do |t|
525535
t.integer "host_id"
526536
t.datetime "created_at", precision: nil
@@ -530,7 +540,8 @@
530540
t.string "name"
531541
t.datetime "updated_at", precision: nil
532542
t.text "info"
533-
t.index ["host_id", "port", "proto"], name: "index_services_on_host_id_and_port_and_proto", unique: true
543+
t.jsonb "resource", default: {}, null: false
544+
t.index ["host_id", "port", "proto", "name", "resource"], name: "index_services_on_5_columns", unique: true
534545
t.index ["name"], name: "index_services_on_name"
535546
t.index ["port"], name: "index_services_on_port"
536547
t.index ["proto"], name: "index_services_on_proto"
@@ -686,6 +697,7 @@
686697
t.integer "vuln_attempt_count", default: 0
687698
t.integer "origin_id"
688699
t.string "origin_type"
700+
t.jsonb "resource", default: {}, null: false
689701
t.index ["name"], name: "index_vulns_on_name"
690702
t.index ["origin_id"], name: "index_vulns_on_origin_id"
691703
end
@@ -803,4 +815,7 @@
803815
t.boolean "limit_to_network", default: false, null: false
804816
t.boolean "import_fingerprint", default: false
805817
end
818+
819+
add_foreign_key "service_links", "services", column: "child_id"
820+
add_foreign_key "service_links", "services", column: "parent_id"
806821
end

lib/msf/core/db_manager/service.rb

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,15 @@ def find_or_create_service(opts)
4343
# +:workspace+:: the workspace for the service
4444
#
4545
# opts may contain
46-
# +:name+:: the application layer protocol (e.g. ssh, mssql, smb)
47-
# +:sname+:: an alias for the above
48-
# +:info+:: Detailed information about the service such as name and version information
49-
# +:state+:: The current listening state of the service (one of: open, closed, filtered, unknown)
50-
#
46+
# +:name+:: the application layer protocol (e.g. ssh, mssql, smb)
47+
# +:sname+:: an alias for the above
48+
# +:info+:: detailed information about the service such as name and version information
49+
# +:state+:: the current listening state of the service (one of: open, closed, filtered, unknown)
50+
# +:resource+:: the resource this service is associated with, such as a a DN for an an LDAP object
51+
# base URI for a web application, pipe name for DCERPC service, etc.
52+
# +:parents+:: a single service Hash or an Array of service Hash representing the parent services this
53+
# service is associated with, such as a HTTP service for a web application.
54+
#`
5155
# @return [Mdm::Service,nil]
5256
def report_service(opts)
5357
return if !active
@@ -69,6 +73,7 @@ def report_service(opts)
6973
if opts[:sname]
7074
opts[:name] = opts.delete(:sname)
7175
end
76+
opts[:name] = opts[:name].to_s.downcase if opts[:name]
7277

7378
if addr.kind_of? ::Mdm::Host
7479
host = addr
@@ -84,7 +89,14 @@ def report_service(opts)
8489

8590
proto = opts[:proto] || Msf::DBManager::DEFAULT_SERVICE_PROTO
8691

87-
service = host.services.where(port: opts[:port].to_i, proto: proto).first_or_initialize
92+
sopts = {
93+
port: opts[:port].to_i,
94+
proto: proto
95+
}
96+
sopts[:name] = opts[:name] if opts[:name]
97+
sopts[:resource] = opts[:resource] if opts[:resource]
98+
service = host.services.where(sopts).first_or_initialize
99+
88100
ostate = service.state
89101
opts.each { |k,v|
90102
if (service.attribute_names.include?(k.to_s))
@@ -93,8 +105,15 @@ def report_service(opts)
93105
dlog("Unknown attribute for Service: #{k}")
94106
end
95107
}
108+
96109
service.state ||= Msf::ServiceState::Open
97110
service.info ||= ""
111+
parents = process_service_chain(host, opts.delete(:parents)) if opts[:parents]
112+
if parents
113+
parents.each do |parent|
114+
service.parents << parent if parent && !service.parents.include?(parent)
115+
end
116+
end
98117

99118
begin
100119
framework.events.on_db_service(service) if service.new_record?
@@ -163,4 +182,44 @@ def update_service(opts)
163182
return service
164183
}
165184
end
185+
186+
def process_service_chain(host, services)
187+
return if services.nil? || host.nil?
188+
return unless services.is_a?(Hash) || services.is_a?(::Array)
189+
return unless host.is_a?(Mdm::Host)
190+
191+
services = [services] unless services.is_a?(Array)
192+
services.map do |service|
193+
return unless service.is_a?(Hash)
194+
return if service[:port].nil? || service[:proto].nil?
195+
196+
parents =nil
197+
if service[:parents]&.any?
198+
parents = process_service_chain(host, service[:parents])
199+
end
200+
201+
service_info = {
202+
port: service[:port].to_i,
203+
proto: service[:proto].to_s.downcase,
204+
}
205+
service_info[:name] = service[:name].downcase if service[:name]
206+
service_info[:resource] = service[:resource] if service[:resource]
207+
service_obj = host.services.find_or_create_by(service_info)
208+
if service_obj.id.nil?
209+
elog("Failed to create service #{service_info.inspect} for host #{host.name} (#{host.address})")
210+
return
211+
end
212+
service_obj.state ||= Msf::ServiceState::Open
213+
service_obj.info ||= ''
214+
215+
if parents
216+
parents.each do |parent|
217+
service_obj.parents << parent if parent && !service_obj.parents.include?(parent)
218+
end
219+
end
220+
221+
service_obj
222+
end
223+
224+
end
166225
end

lib/msf/core/db_manager/session.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,27 @@ def infer_vuln_from_session(session, wspace)
250250
workspace: wspace,
251251
}
252252

253-
port = session.exploit_datastore["RPORT"]
254-
service = (port ? host.services.find_by_port(port.to_i) : nil)
255-
256-
vuln_info[:service] = service if service
253+
if session.exploit.respond_to?(:service_details) && session.exploit.service_details
254+
service_details = session.exploit.service_details
255+
service_name = service_details[:service_name]
256+
port = service_details[:port]
257+
if port.nil?
258+
port = session.respond_to?(:target_port) && session.target_port ? session.target_port : session.exploit_datastore["RPORT"]
259+
end
260+
proto = service_details[:protocol]
261+
vuln_info[:service] = host.services.find_or_create_by(name: service_name, port: port.to_i, proto: proto, state: 'open')
262+
end
263+
unless vuln_info[:service]
264+
port = session.respond_to?(:target_port) && session.target_port ? session.target_port : session.exploit_datastore["RPORT"]
265+
vuln_info[:service] = host.services.find_by_port(port.to_i) if port
266+
end
257267

258268
vuln = report_vuln(vuln_info)
259269

260270
attempt_info = {
261271
host: host,
262272
module: mod_fullname,
263273
refs: refs,
264-
service: service,
265274
session_id: s.id,
266275
timestamp: Time.now.utc,
267276
username: session.username,

lib/msf/core/db_manager/vuln.rb

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,15 @@ def find_vuln_by_details(details_map, host, service=nil)
4444
other_vulns.empty? ? nil : other_vulns.first
4545
end
4646

47-
def find_vuln_by_refs(refs, host, service = nil, cve_only = true)
47+
def find_vuln_by_refs(refs, host, service = nil, cve_only = true, resource = nil)
4848
ref_ids = cve_only ? refs.find_all { |ref| ref.name.starts_with? 'CVE-'} : refs
4949
relation = host.vulns.joins(:refs)
5050
if !service.try(:id).nil?
51-
return relation.where(service_id: service.try(:id), refs: { id: ref_ids}).first
51+
if resource
52+
return relation.where(service_id: service.try(:id), refs: { id: ref_ids}, resource: resource).first
53+
else
54+
return relation.where(service_id: service.try(:id), refs: { id: ref_ids}).first
55+
end
5256
end
5357
return relation.where(refs: { id: ref_ids}).first
5458
end
@@ -80,12 +84,20 @@ def has_vuln?(name)
8084
# opts MUST contain
8185
# +:host+:: the host where this vulnerability resides
8286
# +:name+:: the friendly name for this vulnerability (title)
87+
# +:workspace+:: the workspace to report this vulnerability in
8388
#
8489
# opts can contain
8590
# +:info+:: a human readable description of the vuln, free-form text
8691
# +:refs+:: an array of Ref objects or string names of references
8792
# +:details+:: a hash with :key pointed to a find criteria hash and the rest containing VulnDetail fields
8893
# +:sname+:: the name of the service this vulnerability relates to, used to associate it or create it.
94+
# +:exploited_at+:: a timestamp indicating when this vulnerability was exploited, if applicable
95+
# +:ref_ids+:: an array of reference IDs to associate with this vulnerability
96+
# +:service+:: a Mdm::Service object or a Hash with service attributes to associate this vulnerability with
97+
# +:port+:: the port number of the service this vulnerability relates to, if applicable
98+
# +:proto+:: the transport layer protocol of the service this vulnerability relates to, if applicable
99+
# +:details_match+:: a Mdm:VulnDetail with details related to this vulnerability
100+
# +:resource+:: a resource hash to associate with this vulnerability, such as a URI or pipe name
89101
#
90102
def report_vuln(opts)
91103
return if not active
@@ -141,7 +153,16 @@ def report_vuln(opts)
141153
vuln = nil
142154

143155
# Identify the associated service
144-
service = opts.delete(:service)
156+
service_opt = opts.delete(:service)
157+
case service_opt
158+
when Mdm::Service
159+
service = service_opt
160+
when Hash
161+
service = report_service(service_opt.merge(workspace: wspace, host: host))
162+
else
163+
dlog("Skipping service since it is not a Hash or Mdm::Service: #{service.class}")
164+
service = nil
165+
end
145166

146167
# Treat port zero as no service
147168
if service or opts[:port].to_i > 0
@@ -160,9 +181,17 @@ def report_vuln(opts)
160181
sname = opts[:proto]
161182
end
162183

163-
services = host.services.where(port: opts[:port].to_i, proto: proto)
164-
services = services.where(name: sname) if sname.present?
165-
service = services.first_or_create
184+
# If sname and proto are not provided, this will assign the first service
185+
# registered in the database for this host with the given port and proto.
186+
# This is likely to be the TCP service.
187+
sopts = {
188+
workspace: wspace,
189+
host: host,
190+
port: opts[:port].to_i,
191+
proto: proto
192+
}
193+
sopts[:name] = sname if sname.present?
194+
service = report_service(sopts)
166195
end
167196

168197
# Try to find an existing vulnerability with the same service & references
@@ -172,8 +201,12 @@ def report_vuln(opts)
172201
# prevent dupes of the same vuln found by both local patch and
173202
# service detection.
174203
if rids and rids.length > 0
175-
vuln = find_vuln_by_refs(rids, host, service)
176-
vuln.service = service if vuln
204+
if opts[:resource]
205+
vuln = find_vuln_by_refs(rids, host, service, nil, opts[:resource])
206+
else
207+
vuln = find_vuln_by_refs(rids, host, service)
208+
end
209+
vuln.service = service if vuln && !vuln.service_id?
177210
end
178211
else
179212
# Try to find an existing vulnerability with the same host & references
@@ -194,9 +227,17 @@ def report_vuln(opts)
194227
# No matches, so create a new vuln record
195228
unless vuln
196229
if service
197-
vuln = service.vulns.find_by_name(name)
230+
if opts[:resource]
231+
vuln = service.vulns.find_by(name: name, resource: opts[:resource])
232+
else
233+
vuln = service.vulns.find_by_name(name)
234+
end
198235
else
199-
vuln = host.vulns.find_by_name(name)
236+
if opts[:resource]
237+
vuln = host.vulns.find_by(name: name, resource: opts[:resource])
238+
else
239+
vuln = host.vulns.find_by_name(name)
240+
end
200241
end
201242

202243
unless vuln
@@ -208,6 +249,7 @@ def report_vuln(opts)
208249
}
209250

210251
vinf[:service_id] = service.id if service
252+
vinf[:resource] = opts[:resource] if opts[:resource]
211253
vuln = Mdm::Vuln.create(vinf)
212254

213255
begin

0 commit comments

Comments
 (0)