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
This commit is contained in:
@@ -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
|
||||
|
||||
+20
-10
@@ -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:
|
||||
@@ -353,16 +372,6 @@ GEM
|
||||
mutex_m
|
||||
railties (~> 7.0)
|
||||
metasploit-payloads (2.0.237)
|
||||
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.7.0)
|
||||
@@ -685,6 +694,7 @@ DEPENDENCIES
|
||||
license_finder (= 5.11.1)
|
||||
memory_profiler
|
||||
metasploit-framework!
|
||||
metasploit_data_models!
|
||||
octokit
|
||||
pry-byebug
|
||||
rake
|
||||
|
||||
+17
-2
@@ -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"
|
||||
|
||||
@@ -521,6 +521,16 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_04_172657) do
|
||||
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
|
||||
@@ -530,7 +540,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_04_172657) do
|
||||
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"
|
||||
@@ -686,6 +697,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_04_172657) do
|
||||
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
|
||||
@@ -803,4 +815,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_04_172657) do
|
||||
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
|
||||
|
||||
@@ -43,11 +43,15 @@ module Msf::DBManager::Service
|
||||
# +: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
|
||||
@@ -69,6 +73,7 @@ module Msf::DBManager::Service
|
||||
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
|
||||
@@ -84,7 +89,14 @@ module Msf::DBManager::Service
|
||||
|
||||
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))
|
||||
@@ -93,8 +105,15 @@ module Msf::DBManager::Service
|
||||
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?
|
||||
@@ -163,4 +182,44 @@ module Msf::DBManager::Service
|
||||
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
|
||||
|
||||
@@ -250,10 +250,20 @@ module Msf::DBManager::Session
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -261,7 +271,6 @@ module Msf::DBManager::Session
|
||||
host: host,
|
||||
module: mod_fullname,
|
||||
refs: refs,
|
||||
service: service,
|
||||
session_id: s.id,
|
||||
timestamp: Time.now.utc,
|
||||
username: session.username,
|
||||
|
||||
@@ -44,11 +44,15 @@ module Msf::DBManager::Vuln
|
||||
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
|
||||
@@ -80,12 +84,20 @@ module Msf::DBManager::Vuln
|
||||
# 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
|
||||
@@ -141,7 +153,16 @@ module Msf::DBManager::Vuln
|
||||
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
|
||||
@@ -160,9 +181,17 @@ module Msf::DBManager::Vuln
|
||||
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
|
||||
@@ -172,8 +201,12 @@ module Msf::DBManager::Vuln
|
||||
# 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
|
||||
@@ -194,9 +227,17 @@ module Msf::DBManager::Vuln
|
||||
# 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
|
||||
@@ -208,6 +249,7 @@ module Msf::DBManager::Vuln
|
||||
}
|
||||
|
||||
vinf[:service_id] = service.id if service
|
||||
vinf[:resource] = opts[:resource] if opts[:resource]
|
||||
vuln = Mdm::Vuln.create(vinf)
|
||||
|
||||
begin
|
||||
|
||||
@@ -49,7 +49,7 @@ class Exploit < Msf::Module
|
||||
# https://docs.metasploit.com/docs/development/developing-modules/guides/how-to-write-a-check-method.html
|
||||
#
|
||||
##
|
||||
class CheckCode < Struct.new(:code, :message, :reason, :details)
|
||||
class CheckCode < Struct.new(:code, :message, :reason, :details, :vuln)
|
||||
# Do customization here because we need class constants and special
|
||||
# optional values and the block mode of Struct.new does not support that.
|
||||
#
|
||||
@@ -77,8 +77,8 @@ class Exploit < Msf::Module
|
||||
self.new('appears', reason, details: details)
|
||||
end
|
||||
|
||||
def Vulnerable(reason = nil, details: {})
|
||||
self.new('vulnerable', reason, details: details)
|
||||
def Vulnerable(reason = nil, details: {}, vuln: {})
|
||||
self.new('vulnerable', reason, details: details, vuln: vuln)
|
||||
end
|
||||
|
||||
def Unsupported(reason = nil, details: {})
|
||||
@@ -100,7 +100,7 @@ class Exploit < Msf::Module
|
||||
other.is_a?(self.class) && self.code == other.code
|
||||
end
|
||||
|
||||
def initialize(code, reason, details: {})
|
||||
def initialize(code, reason, details: {}, vuln: {})
|
||||
msg = case code
|
||||
when 'unknown'; 'Cannot reliably check exploitability.'
|
||||
when 'safe'; 'The target is not exploitable.'
|
||||
@@ -111,7 +111,7 @@ class Exploit < Msf::Module
|
||||
else
|
||||
''
|
||||
end
|
||||
super(code, "#{msg} #{reason}".strip, reason, details)
|
||||
super(code, "#{msg} #{reason}".strip, reason, details, vuln)
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
@@ -26,7 +26,7 @@ module Msf::WebServices::ServiceServlet
|
||||
begin
|
||||
sanitized_params = sanitize_params(params, env['rack.request.query_hash'])
|
||||
data = get_db.services(sanitized_params)
|
||||
includes = [:host]
|
||||
includes = [:host, :parents]
|
||||
data = data.first if is_single_object?(data, sanitized_params)
|
||||
set_json_data_response(response: data, includes: includes)
|
||||
rescue => e
|
||||
@@ -39,7 +39,7 @@ module Msf::WebServices::ServiceServlet
|
||||
lambda {
|
||||
warden.authenticate!
|
||||
job = lambda { |opts| get_db.report_service(opts) }
|
||||
includes = [:host]
|
||||
includes = [:host, :parents]
|
||||
exec_report_job(request, includes, &job)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -846,6 +846,7 @@ class Db
|
||||
output_file = nil
|
||||
set_rhosts = false
|
||||
col_search = ['port', 'proto', 'name', 'state', 'info']
|
||||
extra_columns = ['resource', 'parents']
|
||||
|
||||
names = nil
|
||||
order_by = nil
|
||||
@@ -962,7 +963,7 @@ class Db
|
||||
end
|
||||
tbl = Rex::Text::Table.new({
|
||||
'Header' => "Services",
|
||||
'Columns' => ['host'] + col_names,
|
||||
'Columns' => extra_columns.empty? ? (['host'] + col_names) : (['host'] + col_names + extra_columns),
|
||||
'SortIndex' => order_by
|
||||
})
|
||||
|
||||
@@ -976,6 +977,7 @@ class Db
|
||||
opts[:workspace] = framework.db.workspace
|
||||
opts[:hosts] = {address: host_search} if !host_search.nil?
|
||||
opts[:port] = ports if ports
|
||||
opts[:name] = names if names
|
||||
framework.db.services(opts).each do |service|
|
||||
|
||||
unless service.state == 'open'
|
||||
@@ -993,6 +995,10 @@ class Db
|
||||
end
|
||||
|
||||
columns = [host.address] + col_names.map { |n| service[n].to_s || "" }
|
||||
unless extra_columns.empty?
|
||||
columns << service.resource.to_json
|
||||
columns << service.parents.map { |parent| "#{parent.name} (#{parent.port}/#{parent.proto})"}.join(', ')
|
||||
end
|
||||
tbl << columns
|
||||
if set_rhosts
|
||||
addr = (host.scope.to_s != "" ? host.address + '%' + host.scope : host.address )
|
||||
@@ -1066,7 +1072,7 @@ class Db
|
||||
def cmd_vulns(*args)
|
||||
return unless active?
|
||||
|
||||
default_columns = ['Timestamp', 'Host', 'Name', 'References']
|
||||
default_columns = ['Timestamp', 'Host', 'Service', 'Resource', 'Name', 'References']
|
||||
host_ranges = []
|
||||
port_ranges = []
|
||||
svcs = []
|
||||
@@ -1167,6 +1173,8 @@ class Db
|
||||
row = []
|
||||
row << vuln.created_at
|
||||
row << vuln.host.address
|
||||
row << (vuln.service.present? ? "#{vuln.service.name} (#{vuln.service.port}/#{vuln.service.proto})" : 'None')
|
||||
row << vuln.resource.to_s
|
||||
row << vuln.name
|
||||
row << reflist.join(',')
|
||||
if show_info
|
||||
@@ -2362,6 +2370,15 @@ class Db
|
||||
|
||||
def _format_vulns_and_vuln_attempts(vulns)
|
||||
vulns.map.with_index do |vuln, index|
|
||||
service_str = ''
|
||||
if vuln.service.present?
|
||||
service_str << "#{vuln.service.name} (port: #{vuln.service.port}, resource: #{vuln.service.resource.to_json})"
|
||||
if vuln.service.parents.any?
|
||||
service_str << "\nParent Services:\n".indent(5)
|
||||
service_str << _print_service_parents(vuln.service).indent(7)
|
||||
end
|
||||
end
|
||||
|
||||
vuln_formatted = <<~EOF.strip.indent(2)
|
||||
#{index}. Vuln ID: #{vuln.id}
|
||||
Timestamp: #{vuln.created_at}
|
||||
@@ -2369,6 +2386,8 @@ class Db
|
||||
Name: #{vuln.name}
|
||||
References: #{vuln.refs.map {|r| r.name}.join(',')}
|
||||
Information: #{_format_vuln_value(vuln.info)}
|
||||
Resource: #{vuln.resource.to_json}
|
||||
Service: #{service_str}
|
||||
EOF
|
||||
|
||||
vuln_attempts_formatted = vuln.vuln_attempts.map.with_index do |vuln_attempt, i|
|
||||
@@ -2390,6 +2409,16 @@ class Db
|
||||
end
|
||||
end
|
||||
|
||||
def _print_service_parents(service, indent_level = 0)
|
||||
service.parents.map do |parent_service|
|
||||
parent_service_str = "#{parent_service.name} (port: #{parent_service.port}, resource: #{parent_service.resource.to_json})".indent(indent_level * 2)
|
||||
if parent_service.parents&.any?
|
||||
parent_service_str << "\n#{_print_service_parents(parent_service, indent_level + 1)}"
|
||||
end
|
||||
parent_service_str
|
||||
end.flatten.join("\n")
|
||||
end
|
||||
|
||||
def _print_vulns_and_attempts(vulns_and_attempts)
|
||||
print_line("Vulnerabilities\n===============")
|
||||
vulns_and_attempts.each do |vuln_and_attempt|
|
||||
|
||||
@@ -174,14 +174,25 @@ module ModuleCommandDispatcher
|
||||
print_line
|
||||
end
|
||||
|
||||
def report_vuln(instance)
|
||||
framework.db.report_vuln(
|
||||
def report_vuln(instance, checkcode = nil)
|
||||
opts = {
|
||||
workspace: instance.workspace,
|
||||
host: instance.rhost,
|
||||
host: instance.respond_to?(:target_host) && instance.target_host ? instance.target_host : instance.datastore['RHOST'],
|
||||
proto: instance.datastore['PROTO'] || 'tcp',
|
||||
name: instance.name,
|
||||
info: "This was flagged as vulnerable by the explicit check of #{instance.fullname}.",
|
||||
refs: instance.references
|
||||
)
|
||||
}
|
||||
|
||||
if checkcode&.kind_of?(Msf::Exploit::CheckCode) && checkcode.vuln.present?
|
||||
if checkcode.vuln.kind_of?(Array)
|
||||
checkcode.vuln.each { |vuln| framework.db.report_vuln(opts.merge(vuln)) }
|
||||
else
|
||||
framework.db.report_vuln(opts.merge(checkcode.vuln))
|
||||
end
|
||||
else
|
||||
framework.db.report_vuln(opts)
|
||||
end
|
||||
end
|
||||
|
||||
def check_simple(instance=nil)
|
||||
@@ -214,7 +225,7 @@ module ModuleCommandDispatcher
|
||||
print_good("#{peer_msg}#{code[1]}")
|
||||
# Restore RHOST for report_vuln
|
||||
instance.datastore['RHOST'] ||= rhost
|
||||
report_vuln(instance)
|
||||
report_vuln(instance, code)
|
||||
else
|
||||
print_status("#{peer_msg}#{code[1]}")
|
||||
end
|
||||
|
||||
@@ -225,6 +225,7 @@ RSpec.describe "Metasploit's json-rpc" do
|
||||
result: {
|
||||
code: 'safe',
|
||||
details: {},
|
||||
vuln: {},
|
||||
message: 'The target is not exploitable.',
|
||||
reason: nil
|
||||
}
|
||||
|
||||
@@ -195,7 +195,69 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
|
||||
|
||||
end
|
||||
|
||||
describe "#cmd_services" do
|
||||
describe "#cmd_services", if: !ENV['REMOTE_DB'] do
|
||||
context "with resource and parent" do
|
||||
before(:example) do
|
||||
framework.db.delete_host(ids: Mdm::Host.pluck(:id))
|
||||
|
||||
@services = []
|
||||
service3 = {
|
||||
host: '192.168.0.1',
|
||||
port: 1024,
|
||||
name: 'service3',
|
||||
proto: 'udp',
|
||||
resource: {base_url: '/service3'},
|
||||
parents: nil
|
||||
}
|
||||
service2 = {
|
||||
host: '192.168.0.1',
|
||||
port: 1024,
|
||||
name: 'service2',
|
||||
proto: 'udp',
|
||||
resource: {base_url: '/service2'},
|
||||
parents: service3
|
||||
}
|
||||
service1 = {
|
||||
host: '192.168.0.1',
|
||||
port: 1024,
|
||||
name: 'service1',
|
||||
proto: 'udp',
|
||||
resource: {base_url: '/service1'},
|
||||
parents: service2
|
||||
}
|
||||
|
||||
framework.db.report_service(service1)
|
||||
framework.db.report_service(service2)
|
||||
framework.db.report_service(service3)
|
||||
end
|
||||
|
||||
after(:example) do
|
||||
framework.db.delete_host(ids: Mdm::Host.pluck(:id))
|
||||
end
|
||||
|
||||
it "should list services with their resource and parent" do
|
||||
orig = RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length
|
||||
RSpec::Expectations.configuration.max_formatted_output_length = nil
|
||||
|
||||
db.cmd_services
|
||||
expect(@output).to match_array [
|
||||
"Services",
|
||||
"========",
|
||||
"",
|
||||
"host port proto name state info resource parents",
|
||||
"---- ---- ----- ---- ----- ---- -------- -------",
|
||||
"192.168.0.1 1024 udp service3 open {\"base_url\":\"/service3\"}",
|
||||
"192.168.0.1 1024 udp service1 open {\"base_url\":\"/service1\"} service2 (1024/udp)",
|
||||
"192.168.0.1 1024 udp service2 open {\"base_url\":\"/service2\"} service3 (1024/udp)"
|
||||
]
|
||||
ensure
|
||||
RSpec::Expectations.configuration.max_formatted_output_length = orig
|
||||
end
|
||||
end
|
||||
|
||||
context "with some services" do
|
||||
end
|
||||
|
||||
describe "-h" do
|
||||
it "should show a help message" do
|
||||
db.cmd_services "-h"
|
||||
@@ -244,10 +306,10 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
|
||||
"Services",
|
||||
"========",
|
||||
"",
|
||||
"host port proto name state info",
|
||||
"---- ---- ----- ---- ----- ----",
|
||||
"192.168.0.1 1024 udp service1 open",
|
||||
"192.168.0.1 1025 tcp service2 open"
|
||||
"host port proto name state info resource parents",
|
||||
"---- ---- ----- ---- ----- ---- -------- -------",
|
||||
"192.168.0.1 1024 udp service1 open {}",
|
||||
"192.168.0.1 1025 tcp service2 open {}"
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -288,6 +350,14 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
|
||||
end
|
||||
|
||||
describe "#cmd_vulns" do
|
||||
before(:example) do
|
||||
framework.db.delete_host(ids: Mdm::Host.pluck(:id))
|
||||
end
|
||||
|
||||
after(:example) do
|
||||
framework.db.delete_host(ids: Mdm::Host.pluck(:id))
|
||||
end
|
||||
|
||||
describe "-h" do
|
||||
it "should show a help message" do
|
||||
db.cmd_vulns "-h"
|
||||
@@ -314,59 +384,206 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do
|
||||
end
|
||||
|
||||
describe "-v" do
|
||||
before(:example) do
|
||||
vuln_opts = {
|
||||
updated_at: Time.utc(2025, 6, 17, 9, 17, 37),
|
||||
host: '192.168.0.1',
|
||||
name: 'ThinkPHP Multiple PHP Injection RCEs',
|
||||
info: 'Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1',
|
||||
refs: ["CVE-2018-20062"]
|
||||
}
|
||||
context 'without service' do
|
||||
before(:example) do
|
||||
vuln_opts = {
|
||||
updated_at: Time.utc(2025, 6, 17, 9, 17, 37),
|
||||
host: '192.168.0.1',
|
||||
name: 'ThinkPHP Multiple PHP Injection RCEs',
|
||||
info: 'Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1',
|
||||
refs: ["CVE-2018-20062"]
|
||||
}
|
||||
|
||||
vuln_attempt_opts = {
|
||||
id: 3,
|
||||
vuln_id: 1,
|
||||
attempted_at: Time.utc(2025, 6, 17, 9, 17, 37),
|
||||
exploited: true,
|
||||
fail_reason: nil,
|
||||
username: "foo",
|
||||
module: "exploit/unix/webapp/thinkphp_rce",
|
||||
session_id: 1,
|
||||
loot_id: nil,
|
||||
fail_detail: nil
|
||||
}
|
||||
vuln_attempt_opts = {
|
||||
id: 3,
|
||||
vuln_id: 1,
|
||||
attempted_at: Time.utc(2025, 6, 17, 9, 17, 37),
|
||||
exploited: true,
|
||||
fail_reason: nil,
|
||||
username: "foo",
|
||||
module: "exploit/unix/webapp/thinkphp_rce",
|
||||
session_id: 1,
|
||||
loot_id: nil,
|
||||
fail_detail: nil
|
||||
}
|
||||
|
||||
@vuln = framework.db.report_vuln(vuln_opts)
|
||||
@vuln_attempt = framework.db.report_vuln_attempt(@vuln, vuln_attempt_opts)
|
||||
@vuln = framework.db.report_vuln(vuln_opts)
|
||||
@vuln_attempt = framework.db.report_vuln_attempt(@vuln, vuln_attempt_opts)
|
||||
end
|
||||
|
||||
after(:example) do
|
||||
framework.db.delete_vuln({ids: [@vuln.id]})
|
||||
end
|
||||
|
||||
it "should list vulns and vuln attempts" do
|
||||
db.cmd_vulns "-v"
|
||||
expect(@output).to match_array [
|
||||
"Vulnerabilities",
|
||||
"===============",
|
||||
" 0. Vuln ID: #{@vuln.id}",
|
||||
" Timestamp: #{@vuln.created_at}",
|
||||
" Host: 192.168.0.1",
|
||||
" Name: ThinkPHP Multiple PHP Injection RCEs",
|
||||
" References: CVE-2018-20062",
|
||||
" Information: Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1",
|
||||
" Resource: {}",
|
||||
" Service:",
|
||||
" Vuln attempts:",
|
||||
" 0. ID: #{@vuln_attempt.id}",
|
||||
" Vuln ID: #{@vuln.id}",
|
||||
" Timestamp: #{@vuln_attempt.attempted_at}",
|
||||
" Exploit: true",
|
||||
" Fail reason: nil",
|
||||
" Username: foo",
|
||||
" Module: exploit/unix/webapp/thinkphp_rce",
|
||||
" Session ID: 1",
|
||||
" Loot ID: nil",
|
||||
" Fail Detail: nil",
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
after(:example) do
|
||||
framework.db.delete_vuln({ids: [@vuln.id]})
|
||||
end
|
||||
context 'with service' do
|
||||
let(:myservice) do
|
||||
{
|
||||
name: 'SRV',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv'}
|
||||
}
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
vuln_opts = {
|
||||
updated_at: Time.utc(2025, 6, 17, 9, 17, 37),
|
||||
host: '192.168.0.1',
|
||||
name: 'ThinkPHP Multiple PHP Injection RCEs',
|
||||
info: 'Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1',
|
||||
refs: ["CVE-2018-20062"],
|
||||
resource: {uri: '/thinkphp_rce'},
|
||||
service: myservice
|
||||
}
|
||||
|
||||
@vuln = framework.db.report_vuln(vuln_opts)
|
||||
end
|
||||
|
||||
after(:example) do
|
||||
framework.db.delete_vuln({ids: [@vuln.id]})
|
||||
end
|
||||
|
||||
it 'print the service with resource' do
|
||||
db.cmd_vulns "-v"
|
||||
expect(@output).to match_array [
|
||||
"Vulnerabilities",
|
||||
"===============",
|
||||
" 0. Vuln ID: #{@vuln.id}",
|
||||
" Timestamp: #{@vuln.created_at}",
|
||||
" Host: 192.168.0.1",
|
||||
" Name: ThinkPHP Multiple PHP Injection RCEs",
|
||||
" References: CVE-2018-20062",
|
||||
" Information: Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1",
|
||||
" Resource: {\"uri\":\"/thinkphp_rce\"}",
|
||||
" Service: srv (port: 80, resource: {\"base_url\":\"/srv\"})",
|
||||
" Vuln attempts:",
|
||||
]
|
||||
end
|
||||
|
||||
context 'with parent services' do
|
||||
let(:srv_0_0) do
|
||||
{
|
||||
name: 'SRV_0_0',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv_0_0'},
|
||||
parents: nil
|
||||
}
|
||||
end
|
||||
|
||||
let(:srv_0_1) do
|
||||
{
|
||||
name: 'SRV_0_1',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv_0_1'},
|
||||
parents: nil
|
||||
}
|
||||
end
|
||||
|
||||
let(:srv_0) do
|
||||
{
|
||||
name: 'SRV_0',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv_0'},
|
||||
parents: [srv_0_0, srv_0_1]
|
||||
}
|
||||
end
|
||||
|
||||
let(:srv_1_0_0) do
|
||||
{
|
||||
name: 'SRV_1_0_0',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv_1_0_0'},
|
||||
parents: nil
|
||||
}
|
||||
end
|
||||
|
||||
let(:srv_1_0) do
|
||||
{
|
||||
name: 'SRV_1_0',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv_1_0'},
|
||||
parents: srv_1_0_0
|
||||
}
|
||||
end
|
||||
|
||||
let(:srv_1) do
|
||||
{
|
||||
name: 'SRV_1',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv_1'},
|
||||
parents: srv_1_0
|
||||
}
|
||||
end
|
||||
|
||||
let(:myservice) do
|
||||
{
|
||||
name: 'SRV',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
resource: {base_url: '/srv'},
|
||||
parents: [srv_0, srv_1]
|
||||
}
|
||||
end
|
||||
|
||||
it 'print the service and the parent services' do
|
||||
db.cmd_vulns "-v"
|
||||
expect(@output).to match_array [
|
||||
"Vulnerabilities",
|
||||
"===============",
|
||||
" 0. Vuln ID: #{@vuln.id}",
|
||||
" Timestamp: #{@vuln.created_at}",
|
||||
" Host: 192.168.0.1",
|
||||
" Name: ThinkPHP Multiple PHP Injection RCEs",
|
||||
" References: CVE-2018-20062",
|
||||
" Information: Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1",
|
||||
" Resource: {\"uri\":\"/thinkphp_rce\"}",
|
||||
" Service: srv (port: 80, resource: {\"base_url\":\"/srv\"})",
|
||||
" Parent Services:",
|
||||
" srv_0 (port: 80, resource: {\"base_url\":\"/srv_0\"})",
|
||||
" srv_0_0 (port: 80, resource: {\"base_url\":\"/srv_0_0\"})",
|
||||
" srv_0_1 (port: 80, resource: {\"base_url\":\"/srv_0_1\"})",
|
||||
" srv_1 (port: 80, resource: {\"base_url\":\"/srv_1\"})",
|
||||
" srv_1_0 (port: 80, resource: {\"base_url\":\"/srv_1_0\"})",
|
||||
" srv_1_0_0 (port: 80, resource: {\"base_url\":\"/srv_1_0_0\"})",
|
||||
" Vuln attempts:",
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
it "should list vulns and vuln attempts" do
|
||||
db.cmd_vulns "-v"
|
||||
expect(@output).to match_array [
|
||||
"Vulnerabilities",
|
||||
"===============",
|
||||
" 0. Vuln ID: #{@vuln.id}",
|
||||
" Timestamp: #{@vuln.created_at}",
|
||||
" Host: 192.168.0.1",
|
||||
" Name: ThinkPHP Multiple PHP Injection RCEs",
|
||||
" References: CVE-2018-20062",
|
||||
" Information: Exploited by exploit/unix/webapp/thinkphp_rce to create Session 1",
|
||||
" Vuln attempts:",
|
||||
" 0. ID: #{@vuln_attempt.id}",
|
||||
" Vuln ID: #{@vuln.id}",
|
||||
" Timestamp: #{@vuln_attempt.attempted_at}",
|
||||
" Exploit: true",
|
||||
" Fail reason: nil",
|
||||
" Username: foo",
|
||||
" Module: exploit/unix/webapp/thinkphp_rce",
|
||||
" Session ID: 1",
|
||||
" Loot ID: nil",
|
||||
" Fail Detail: nil",
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,6 +13,11 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Exploit do
|
||||
'Name' => 'mock module',
|
||||
'Description' => 'mock module',
|
||||
'Author' => ['Unknown'],
|
||||
'References' => [
|
||||
[ 'OSVDB', '12345' ],
|
||||
[ 'EDB', '12345' ],
|
||||
[ 'CVE', '1978-1234']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Arch' => ARCH_CMD,
|
||||
'Platform' => ['unix'],
|
||||
@@ -170,6 +175,10 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Exploit do
|
||||
end
|
||||
|
||||
describe '#cmd_check' do
|
||||
after :example do
|
||||
framework.db.delete_host(ids: Mdm::Host.pluck(:id))
|
||||
end
|
||||
|
||||
context 'when checking a remote exploit module' do
|
||||
let(:current_mod) { remote_exploit_mod }
|
||||
|
||||
@@ -266,6 +275,76 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Exploit do
|
||||
|
||||
expect(@combined_output).to match_array(expected_output)
|
||||
end
|
||||
|
||||
context 'when the check returns CheckCode::Vulnerable' do
|
||||
let(:port) { 80 }
|
||||
let(:wordpress_service) do
|
||||
{
|
||||
name: 'Wordpress',
|
||||
proto: 'tcp',
|
||||
port: port,
|
||||
resource: {base_url: '/wordpress'}
|
||||
}
|
||||
end
|
||||
let(:http_service) do
|
||||
{
|
||||
name: 'HTTP',
|
||||
proto: 'tcp',
|
||||
port: port,
|
||||
resource: {method: 'GET'}
|
||||
}
|
||||
end
|
||||
let(:tcp_service) do
|
||||
{
|
||||
name: 'TCP',
|
||||
proto: 'tcp',
|
||||
port: port,
|
||||
resource: {port: port}
|
||||
}
|
||||
end
|
||||
let(:vuln) do
|
||||
{
|
||||
host: current_mod.datastore['RHOSTS'],
|
||||
port: port,
|
||||
proto: 'tcp',
|
||||
name: 'Test Vulnerability - Web',
|
||||
info: 'Mock Vulnerability',
|
||||
refs: current_mod.references,
|
||||
exploited_at: Time.now.utc,
|
||||
resource: {uri: '/myapp/from_checkcode'},
|
||||
service: wordpress_service
|
||||
}
|
||||
end
|
||||
|
||||
before :example do
|
||||
current_mod.datastore['RHOSTS'] = '192.0.2.1'
|
||||
current_mod.datastore['RPORT'] = port.to_s
|
||||
allow(current_mod).to receive(:check).and_return(Msf::Exploit::CheckCode::Vulnerable('Vulnerable!', vuln: vuln))
|
||||
end
|
||||
|
||||
it 'reports a vulnerability with the corresponding service' do
|
||||
expect { subject.cmd_check }.to change { Mdm::Vuln.count }.by(1)
|
||||
new_vuln = Mdm::Vuln.last
|
||||
expect(new_vuln.name).to eq(vuln[:name])
|
||||
expect(new_vuln.info).to eq(vuln[:info])
|
||||
expect(new_vuln.exploited_at).to be_within(1.second).of(vuln[:exploited_at])
|
||||
expect(new_vuln.resource).to eq(vuln[:resource].transform_keys(&:to_s))
|
||||
expect(new_vuln.host.address.to_s).to eq(vuln[:host])
|
||||
end
|
||||
|
||||
it 'reports each service layer associated to the vulnerability' do
|
||||
wordpress_service[:parents] = http_service
|
||||
http_service[:parents] = tcp_service
|
||||
tcp_service[:parents] = nil
|
||||
|
||||
expect { subject.cmd_check }.to change { Mdm::Service.count }.by(3)
|
||||
new_vuln = Mdm::Vuln.last
|
||||
service = new_vuln.service
|
||||
expect(service.name).to eq(wordpress_service[:name].downcase)
|
||||
expect(service.parents.first.name).to eq(http_service[:name].downcase)
|
||||
expect(service.parents.first.parents.first.name).to eq(tcp_service[:name].downcase)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when checking a non remote exploit module' do
|
||||
|
||||
@@ -18,10 +18,12 @@ RSpec.shared_examples_for 'Msf::DBManager::Service' do
|
||||
subject.report_task(workspace: workspace, user: 'test_user', info: 'info', path: 'mock/path')
|
||||
end
|
||||
|
||||
let(:host_addr) { '192.0.2.1' }
|
||||
|
||||
context 'without a task' do
|
||||
it 'creates a service' do
|
||||
service = subject.report_service(
|
||||
host: '192.0.2.1',
|
||||
host: host_addr,
|
||||
port: '5000',
|
||||
name: 'test_service',
|
||||
proto: 'tcp',
|
||||
@@ -33,7 +35,7 @@ RSpec.shared_examples_for 'Msf::DBManager::Service' do
|
||||
expect(service.port).to eq 5000
|
||||
expect(service.proto).to eq 'tcp'
|
||||
expect(service.info).to eq 'banner'
|
||||
expect(service.host.address.to_s).to eq '192.0.2.1'
|
||||
expect(service.host.address.to_s).to eq host_addr
|
||||
expect(service.host.workspace).to eq workspace
|
||||
expect(service.task_services).to be_empty
|
||||
expect(task.task_services).to be_empty
|
||||
@@ -44,7 +46,7 @@ RSpec.shared_examples_for 'Msf::DBManager::Service' do
|
||||
it 'creates a service' do
|
||||
service = 3.times.map do |count|
|
||||
subject.report_service(
|
||||
host: '192.0.2.1',
|
||||
host: host_addr,
|
||||
port: '5000',
|
||||
name: 'test_service',
|
||||
proto: 'tcp',
|
||||
@@ -58,11 +60,374 @@ RSpec.shared_examples_for 'Msf::DBManager::Service' do
|
||||
expect(service.port).to eq 5000
|
||||
expect(service.proto).to eq 'tcp'
|
||||
expect(service.info).to eq 'banner 2'
|
||||
expect(service.host.address.to_s).to eq '192.0.2.1'
|
||||
expect(service.host.address.to_s).to eq host_addr
|
||||
expect(service.host.workspace).to eq workspace
|
||||
expect(service.task_services.length).to eq 1
|
||||
expect(task.task_services.length).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not active' do
|
||||
let(:active) { false }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.report_service(host: host_addr, port: 80, proto: 'tcp', workspace: workspace)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when port is zero' do
|
||||
it 'returns nil and logs skipping' do
|
||||
expect(subject).to receive(:dlog).with(/Skipping port zero for service '.*' on host '#{host_addr}'/)
|
||||
result = subject.report_service(host: host_addr, port: 0, proto: 'tcp', workspace: workspace)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when creating a new service with required fields' do
|
||||
let(:opts) do
|
||||
{
|
||||
workspace: workspace,
|
||||
host: host_addr,
|
||||
port: 8080,
|
||||
proto: 'tcp'
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates and returns the service' do
|
||||
service = subject.report_service(opts)
|
||||
expect(service).to be_persisted
|
||||
expect(service.port).to eq(opts[:port])
|
||||
expect(service.proto).to eq(opts[:proto])
|
||||
expect(service.host.address).to eq(host_addr)
|
||||
expect(service.state).to eq(Msf::ServiceState::Open)
|
||||
expect(service.info).to be_empty
|
||||
end
|
||||
|
||||
context 'with parent services' do
|
||||
let(:service2) do
|
||||
{name: 'service2', port: 8080, proto: 'tcp'}
|
||||
end
|
||||
let(:service1) do
|
||||
{name: 'service1', port: 8080, proto: 'tcp'}
|
||||
end
|
||||
|
||||
it 'creates the service and its parent services' do
|
||||
expect {
|
||||
service1[:parents] = service2
|
||||
opts[:parents] = service1
|
||||
result = subject.report_service(opts)
|
||||
|
||||
expect(result.parents.size).to eq(1)
|
||||
expect(result.parents.first.name).to eq(service1[:name])
|
||||
expect(result.parents.first.port).to eq(service1[:port])
|
||||
expect(result.parents.first.proto).to eq(service1[:proto])
|
||||
|
||||
expect(result.parents.first.parents.size).to eq(1)
|
||||
expect(result.parents.first.parents.first.name).to eq(service2[:name])
|
||||
}.to change(Mdm::Service, :count).by(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :sname is present' do
|
||||
it 'uses :sname as :name and downcases it' do
|
||||
opts = { host: host_addr, port: 22, proto: 'tcp', workspace: workspace, sname: 'SSH' }
|
||||
service = subject.report_service(opts)
|
||||
expect(service.name).to eq('ssh')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when additional attributes are present' do
|
||||
it 'sets them on the service' do
|
||||
opts = {
|
||||
host: host_addr,
|
||||
port: 443,
|
||||
proto: 'tcp',
|
||||
workspace: workspace,
|
||||
name: 'https',
|
||||
info: 'nginx 1.18',
|
||||
state: 'open',
|
||||
resource: {uri: '/api'}
|
||||
}
|
||||
service = subject.report_service(opts)
|
||||
expect(service.name).to eq('https')
|
||||
expect(service.info).to eq('nginx 1.18')
|
||||
expect(service.state).to eq('open')
|
||||
expect(service.resource).to eq({'uri' => '/api'})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when host is not an Mdm::Host' do
|
||||
it 'calls #report_host and uses its result' do
|
||||
opts = { host: host_addr, port: 21, proto: 'tcp', workspace: workspace }
|
||||
expect(subject).to receive(:report_host).with({workspace: workspace, host: host_addr}).and_call_original
|
||||
service = subject.report_service(opts)
|
||||
expect(service.host.address).to eq(host_addr)
|
||||
expect(service.host.workspace).to eq(workspace)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with framework events' do
|
||||
before :example do
|
||||
allow(subject.framework).to receive(:events).and_return(
|
||||
double('events', on_db_host:nil, on_db_service: nil, on_db_service_state: nil)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when a new service is created' do
|
||||
it 'triggers framework events' do
|
||||
opts = { host: host_addr, port: 3306, proto: 'tcp', workspace: workspace }
|
||||
service = subject.report_service(opts)
|
||||
expect(subject.framework.events).to have_received(:on_db_service).with(service)
|
||||
expect(subject.framework.events).to have_received(:on_db_service_state).with(service, 3306, nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service state changes' do
|
||||
it 'triggers framework state change event' do
|
||||
host = FactoryBot.create(:mdm_host, address: host_addr, workspace: workspace)
|
||||
service = host.services.create!(port: 5432, proto: 'tcp', state: Msf::ServiceState::Closed)
|
||||
opts = { host: host_addr, port: 5432, proto: 'tcp', workspace: workspace, state: Msf::ServiceState::Open }
|
||||
subject.report_service(opts)
|
||||
expect(subject.framework.events).to have_received(:on_db_service_state).with(service, 5432, Msf::ServiceState::Closed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service already exists' do
|
||||
let(:host) { FactoryBot.create(:mdm_host, address: host_addr, workspace: workspace) }
|
||||
let(:existing_service) do
|
||||
host.services.create!(port: 80, proto: 'tcp', name: 'http', resource: {uri: '/api'})
|
||||
end
|
||||
let(:opts) do
|
||||
{
|
||||
workspace: workspace,
|
||||
host: host_addr,
|
||||
port: existing_service.port,
|
||||
proto: existing_service.proto,
|
||||
name: existing_service.name
|
||||
}
|
||||
end
|
||||
|
||||
context 'without resource' do
|
||||
it 'returns the existing service' do
|
||||
service = subject.report_service(opts)
|
||||
expect(service).to eq(existing_service)
|
||||
end
|
||||
end
|
||||
context 'with the same resource' do
|
||||
it 'returns the existing service' do
|
||||
opts[:resource] = existing_service.resource
|
||||
service = subject.report_service(opts)
|
||||
expect(service).to eq(existing_service)
|
||||
end
|
||||
end
|
||||
context 'with a different resource' do
|
||||
it 'creates a new service' do
|
||||
opts[:resource] = {uri: '/new'}
|
||||
service = subject.report_service(opts)
|
||||
expect(service).not_to eq(existing_service)
|
||||
expect(host.services.count).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'with parent services' do
|
||||
let(:service2) do
|
||||
{name: 'service2', port: 8080, proto: 'tcp'}
|
||||
end
|
||||
let(:service1) do
|
||||
{name: 'service1', port: 8080, proto: 'tcp'}
|
||||
end
|
||||
|
||||
it 'creates parent services and add them to the existing service' do
|
||||
# Force existing service creation
|
||||
existing_service
|
||||
|
||||
expect {
|
||||
service1[:parents] = service2
|
||||
opts[:parents] = service1
|
||||
result = subject.report_service(opts)
|
||||
|
||||
expect(result).to eq(existing_service)
|
||||
|
||||
expect(existing_service.parents.size).to eq(1)
|
||||
expect(existing_service.parents.first.name).to eq(service1[:name])
|
||||
expect(existing_service.parents.first.port).to eq(service1[:port])
|
||||
expect(existing_service.parents.first.proto).to eq(service1[:proto])
|
||||
|
||||
expect(existing_service.parents.first.parents.size).to eq(1)
|
||||
expect(existing_service.parents.first.parents.first.name).to eq(service2[:name])
|
||||
}.to change(Mdm::Service, :count).by(2)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
describe '#process_service_chain', if: !ENV['REMOTE_DB'] do
|
||||
let(:workspace) { subject.default_workspace }
|
||||
let(:host) { FactoryBot.create(:mdm_host, workspace: workspace) }
|
||||
|
||||
context 'when given valid service parameters' do
|
||||
let(:service_hash) do
|
||||
{
|
||||
name: 'http',
|
||||
port: 80,
|
||||
proto: 'tcp'
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new service if none exists' do
|
||||
expect {
|
||||
service = subject.process_service_chain(host, service_hash)
|
||||
expect(service).to be_a(Array)
|
||||
expect(service.size).to eq(1)
|
||||
expect(service.first).to be_a(Mdm::Service)
|
||||
expect(service.first.name).to eq('http')
|
||||
expect(service.first.port).to eq(80)
|
||||
expect(service.first.proto).to eq('tcp')
|
||||
expect(service.first.state).to eq(Msf::ServiceState::Open)
|
||||
}.to change(Mdm::Service, :count).by(1)
|
||||
end
|
||||
|
||||
it 'returns existing service if it already exists' do
|
||||
existing_service = FactoryBot.create(
|
||||
:mdm_service,
|
||||
host: host,
|
||||
name: service_hash[:name],
|
||||
port: service_hash[:port],
|
||||
proto: service_hash[:proto]
|
||||
)
|
||||
|
||||
expect {
|
||||
service = subject.process_service_chain(host, service_hash).first
|
||||
expect(service.id).to eq(existing_service.id)
|
||||
}.not_to change(Mdm::Service, :count)
|
||||
end
|
||||
|
||||
it 'converts service parameters to expected types' do
|
||||
service_hash = {
|
||||
name: 'HTTP', # should be downcased
|
||||
port: '80', # should be converted to integer
|
||||
proto: 'TCP' # should be downcased
|
||||
}
|
||||
|
||||
service = subject.process_service_chain(host, service_hash).first
|
||||
expect(service.name).to eq('http')
|
||||
expect(service.port).to eq(80)
|
||||
expect(service.proto).to eq('tcp')
|
||||
end
|
||||
|
||||
it 'sets the resource when provided' do
|
||||
service_hash[:resource] = 'test_resource'
|
||||
|
||||
service = subject.process_service_chain(host, service_hash).first
|
||||
expect(service.resource).to eq('test_resource')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with parent services' do
|
||||
it 'processes a single parent service' do
|
||||
parent_hash = {
|
||||
name: 'ssl',
|
||||
port: 443,
|
||||
proto: 'tcp'
|
||||
}
|
||||
|
||||
service_hash = {
|
||||
name: 'https',
|
||||
port: 443,
|
||||
proto: 'tcp',
|
||||
parents: parent_hash
|
||||
}
|
||||
|
||||
expect {
|
||||
service = subject.process_service_chain(host, service_hash).first
|
||||
expect(service.parents.count).to eq(1)
|
||||
expect(service.parents.first.name).to eq('ssl')
|
||||
expect(service.parents.first.port).to eq(443)
|
||||
}.to change(Mdm::Service, :count).by(2)
|
||||
end
|
||||
|
||||
it 'processes multiple parent services' do
|
||||
parent_hash1 = {
|
||||
name: 'https',
|
||||
port: 443,
|
||||
proto: 'tcp'
|
||||
}
|
||||
|
||||
parent_hash2 = {
|
||||
name: 'http',
|
||||
port: 80,
|
||||
proto: 'tcp'
|
||||
}
|
||||
|
||||
service_hash = {
|
||||
name: 'webapp',
|
||||
port: 80,
|
||||
proto: 'tcp',
|
||||
parents: [parent_hash1, parent_hash2]
|
||||
}
|
||||
|
||||
expect {
|
||||
service = subject.process_service_chain(host, service_hash).first
|
||||
expect(service.parents.count).to eq(2)
|
||||
expect(service.parents.map(&:name)).to include('https', 'http')
|
||||
}.to change(Mdm::Service, :count).by(3)
|
||||
end
|
||||
|
||||
it 'handles nested parent services' do
|
||||
grandparent_hash = {
|
||||
name: 'tcp',
|
||||
port: 443,
|
||||
proto: 'tcp'
|
||||
}
|
||||
|
||||
parent_hash = {
|
||||
name: 'ssl',
|
||||
port: 443,
|
||||
proto: 'tcp',
|
||||
parents: grandparent_hash
|
||||
}
|
||||
|
||||
service_hash = {
|
||||
name: 'https',
|
||||
port: 443,
|
||||
proto: 'tcp',
|
||||
parents: parent_hash
|
||||
}
|
||||
|
||||
expect {
|
||||
service = subject.process_service_chain(host, service_hash).first
|
||||
parent = service.parents.first
|
||||
expect(parent.name).to eq('ssl')
|
||||
expect(parent.parents.first.name).to eq('tcp')
|
||||
}.to change(Mdm::Service, :count).by(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
it 'returns nil if service hash is nil' do
|
||||
expect(subject.process_service_chain(host, nil)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil if host is nil' do
|
||||
service_hash = { name: 'http', port: 80, proto: 'tcp' }
|
||||
expect(subject.process_service_chain(nil, service_hash)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil if required service parameters are missing' do
|
||||
# Missing port
|
||||
expect(subject.process_service_chain(host, { name: 'http', proto: 'tcp' })).to be_nil
|
||||
|
||||
# Missing proto
|
||||
expect(subject.process_service_chain(host, { name: 'http', port: 80 })).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
@@ -240,10 +240,19 @@ RSpec.shared_examples_for 'Msf::DBManager::Session' do
|
||||
nil
|
||||
end
|
||||
|
||||
let(:service_details) { nil }
|
||||
let(:target_port) { nil }
|
||||
|
||||
before(:example) do
|
||||
Timecop.freeze
|
||||
|
||||
session.exploit_datastore['RPORT'] = rport
|
||||
if service_details
|
||||
allow(session.exploit).to receive(:service_details).and_return(service_details)
|
||||
end
|
||||
if target_port
|
||||
allow(session).to receive(:target_port).and_return(target_port)
|
||||
end
|
||||
|
||||
report_session
|
||||
end
|
||||
@@ -322,6 +331,43 @@ RSpec.shared_examples_for 'Msf::DBManager::Session' do
|
||||
context 'without RPORT' do
|
||||
it { expect(subject.service).to be_nil }
|
||||
end
|
||||
|
||||
context 'with session.exploit implementing #service_details' do
|
||||
let(:service_details) do
|
||||
{
|
||||
:service_name => 'HTTP',
|
||||
:port => 80,
|
||||
:protocol => 'tcp'
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a Mdm::Service with the data provided by #service_details' do
|
||||
expect(subject.service).to be_present
|
||||
expect(subject.service).to be_a(Mdm::Service)
|
||||
expect(subject.service.name).to eq(service_details[:service_name])
|
||||
expect(subject.service.port).to eq(service_details[:port])
|
||||
expect(subject.service.proto).to eq(service_details[:protocol])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service_details does not provide port information and session has a target_port' do
|
||||
let(:service_details) do
|
||||
{
|
||||
:service_name => 'HTTP',
|
||||
:protocol => 'tcp'
|
||||
}
|
||||
end
|
||||
let(:target_port) { rand(2**16 - 1) }
|
||||
|
||||
it 'create a service wth port from session.target_port' do
|
||||
expect(subject.service).to be_present
|
||||
expect(subject.service).to be_a(Mdm::Service)
|
||||
expect(subject.service.name).to eq(service_details[:service_name])
|
||||
expect(subject.service.port).to eq(session.target_port)
|
||||
expect(subject.service.proto).to eq(service_details[:protocol])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'created Mdm::ExploitAttempt' do
|
||||
|
||||
@@ -49,4 +49,373 @@ RSpec.shared_examples_for 'Msf::DBManager::Vuln' do
|
||||
end
|
||||
|
||||
it { is_expected.to respond_to :vulns }
|
||||
|
||||
|
||||
describe '#find_vuln_by_refs', if: !ENV['REMOTE_DB'] do
|
||||
let(:workspace) { subject.default_workspace }
|
||||
let(:host_addr) { '192.0.2.1' }
|
||||
let(:host) { FactoryBot.create(:mdm_host, address: host_addr, workspace: workspace) }
|
||||
let(:service1) { host.services.create!(port: 5432, proto: 'tcp') }
|
||||
let(:service2) { host.services.create!(port: 80, proto: 'tcp') }
|
||||
let(:ref_cve1) { FactoryBot.create(:mdm_ref, name: 'CVE-2023-0001') }
|
||||
let(:ref_cve2) { FactoryBot.create(:mdm_ref, name: 'CVE-2023-0002') }
|
||||
let(:ref_ms) { FactoryBot.create(:mdm_ref, name: 'MS-1234') }
|
||||
let!(:vuln1) { service1.vulns.create!(host: host, name: 'Vuln1', resource: {uri: '/api'}, refs: [ref_cve1, ref_ms]) }
|
||||
let!(:vuln2) { service1.vulns.create!(host: host, name: 'Vuln2', resource: {uri: '/other'}, refs: [ref_cve2]) }
|
||||
let!(:vuln3) { FactoryBot.create(:mdm_vuln, host: host, name: 'Vuln3', refs: [ref_cve1, ref_cve2]) }
|
||||
let!(:vuln4) { service2.vulns.create!(host: host, name: 'Vuln4', resource: {uri: '/api'}, refs: [ref_ms]) }
|
||||
|
||||
context 'when cve_only is true' do
|
||||
it 'finds vuln by service and CVE ref' do
|
||||
expect(subject.find_vuln_by_refs([ref_cve1, ref_ms], host, service1, true)).to eq(vuln1)
|
||||
end
|
||||
|
||||
it 'returns nil if no CVE refs match' do
|
||||
expect(subject.find_vuln_by_refs([ref_ms], host, service1, true)).to be_nil
|
||||
end
|
||||
|
||||
it 'finds vuln by CVE ref through host when no service is provided' do
|
||||
expect(subject.find_vuln_by_refs([ref_cve2], host, nil, true)).to eq(vuln2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cve_only is false' do
|
||||
it 'finds vuln by service and any ref' do
|
||||
expect(subject.find_vuln_by_refs([ref_ms], host, service2, false)).to eq(vuln4)
|
||||
end
|
||||
|
||||
it 'finds vuln by any ref through host when no service is provided' do
|
||||
expect(subject.find_vuln_by_refs([ref_ms], host, nil, false)).to eq(vuln1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when resource is specified' do
|
||||
it 'finds vuln by service, ref, and resource' do
|
||||
expect(subject.find_vuln_by_refs([ref_cve1], host, service1, true, {uri: '/api'})).to eq(vuln1)
|
||||
end
|
||||
|
||||
it 'returns nil if resource does not match' do
|
||||
expect(subject.find_vuln_by_refs([ref_cve1], host, service1, true, {uri: '/other'})).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no vulns match' do
|
||||
it 'returns nil' do
|
||||
ref_unknown = Mdm::Ref.new(name: 'CVE-9999-9999')
|
||||
expect(subject.find_vuln_by_refs([ref_unknown], host, service1, true)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when refs is empty' do
|
||||
it 'returns nil' do
|
||||
expect(subject.find_vuln_by_refs([], host, service1, true)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#report_vuln' , if: !ENV['REMOTE_DB']do
|
||||
let(:workspace) { subject.default_workspace }
|
||||
let(:host) { FactoryBot.create(:mdm_host, workspace: workspace) }
|
||||
let(:service) { FactoryBot.create(:mdm_service, host: host) }
|
||||
let(:ref1) { FactoryBot.create(:mdm_module_ref, name: 'CVE-2023-0001') }
|
||||
let(:ref2) { FactoryBot.create(:mdm_module_ref, name: 'MS-1234') }
|
||||
|
||||
context 'when :host is missing' do
|
||||
it 'raises error' do
|
||||
expect { subject.report_vuln(name: 'foo') }.to raise_error(ArgumentError, /Missing required option :host/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :data is present' do
|
||||
it 'raises error' do
|
||||
expect { subject.report_vuln(host: host, name: 'foo', data: 'deprecated') }.to raise_error(ArgumentError, /Deprecated data column/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not active' do
|
||||
let(:active) { false }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.report_vuln(host: double('host'), name: 'foo')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no vuln exists' do
|
||||
it 'creates a new vuln' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', info: 'desc', workspace: workspace)
|
||||
expect(result).to be_a(Mdm::Vuln)
|
||||
expect(result.name).to eq('foo')
|
||||
expect(result.info).to eq('desc')
|
||||
expect(host.vulns).to include(result)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the vuln already exists' do
|
||||
let(:name) { 'existing vuln' }
|
||||
let!(:existing_vuln) { FactoryBot.create(:mdm_vuln, host: host, name: name) }
|
||||
|
||||
it 'returns the vuln with the same name' do
|
||||
result = subject.report_vuln(host: host, name: name, workspace: workspace)
|
||||
expect(result.id).to eq(existing_vuln.id)
|
||||
expect(result.name).to eq(existing_vuln.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with refs' do
|
||||
it 'adds `Mdm::Module::Ref` refs' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, refs: [ref1, ref2])
|
||||
expect(result.refs.size).to eq(2)
|
||||
expect(result.refs.map(&:name)).to include(ref1.name, ref2.name)
|
||||
end
|
||||
|
||||
it 'adds `Msf::Module::SiteReference` refs' do
|
||||
ref1 = Msf::Module::SiteReference.from_a(['CVE', '1978-1234'])
|
||||
ref2 = Msf::Module::SiteReference.from_a(['URL', 'http://example.com'])
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, refs: [ref1, ref2])
|
||||
expect(result.refs.size).to eq(2)
|
||||
expect(result.refs.map(&:name)).to include("#{ref1.ctx_id}-#{ref1.ctx_val}", "#{ref2.ctx_id}-#{ref2.ctx_val}")
|
||||
end
|
||||
|
||||
it 'adds refs as Hash' do
|
||||
ref1 = {ctx_id: 'CVE', ctx_val: '1978-1234'}
|
||||
ref2 = {ctx_id: 'URL', ctx_val: 'http://example.com'}
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, refs: [ref1, ref2])
|
||||
expect(result.refs.size).to eq(2)
|
||||
expect(result.refs.map(&:name)).to include("#{ref1[:ctx_id]}-#{ref1[:ctx_val]}", "#{ref2[:ctx_id]}-#{ref2[:ctx_val]}")
|
||||
end
|
||||
|
||||
it 'adds refs as String' do
|
||||
ref1 = 'CVE-1978-1234'
|
||||
ref2 = 'http://example.com'
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, refs: [ref1, ref2])
|
||||
expect(result.refs.size).to eq(2)
|
||||
expect(result.refs.map(&:name)).to include(ref1, ref2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with name and info' do
|
||||
it 'sets them and truncates if too long' do
|
||||
long_info = 'a' * 70000
|
||||
long_name = 'b' * 300
|
||||
result = subject.report_vuln(host: host, name: long_name, info: long_info, workspace: workspace)
|
||||
expect(result.name.length).to eq(255)
|
||||
expect(result.info.length).to eq(65535)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with exploited_at' do
|
||||
it 'sets exploited_at' do
|
||||
now = Time.now
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, exploited_at: now)
|
||||
expect(result.exploited_at).to eq(now)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with service as Mdm::Service' do
|
||||
it 'sets service' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service)
|
||||
expect(result.service).to eq(service)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with service as Hash' do
|
||||
let (:service_hash) { {name: service.name, port: service.port, proto: service.proto} }
|
||||
|
||||
it 'sets service' do
|
||||
expect {
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service_hash)
|
||||
expect(result.service).to eq(service)
|
||||
}.to change(Mdm::Service, :count).by(1)
|
||||
end
|
||||
|
||||
context 'with parent services' do
|
||||
let(:service2) do
|
||||
{name: 'service2', port: 8080, proto: 'tcp'}
|
||||
end
|
||||
let(:service1) do
|
||||
{name: 'service1', port: 8080, proto: 'tcp'}
|
||||
end
|
||||
|
||||
context 'with an existing service' do
|
||||
it 'creates parent services and add them to the existing service' do
|
||||
# Force service creation
|
||||
service
|
||||
|
||||
expect {
|
||||
service1[:parents] = service2
|
||||
service_hash[:parents] = service1
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service_hash)
|
||||
|
||||
expect(result.service).to eq(service)
|
||||
|
||||
expect(service.parents.size).to eq(1)
|
||||
expect(service.parents.first.name).to eq(service1[:name])
|
||||
expect(service.parents.first.port).to eq(service1[:port])
|
||||
expect(service.parents.first.proto).to eq(service1[:proto])
|
||||
|
||||
expect(service.parents.first.parents.size).to eq(1)
|
||||
expect(service.parents.first.parents.first.name).to eq(service2[:name])
|
||||
}.to change(Mdm::Service, :count).by(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a non-existing service' do
|
||||
it 'creates the service and its parent services' do
|
||||
expect {
|
||||
service1[:parents] = service2
|
||||
service_hash = {
|
||||
name: 'other service',
|
||||
port: 8080,
|
||||
proto: 'tcp',
|
||||
parents: service1
|
||||
}
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service_hash)
|
||||
|
||||
expect(result.service).to be_a(Mdm::Service)
|
||||
expect(result.service.name).to eq(service_hash[:name])
|
||||
expect(result.service.port).to eq(service_hash[:port])
|
||||
expect(result.service.proto).to eq(service_hash[:proto])
|
||||
|
||||
expect(result.service.parents.size).to eq(1)
|
||||
expect(result.service.parents.first.name).to eq(service1[:name])
|
||||
|
||||
expect(result.service.parents.first.parents.size).to eq(1)
|
||||
expect(result.service.parents.first.parents.first.name).to eq(service2[:name])
|
||||
}.to change(Mdm::Service, :count).by(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with service and refs' do
|
||||
let!(:vuln) do
|
||||
ref = FactoryBot.create(:mdm_ref, name: ref1.name)
|
||||
service.vulns.create!(host: host, name: 'foo', refs: [ref])
|
||||
end
|
||||
|
||||
it 'returns an existing vuln if service and refs match' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service, refs: [ref1])
|
||||
expect(result).to eq(vuln)
|
||||
end
|
||||
|
||||
context 'with resource' do
|
||||
let(:resource) { {uri: '/api'} }
|
||||
|
||||
it 'returns an existing vuln with the same service, refs and resource' do
|
||||
vuln.update!(resource: resource)
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service, refs: [ref1], resource: resource)
|
||||
expect(result).to eq(vuln)
|
||||
end
|
||||
|
||||
it 'creates a new vuln if resource does not match' do
|
||||
new_resource = {uri: '/other'}
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service, refs: [ref1], resource: new_resource)
|
||||
expect(result).not_to eq(vuln)
|
||||
expect(result.resource).to eq(new_resource.transform_keys(&:to_s))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with service and resource' do
|
||||
let(:resource) { {uri: '/api'} }
|
||||
let!(:vuln) { service.vulns.create!(host: host, name: 'foo', resource: resource) }
|
||||
|
||||
it 'returns an existing vuln if service, name and resource match' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, service: service, resource: resource)
|
||||
expect(result).to eq(vuln)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without service' do
|
||||
it 'does not set any service' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace)
|
||||
expect(result.service).to be_nil
|
||||
end
|
||||
|
||||
context 'with port' do
|
||||
let(:port) { 8080 }
|
||||
|
||||
it 'creates a service with the given port' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, port: port)
|
||||
expect(result.service).not_to be_nil
|
||||
expect(result.service.port).to eq(port)
|
||||
expect(result.service.proto).to eq('tcp') # default proto
|
||||
end
|
||||
|
||||
context 'with proto' do
|
||||
let(:proto) { 'udp' }
|
||||
|
||||
it 'creates a service with the given port and proto' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, port: port, proto: proto)
|
||||
expect(result.service).not_to be_nil
|
||||
expect(result.service.port).to eq(port)
|
||||
expect(result.service.proto).to eq(proto)
|
||||
end
|
||||
|
||||
context 'with sname' do
|
||||
let(:sname) { 'myservice' }
|
||||
|
||||
it 'creates a service with the given port, proto and sname' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, port: port, proto: proto, sname: sname)
|
||||
expect(result.service).not_to be_nil
|
||||
expect(result.service.port).to eq(port)
|
||||
expect(result.service.proto).to eq(proto)
|
||||
expect(result.service.name).to eq(sname)
|
||||
end
|
||||
|
||||
it 'returns the service if it already exists' do
|
||||
existing_service = FactoryBot.create(:mdm_service, host: host, port: port, proto: proto, name: sname)
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, port: port, proto: proto, sname: sname)
|
||||
expect(result.service).to eq(existing_service)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with resource' do
|
||||
let(:resource) { {uri: '/api'} }
|
||||
|
||||
it 'creates a vuln with the resource' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, resource: resource)
|
||||
expect(result.resource).to eq(resource.transform_keys(&:to_s))
|
||||
end
|
||||
|
||||
it 'returns an existing vuln if resource matches' do
|
||||
existing_vuln = host.vulns.create!(name: 'foo', resource: resource)
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, resource: resource)
|
||||
expect(result).to eq(existing_vuln)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with vuln details' do
|
||||
let(:vuln_details) { { description: 'desc', proof: 'proof'} }
|
||||
|
||||
it 'sets vuln details' do
|
||||
result = subject.report_vuln(host: host, name: 'foo', workspace: workspace, details: vuln_details)
|
||||
expect(result.vuln_details.size).to eq(1)
|
||||
expect(result.vuln_details.first).to be_a(Mdm::VulnDetail)
|
||||
expect(result.vuln_details.first.description).to eq(vuln_details[:description])
|
||||
expect(result.vuln_details.first.proof).to eq(vuln_details[:proof])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with framework events' do
|
||||
before :example do
|
||||
allow(subject.framework).to receive(:events).and_return(
|
||||
double('events', on_db_vuln:nil)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when a new vuln is created' do
|
||||
it 'triggers framework events' do
|
||||
vuln = subject.report_vuln(host: host, name: 'foo', workspace: workspace)
|
||||
expect(subject.framework.events).to have_received(:on_db_vuln).with(vuln)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user