Skip to content
Open
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.git', branch: 'MS-9930_resource_layered_services'

# separate from test as simplecov is not run on travis-ci
group :coverage do
# code coverage for tests
Expand Down
30 changes: 20 additions & 10 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.git
revision: 40b566f81584a365fea3f4e961e5c2519042d426
branch: MS-9930_resource_layered_services
specs:
metasploit_data_models (6.0.11)
activerecord (~> 7.0)
activesupport (~> 7.0)
arel-helpers
bigdecimal
drb
metasploit-concern
metasploit-model (>= 3.1)
mutex_m
pg
railties (~> 7.0)
recog
webrick

PATH
remote: .
specs:
Expand Down Expand Up @@ -348,16 +367,6 @@ GEM
mutex_m
railties (~> 7.0)
metasploit-payloads (2.0.221)
metasploit_data_models (6.0.9)
activerecord (~> 7.0)
activesupport (~> 7.0)
arel-helpers
metasploit-concern
metasploit-model (>= 3.1)
pg
railties (~> 7.0)
recog
webrick
metasploit_payloads-mettle (1.0.45)
method_source (1.1.0)
mime-types (3.6.0)
Expand Down Expand Up @@ -669,6 +678,7 @@ DEPENDENCIES
license_finder (= 5.11.1)
memory_profiler
metasploit-framework!
metasploit_data_models!
octokit
pry-byebug
rake
Expand Down
19 changes: 17 additions & 2 deletions 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[7.2].define(version: 2025_02_04_172657) do
ActiveRecord::Schema[7.2].define(version: 2025_07_21_114306) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand Down Expand Up @@ -521,6 +521,16 @@
t.string "netmask"
end

create_table "service_links", force: :cascade do |t|
t.bigint "parent_id", null: false
t.bigint "child_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["child_id"], name: "index_service_links_on_child_id"
t.index ["parent_id", "child_id"], name: "index_service_links_on_parent_id_and_child_id", unique: true
t.index ["parent_id"], name: "index_service_links_on_parent_id"
end

create_table "services", id: :serial, force: :cascade do |t|
t.integer "host_id"
t.datetime "created_at", precision: nil
Expand All @@ -530,7 +540,8 @@
t.string "name"
t.datetime "updated_at", precision: nil
t.text "info"
t.index ["host_id", "port", "proto"], name: "index_services_on_host_id_and_port_and_proto", unique: true
t.jsonb "resource", default: {}, null: false
t.index ["host_id", "port", "proto", "name", "resource"], name: "index_services_on_5_columns", unique: true
t.index ["name"], name: "index_services_on_name"
t.index ["port"], name: "index_services_on_port"
t.index ["proto"], name: "index_services_on_proto"
Expand Down Expand Up @@ -686,6 +697,7 @@
t.integer "vuln_attempt_count", default: 0
t.integer "origin_id"
t.string "origin_type"
t.jsonb "resource", default: {}, null: false
t.index ["name"], name: "index_vulns_on_name"
t.index ["origin_id"], name: "index_vulns_on_origin_id"
end
Expand Down Expand Up @@ -803,4 +815,7 @@
t.boolean "limit_to_network", default: false, null: false
t.boolean "import_fingerprint", default: false
end

add_foreign_key "service_links", "services", column: "child_id"
add_foreign_key "service_links", "services", column: "parent_id"
end
71 changes: 65 additions & 6 deletions lib/msf/core/db_manager/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@ def find_or_create_service(opts)
# +:workspace+:: the workspace for the service
#
# opts may contain
# +:name+:: the application layer protocol (e.g. ssh, mssql, smb)
# +:sname+:: an alias for the above
# +:info+:: Detailed information about the service such as name and version information
# +:state+:: The current listening state of the service (one of: open, closed, filtered, unknown)
#
# +:name+:: the application layer protocol (e.g. ssh, mssql, smb)
# +:sname+:: an alias for the above
# +:info+:: detailed information about the service such as name and version information
# +:state+:: the current listening state of the service (one of: open, closed, filtered, unknown)
# +:resource+:: the resource this service is associated with, such as a a DN for an an LDAP object
# base URI for a web application, pipe name for DCERPC service, etc.
# +:parents+:: a single service Hash or an Array of service Hash representing the parent services this
# service is associated with, such as a HTTP service for a web application.
#`
# @return [Mdm::Service,nil]
def report_service(opts)
return if !active
Expand All @@ -69,6 +73,7 @@ def report_service(opts)
if opts[:sname]
opts[:name] = opts.delete(:sname)
end
opts[:name] = opts[:name].to_s.downcase if opts[:name]

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

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

service = host.services.where(port: opts[:port].to_i, proto: proto).first_or_initialize
sopts = {
port: opts[:port].to_i,
proto: proto
}
sopts[:name] = opts[:name] if opts[:name]
sopts[:resource] = opts[:resource] if opts[:resource]
service = host.services.where(sopts).first_or_initialize

ostate = service.state
opts.each { |k,v|
if (service.attribute_names.include?(k.to_s))
Expand All @@ -93,8 +105,15 @@ def report_service(opts)
dlog("Unknown attribute for Service: #{k}")
end
}

service.state ||= Msf::ServiceState::Open
service.info ||= ""
parents = process_service_chain(host, opts.delete(:parents)) if opts[:parents]
if parents
parents.each do |parent|
service.parents << parent if parent && !service.parents.include?(parent)
end
end

begin
framework.events.on_db_service(service) if service.new_record?
Expand Down Expand Up @@ -163,4 +182,44 @@ def update_service(opts)
return service
}
end

def process_service_chain(host, services)
return if services.nil? || host.nil?
return unless services.is_a?(Hash) || services.is_a?(::Array)
return unless host.is_a?(Mdm::Host)

services = [services] unless services.is_a?(Array)
services.map do |service|
return unless service.is_a?(Hash)
return if service[:port].nil? || service[:proto].nil?

parents =nil
if service[:parents]&.any?
parents = process_service_chain(host, service[:parents])
end

service_info = {
port: service[:port].to_i,
proto: service[:proto].to_s.downcase,
}
service_info[:name] = service[:name].downcase if service[:name]
service_info[:resource] = service[:resource] if service[:resource]
service_obj = host.services.find_or_create_by(service_info)
if service_obj.id.nil?
elog("Failed to create service #{service_info.inspect} for host #{host.name} (#{host.address})")
return
end
service_obj.state ||= Msf::ServiceState::Open
service_obj.info ||= ''

if parents
parents.each do |parent|
service_obj.parents << parent if parent && !service_obj.parents.include?(parent)
end
end

service_obj
end

end
end
19 changes: 14 additions & 5 deletions lib/msf/core/db_manager/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,18 +250,27 @@ def infer_vuln_from_session(session, wspace)
workspace: wspace,
}

port = session.exploit_datastore["RPORT"]
service = (port ? host.services.find_by_port(port.to_i) : nil)

vuln_info[:service] = service if service
if session.exploit.respond_to?(:service_details) && session.exploit.service_details
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This takes advantage of the #service_details method, which some modules implement. For now, the lib/msf/core/exploit/remote/http_client.rb mixin implement it. Maybe we can enforce exploit modules to implemented it in the future.

For example:
https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/http/tomcat_mgr_upload.rb#L428
https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/exploit/remote/http_client.rb#L932

service_details = session.exploit.service_details
service_name = service_details[:service_name]
port = service_details[:port]
if port.nil?
port = session.respond_to?(:target_port) && session.target_port ? session.target_port : session.exploit_datastore["RPORT"]
end
proto = service_details[:protocol]
vuln_info[:service] = host.services.find_or_create_by(name: service_name, port: port.to_i, proto: proto, state: 'open')
end
unless vuln_info[:service]
port = session.respond_to?(:target_port) && session.target_port ? session.target_port : session.exploit_datastore["RPORT"]
vuln_info[:service] = host.services.find_by_port(port.to_i) if port
end

vuln = report_vuln(vuln_info)

attempt_info = {
host: host,
module: mod_fullname,
refs: refs,
service: service,
session_id: s.id,
timestamp: Time.now.utc,
username: session.username,
Expand Down
62 changes: 52 additions & 10 deletions lib/msf/core/db_manager/vuln.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ def find_vuln_by_details(details_map, host, service=nil)
other_vulns.empty? ? nil : other_vulns.first
end

def find_vuln_by_refs(refs, host, service = nil, cve_only = true)
def find_vuln_by_refs(refs, host, service = nil, cve_only = true, resource = nil)
ref_ids = cve_only ? refs.find_all { |ref| ref.name.starts_with? 'CVE-'} : refs
relation = host.vulns.joins(:refs)
if !service.try(:id).nil?
return relation.where(service_id: service.try(:id), refs: { id: ref_ids}).first
if resource
return relation.where(service_id: service.try(:id), refs: { id: ref_ids}, resource: resource).first
else
return relation.where(service_id: service.try(:id), refs: { id: ref_ids}).first
end
end
return relation.where(refs: { id: ref_ids}).first
end
Expand Down Expand Up @@ -80,12 +84,20 @@ def has_vuln?(name)
# opts MUST contain
# +:host+:: the host where this vulnerability resides
# +:name+:: the friendly name for this vulnerability (title)
# +:workspace+:: the workspace to report this vulnerability in
#
# opts can contain
# +:info+:: a human readable description of the vuln, free-form text
# +:refs+:: an array of Ref objects or string names of references
# +:details+:: a hash with :key pointed to a find criteria hash and the rest containing VulnDetail fields
# +:sname+:: the name of the service this vulnerability relates to, used to associate it or create it.
# +:exploited_at+:: a timestamp indicating when this vulnerability was exploited, if applicable
# +:ref_ids+:: an array of reference IDs to associate with this vulnerability
# +:service+:: a Mdm::Service object or a Hash with service attributes to associate this vulnerability with
# +:port+:: the port number of the service this vulnerability relates to, if applicable
# +:proto+:: the transport layer protocol of the service this vulnerability relates to, if applicable
# +:details_match+:: a Mdm:VulnDetail with details related to this vulnerability
# +:resource+:: a resource hash to associate with this vulnerability, such as a URI or pipe name
#
def report_vuln(opts)
return if not active
Expand Down Expand Up @@ -141,7 +153,16 @@ def report_vuln(opts)
vuln = nil

# Identify the associated service
service = opts.delete(:service)
service_opt = opts.delete(:service)
case service_opt
when Mdm::Service
service = service_opt
when Hash
service = report_service(service_opt.merge(workspace: wspace, host: host))
else
dlog("Skipping service since it is not a Hash or Mdm::Service: #{service.class}")
service = nil
end

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

services = host.services.where(port: opts[:port].to_i, proto: proto)
services = services.where(name: sname) if sname.present?
service = services.first_or_create
# If sname and proto are not provided, this will assign the first service
# registered in the database for this host with the given port and proto.
# This is likely to be the TCP service.
sopts = {
workspace: wspace,
host: host,
port: opts[:port].to_i,
proto: proto
}
sopts[:name] = sname if sname.present?
service = report_service(sopts)
end

# Try to find an existing vulnerability with the same service & references
Expand All @@ -172,8 +201,12 @@ def report_vuln(opts)
# prevent dupes of the same vuln found by both local patch and
# service detection.
if rids and rids.length > 0
vuln = find_vuln_by_refs(rids, host, service)
vuln.service = service if vuln
if opts[:resource]
vuln = find_vuln_by_refs(rids, host, service, nil, opts[:resource])
else
vuln = find_vuln_by_refs(rids, host, service)
end
vuln.service = service if vuln && !vuln.service_id?
end
else
# Try to find an existing vulnerability with the same host & references
Expand All @@ -194,9 +227,17 @@ def report_vuln(opts)
# No matches, so create a new vuln record
unless vuln
if service
vuln = service.vulns.find_by_name(name)
if opts[:resource]
vuln = service.vulns.find_by(name: name, resource: opts[:resource])
else
vuln = service.vulns.find_by_name(name)
end
else
vuln = host.vulns.find_by_name(name)
if opts[:resource]
vuln = host.vulns.find_by(name: name, resource: opts[:resource])
else
vuln = host.vulns.find_by_name(name)
end
end

unless vuln
Expand All @@ -208,6 +249,7 @@ def report_vuln(opts)
}

vinf[:service_id] = service.id if service
vinf[:resource] = opts[:resource] if opts[:resource]
vuln = Mdm::Vuln.create(vinf)

begin
Expand Down
Loading
Loading