metasploit-framework/lib/rex/parser/acunetix_nokogiri.rb

407 lines
12 KiB
Ruby

# -*- coding: binary -*-
require "rex/parser/nokogiri_doc_mixin"
require 'rex'
require 'uri'
module Rex
module Parser
# If Nokogiri is available, define the Acunetix document class.
load_nokogiri && class AcunetixDocument < Nokogiri::XML::SAX::Document
include NokogiriDocMixin
# The resolver prefers your local /etc/hosts (or windows equiv), but will
# fall back to regular DNS. It retains a cache for the import to avoid
# spamming your network with DNS requests.
attr_reader :resolv_cache
# If name resolution of the host fails out completely, you will not be
# able to import that Scan task. Other scan tasks in the same report
# should be unaffected.
attr_reader :parse_warnings
def start_document
@parse_warnings = []
@resolv_cache = {}
end
def start_element(name=nil,attrs=[])
attrs = normalize_attrs(attrs)
block = @block
@state[:current_tag][name] = true
case name
when "Scan" # Start of the thing.
when "Name", "StartURL", "Banner", "Os"
@state[:has_text] = true
when "LoginSequence" # Skipping for now
when "Crawler"
record_crawler(attrs)
when "FullURL"
@state[:has_text] = true
when "Variable"
record_variable(attrs)
when "Request", "Response"
@state[:has_text] = true
end
end
def end_element(name=nil)
block = @block
case name
when "Scan"
# Clears most of the @state out, we're done with this web site.
@state.delete_if {|k| k != :current_tag}
when "Name"
@state[:has_text] = false
collect_scan_name
collect_report_item_name
@text = nil
when "StartURL" # Populates @state[:starturl_uri], we use this a lot
@state[:has_text] = false
collect_host
collect_service
@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)
end
when "Banner"
@state[:has_text] = false
collect_and_report_banner
when "Os"
@state[:has_text] = false
report_os_fingerprint
when "LoginSequence" # This comes up later in the report anyway
when "Crawler"
report_starturl_web_site(&block)
when "FullURL"
@state[:has_text] = false
report_web_site(@text,&block)
@text = nil
when "Inputs"
report_web_form(&block)
when "Request"
@state[:has_text] = false
collect_page_request
@text = nil
when "Response"
@state[:has_text] = false
collect_page_response
@text = nil
report_web_page(&block)
end
@state[:current_tag].delete name
end
def collect_page_response
return unless in_tag("TechnicalDetails")
return unless in_tag("ReportItem")
return unless @text
return if @text.to_s.empty?
@state[:page_response] = @text
end
def collect_page_request
return unless in_tag("TechnicalDetails")
return unless in_tag("ReportItem")
return unless @text
return if @text.to_s.empty?
@state[:page_request] = @text
end
def collect_scan_name
return unless in_tag("Scan")
return if in_tag("ReportItems")
return if in_tag("Crawler")
return unless @text
return if @text.strip.empty?
@state[:scan_name] = @text.strip
end
def collect_host
return unless in_tag("Scan")
return unless @text
return if @text.strip.empty?
uri = URI.parse(@text) rescue nil
return unless uri
address = resolve_scan_starturl_address(uri)
@report_data[:host] = address
@report_data[:state] = Msf::HostState::Alive
end
def collect_service
return unless @report_data[:host]
return unless in_tag("Scan")
return unless @text
return if @text.strip.empty?
uri = URI.parse(@text) rescue nil
return unless uri
@state[:starturl_uri] = uri
@report_data[:ports] ||= []
@report_data[:ports] << @state[:starturl_port]
end
def collect_and_report_banner
return unless (svc = @state[:starturl_service_object]) # Yes i want assignment
return unless @text
return if @text.strip.empty?
return unless in_tag("Scan")
svc_info = {
:host => svc.host,
:port => svc.port,
:proto => svc.proto,
:info => @text.strip
}
db_report(:service, svc_info)
@text = nil
end
def collect_report_item_name
return unless in_tag("ReportItem")
return unless @text
return if @text.strip.empty?
@state[:report_item] = @text
end
# @state[:fullurl] is set by report_web_site
def record_variable(attrs)
return unless in_tag("Inputs")
return unless @state[:fullurl].kind_of? URI
method = attr_hash(attrs)["Type"]
return unless method
return if method.strip.empty?
@state[:form_variables] ||= []
@state[:form_variables] << [attr_hash(attrs)["Name"],method]
end
def record_crawler(attrs)
return unless in_tag("Scan")
return unless @state[:starturl_service_object]
starturl = attr_hash(attrs)["StartUrl"]
return unless starturl
@state[:crawler_starturl] = starturl
end
def report_web_form(&block)
return unless in_tag("SiteFiles")
return unless @state[:web_site]
return unless @state[:fullurl].kind_of? URI
return unless @state[:form_variables].kind_of? Array
return if @state[:form_variables].empty?
method = parse_method(@state[:form_variables].first[1])
vars = @state[:form_variables].map {|x| x[0]}
form_info = {}
form_info[:web_site] = @state[:web_site]
form_info[:path] = @state[:fullurl].path
form_info[:query] = @state[:fullurl].query
form_info[:method] = method
form_info[:params] = vars
url = @state[:fullurl].to_s
db.emit(:web_form,url,&block) if block
db_report(:web_form,form_info)
@state[:fullurl] = nil
@state[:form_variables] = nil
end
def report_web_page(&block)
return if should_skip_this_page
return unless @state[:web_site]
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])
return unless path
parsed_response = parse_response(@state[:page_response])
return unless parsed_response
web_page_info = {}
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 << "://"
url << @state[:web_site].vhost.to_s << ":"
url << path
uri = URI.parse(url) rescue nil
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
# Reasons why we shouldn't collect a particular web page.
def should_skip_this_page
if @state[:report_item] =~ /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
end
return false
end
# XXX Rex::Proto::Http::Packet seems broken for
# actually parsing requests and responses, but all I
# need are the headers anyway
def parse_request(request)
headers = Rex::Proto::Http::Packet::Header.new
headers.from_s request.dup # It's destructive.
return unless headers.cmd_string
verb,req = headers.cmd_string.split(/\s+/)
return unless verb
return unless req
path,query_string = req.split(/\?/)[0,2]
end
def parse_response(response)
headers = Rex::Proto::Http::Packet::Header.new
headers.from_s response.dup # It's destructive.
return unless headers.cmd_string
http,code,msg = headers.cmd_string.split(/\s+/)
return unless code
return unless code.to_i.to_s == code
parsed = {}
parsed[:code] = code
parsed[:headers] = {}
headers.each do |k,v|
parsed[:headers][k.to_s.downcase] = []
parsed[:headers][k.to_s.downcase] << v
end
parsed[:body] = "" # We never seem to get this from Acunetix
parsed
end
# Don't cause the web report to die just because we can't tell
# what method was used -- default to GET. Sometimes it's just "POST," and
# sometimes it's "URL encoded POST," and sometimes it might be something
# else.
def parse_method(meth)
verbs = "(GET|POST|PATH)"
real_method = meth.match(/^\s*#{verbs}/)
real_method ||= meth.match(/\s*#{verbs}\s*$/)
( real_method && real_method[1] ) ? real_method[1] : "GET"
end
def report_host(&block)
return unless @report_data[:host]
return unless in_tag("Scan")
if host_is_okay
db.emit(:address,@report_data[:host],&block) if block
host_info = @report_data.merge(:workspace => @args[:wspace])
db_report(:host,host_info)
end
end
# 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
return unless @state[:starturl_uri]
name = @state[:starturl_uri].scheme
port = @state[:starturl_uri].port
addr = host_object.address
svc = {
:host => host_object,
:port => port,
:name => name.dup,
:proto => "tcp"
}
if name and port
db.emit(:service,[addr,port].join(":"),&block) if block
@state[:starturl_service_object] = db_report(:service,svc)
end
end
def report_web_site(url,&block)
return unless in_tag("Crawler")
return unless url
return if url.strip.empty?
uri = URI.parse(url) rescue nil
return unless uri
host = uri.host
port = uri.port
scheme = uri.scheme
return unless scheme[/^https?/]
return unless (host && port && scheme)
address = resolve_address(host)
return unless address
service_info = [ @args[:wspace], address, "tcp", port ]
service_object = db.get_service(*service_info)
service_object = db_report(:service,service_info) unless service_object
web_site_info = {
:workspace => @args[:wspace],
:service => service_object,
:vhost => host,
:ssl => (scheme == "https")
}
@state[:web_site] = db_report(:web_site,web_site_info)
@state[:fullurl] = uri
end
def report_starturl_web_site(&block)
return unless @state[:crawler_starturl]
starturl = @state[:crawler_starturl].dup
report_web_site(starturl,&block)
end
def report_os_fingerprint
return unless @state[:starturl_service_object]
return unless @text
return if @text.strip.empty?
return unless in_tag("Scan")
host = @state[:starturl_service_object].host
fp_note = {
:workspace => host.workspace,
:host => host,
:type => 'host.os.acunetix_fingerprint',
:data => {:os => @text}
}
db_report(:note, fp_note)
@text = nil
end
def resolve_port(uri)
@state[:port] = uri.port
unless @state[:port]
@parse_warnings << "Could not determine a port for '#{@state[:scan_name]}'"
end
@state[:port] = uri.port
end
def resolve_address(host)
return @resolv_cache[host] if @resolv_cache[host]
address = Rex::Socket.resolv_to_dotted(host) rescue nil
@resolv_cache[host] = address
return address
end
def resolve_scan_starturl_address(uri)
if uri.host
address = resolve_address(uri.host)
unless address
@parse_warnings << "Could not resolve address for '#{uri.host}', skipping '#{@state[:scan_name]}'"
end
else
@parse_warnings << "Could not determine a host for '#{@state[:scan_name]}'"
end
address
end
def handle_parse_warnings(&block)
return if @parse_warnings.empty?
@parse_warnings.each do |pwarn|
db.emit(:warning, pwarn, &block) if block
end
end
end
end
end