Acunetix XML import improvements.

This patch updates the MSF db_import functionality  w.r.t. importing Acunetix XML files to do the following:

 - import web vulnerabilities identified by Acunetix
 - import all services for each scanned host
  - does not pull in the specifc program/version name of each service, as that's pretty loosely formatted in the Acunetix XML
bug/bundler_fix
Pearce Barry 2017-04-25 09:30:31 -05:00
parent fc3a880c0d
commit c4f1130619
No known key found for this signature in database
GPG Key ID: 0916F4DEA5C5DE0A
1 changed files with 197 additions and 22 deletions

View File

@ -24,6 +24,7 @@ module Rex
def start_document
@parse_warnings = []
@resolv_cache = {}
@host_object = nil
end
def start_element(name=nil,attrs=[])
@ -32,9 +33,12 @@ module Rex
@state[:current_tag][name] = true
case name
when "Scan" # Start of the thing.
when "Name", "StartURL", "Banner", "Os"
@state[:report_item] = {}
when "Name", "StartURL", "StartTime", "Banner", "Os", "Text", "Severity", "CWE", "URL", "Parameter"
@state[:has_text] = true
when "LoginSequence" # Skipping for now
when "ReportItem"
@state[:report_item] = {}
when "Crawler"
record_crawler(attrs)
when "FullURL"
@ -62,14 +66,56 @@ module Rex
# StartURL does not always include the scheme
@text.prepend("http://") unless URI.parse(@text).scheme
collect_host
collect_service
collect_service_from_url
@text = nil
handle_parse_warnings &block
host_object = report_host &block
if host_object
report_starturl_service(host_object,&block)
db.report_import_note(@args[:wspace],host_object)
@host_object = report_host &block
if @host_object
report_starturl_service(&block)
db.report_import_note(@args[:wspace],@host_object)
end
when "StartTime"
@state[:has_text] = false
@state[:timestamp] = @text.to_s.tr!(',','').tr!('/','-')
@text = nil
when "Text"
@state[:has_text] = false
service = collect_service_from_kbitem_text
@text = nil
return unless service
handle_parse_warnings &block
if @host_object
report_kbitem_service(service,&block)
end
when "Severity"
@state[:has_text] = false
collect_report_item_severity
@text = nil
when "CWE"
@state[:has_text] = false
collect_report_item_cwe
@text = nil
when "URL"
@state[:has_text] = false
collect_report_item_reference_url
@text = nil
when "Parameter"
@state[:has_text] = false
collect_report_item_parameter
@text = nil
when "ReportItem"
vuln = collect_vuln_from_report_item
if vuln.nil?
@state[:page_request] = @state[:page_response] = nil
return
end
handle_parse_warnings &block
if @state[:vuln_info][:refs].nil?
report_web_vuln(&block)
else
report_other_vuln(&block)
end
@state[:page_request] = @state[:page_response] = nil
when "Banner"
@state[:has_text] = false
collect_and_report_banner
@ -134,7 +180,7 @@ module Rex
@report_data[:state] = Msf::HostState::Alive
end
def collect_service
def collect_service_from_url
return unless @report_data[:host]
return unless in_tag("Scan")
return unless @text
@ -146,6 +192,44 @@ module Rex
@report_data[:ports] << @state[:starturl_port]
end
def collect_service_from_kbitem_text
return unless @host_object
return unless in_tag("Scan")
return unless in_tag("KBase")
return unless in_tag("KBItem")
return unless @text
return if @text.strip.empty?
return unless @text =~ /server is running/
matched = / (?<name>\w+) server is running on (?<proto>\w+) port (?<portnum>\d+)\./.match(@text)
@report_data[:ports] ||= []
@report_data[:ports] << matched[:portnum]
return matched
end
def collect_vuln_from_report_item
@state[:vuln_info] = nil
return unless @host_object
return unless in_tag("Scan")
return unless in_tag("ReportItems")
return unless in_tag("ReportItem")
return unless @state[:report_item][:name]
return unless @state[:report_item][:severity]
return unless @state[:report_item][:severity].downcase == "high"
@state[:vuln_info] = {}
@state[:vuln_info][:name] = @state[:report_item][:name]
if @state[:page_request_verb].nil? && @state[:report_item][:name] =~ /deprecated/
# Treating this as a regular vuln, not web-specific
@state[:vuln_info][:refs] = ["ACX-#{@state[:report_item][:reference_url]}"]
unless @state[:report_item_cwe].nil?
@state[:vuln_info][:refs][0] << ",#{@state[:report_item][:cwe]}"
end
end
@state[:vuln_info][:severity] = @state[:report_item][:severity].downcase
@state[:vuln_info][:cwe] = @state[:report_item][:cwe]
return @state[:vuln_info]
end
def collect_and_report_banner
return unless (svc = @state[:starturl_service_object]) # Yes i want assignment
return unless @text
@ -165,7 +249,37 @@ module Rex
return unless in_tag("ReportItem")
return unless @text
return if @text.strip.empty?
@state[:report_item] = @text
@state[:report_item][:name] = @text
end
def collect_report_item_severity
return unless in_tag("ReportItem")
return unless @text
return if @text.strip.empty?
@state[:report_item][:severity] = @text
end
def collect_report_item_cwe
return unless in_tag("ReportItem")
return unless @text
return if @text.strip.empty?
@state[:report_item][:cwe] = @text
end
def collect_report_item_reference_url
return unless in_tag("ReportItem")
return unless in_tag("References")
return unless in_tag("Reference")
return unless @text
return if @text.strip.empty?
@state[:report_item][:reference_url] = @text
end
def collect_report_item_parameter
return unless in_tag("ReportItem")
return unless @text
return if @text.strip.empty?
@state[:report_item][:parameter] = @text
end
# @state[:fullurl] is set by report_web_site
@ -211,20 +325,26 @@ module Rex
def report_web_page(&block)
return if should_skip_this_page
return unless @state[:web_site]
@state[:page_request_verb] = nil
return unless @state[:page_request]
return if @state[:page_request].strip.empty?
return unless @state[:page_response]
return if @state[:page_response].strip.empty?
path,query_string = parse_request(@state[:page_request])
verb,path,query_string = parse_request(@state[:page_request])
return unless path
parsed_response = parse_response(@state[:page_response])
return unless parsed_response
@state[:page_request_verb] = verb
web_page_info = {}
if @state[:page_response].strip.blank?
web_page_info[:code] = ""
web_page_info[:headers] = {}
web_page_info[:body] = ""
else
parsed_response = parse_response(@state[:page_response])
return unless parsed_response
web_page_info[:code] = parsed_response[:code].to_i
web_page_info[:headers] = parsed_response[:headers]
web_page_info[:body] = parsed_response[:body]
end
web_page_info[:web_site] = @state[:web_site]
web_page_info[:path] = path
web_page_info[:code] = parsed_response[:code].to_i
web_page_info[:headers] = parsed_response[:headers]
web_page_info[:body] = parsed_response[:body]
web_page_info[:query] = query_string || ""
url = ""
url << @state[:web_site].service.name.to_s << "://"
@ -234,13 +354,51 @@ module Rex
return unless uri # Sanity checker
db.emit(:web_page, url, &block) if block
web_page_object = db_report(:web_page,web_page_info)
@state[:page_request] = @state[:page_response] = nil
@state[:web_page] = web_page_object
end
def report_web_vuln(&block)
return if should_skip_this_page
return unless @state[:web_page]
return unless @state[:web_site]
return unless @state[:vuln_info]
web_vuln_info = {}
web_vuln_info[:web_site] = @state[:web_site]
web_vuln_info[:path] = @state[:web_page][:path]
web_vuln_info[:query] = @state[:web_page][:query]
web_vuln_info[:method] = @state[:page_request_verb]
web_vuln_info[:pname] = ""
if @state[:page_response].blank?
web_vuln_info[:proof] = "<empty response>"
else
web_vuln_info[:proof] = @state[:page_response]
end
web_vuln_info[:risk] = 5
web_vuln_info[:params] = []
unless @state[:report_item][:parameter].blank?
# Acunetix only lists a single paramter...
web_vuln_info[:params] << [ @state[:report_item][:parameter].to_s, "" ]
end
web_vuln_info[:category] = "imported"
web_vuln_info[:confidence] = 100
web_vuln_info[:name] = @state[:vuln_info][:name]
db.emit(:web_vuln, web_vuln_info[:name], &block) if block
vuln = db_report(:web_vuln, web_vuln_info)
end
def report_other_vuln(&block)
return if should_skip_this_page
return unless @state[:vuln_info]
db.emit(:vuln, @state[:vuln_info][:name], &block) if block
db_report(:vuln, @state[:vuln_info].merge(:host => @host_object))
end
# Reasons why we shouldn't collect a particular web page.
def should_skip_this_page
if @state[:report_item] =~ /Unrestricted File Upload/
if @state[:report_item][:name] =~ /Unrestricted File Upload/
# This means that the page being collected is something the
# auditor put there, so it's not useful to report on.
return true
@ -259,6 +417,7 @@ module Rex
return unless verb
return unless req
path,query_string = req.split(/\?/)[0,2]
return verb,path,query_string
end
def parse_response(response)
@ -302,14 +461,14 @@ module Rex
# The service is super important, so we hang on to it for the
# rest of the scan.
def report_starturl_service(host_object,&block)
return unless host_object
def report_starturl_service(&block)
return unless @host_object
return unless @state[:starturl_uri]
name = @state[:starturl_uri].scheme
port = @state[:starturl_uri].port
addr = host_object.address
addr = @host_object.address
svc = {
:host => host_object,
:host => @host_object,
:port => port,
:name => name.dup,
:proto => "tcp"
@ -320,6 +479,22 @@ module Rex
end
end
def report_kbitem_service(service,&block)
return unless @host_object
return unless @state[:starturl_uri]
addr = @host_object.address
svc = {
:host => @host_object,
:port => service[:portnum].to_i,
:name => service[:name].dup.downcase,
:proto => service[:proto].dup.downcase
}
if service[:name] and service[:portnum]
db.emit(:service,[addr,service[:portnum]].join(":"),&block) if block
db_report(:service,svc)
end
end
def report_web_site(url,&block)
return unless in_tag("Crawler")
return unless url