Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/parser/acunetix_document.rb
Views: 11779
# -*- coding: binary -*-1require "rex/parser/nokogiri_doc_mixin"2require 'rex'3require 'uri'45module Rex6module Parser78# If Nokogiri is available, define the Acunetix document class.9load_nokogiri && class AcunetixDocument < Nokogiri::XML::SAX::Document1011include NokogiriDocMixin1213# The resolver prefers your local /etc/hosts (or windows equiv), but will14# fall back to regular DNS. It retains a cache for the import to avoid15# spamming your network with DNS requests.16attr_reader :resolv_cache1718# If name resolution of the host fails out completely, you will not be19# able to import that Scan task. Other scan tasks in the same report20# should be unaffected.21attr_reader :parse_warnings2223def start_document24@parse_warnings = []25@resolv_cache = {}26@host_object = nil27end2829def start_element(name=nil,attrs=[])30attrs = normalize_attrs(attrs)31block = @block32@state[:current_tag][name] = true33case name34when "Scan" # Start of the thing.35@state[:report_item] = {}36when "Name", "StartURL", "StartTime", "Banner", "Os", "Text", "Severity", "CWE", "URL", "Parameter"37@state[:has_text] = true38when "LoginSequence" # Skipping for now39when "ReportItem"40@state[:report_item] = {}41when "Crawler"42record_crawler(attrs)43when "FullURL"44@state[:has_text] = true45when "Variable"46record_variable(attrs)47when "Request", "Response"48@state[:has_text] = true49end50end5152def end_element(name=nil)53block = @block54case name55when "Scan"56# Clears most of the @state out, we're done with this web site.57@state.delete_if {|k| k != :current_tag}58when "Name"59@state[:has_text] = false60collect_scan_name61collect_report_item_name62@text = nil63when "StartURL" # Populates @state[:starturl_uri], we use this a lot64@state[:has_text] = false65# StartURL does not always include the scheme66@text.prepend("http://") unless URI.parse(@text).scheme67collect_host68collect_service_from_url69@text = nil70handle_parse_warnings &block71@host_object = report_host &block72if @host_object73report_starturl_service(&block)74db.report_import_note(@args[:workspace],@host_object)75end76when "StartTime"77@state[:has_text] = false78@state[:timestamp] = @text.to_time79@text = nil80when "Text"81@state[:has_text] = false82service = collect_service_from_kbitem_text83@text = nil84return unless service85handle_parse_warnings &block86if @host_object87report_kbitem_service(service,&block)88end89when "Severity"90@state[:has_text] = false91collect_report_item_severity92@text = nil93when "CWE"94@state[:has_text] = false95collect_report_item_cwe96@text = nil97when "URL"98@state[:has_text] = false99collect_report_item_reference_url100@text = nil101when "Parameter"102@state[:has_text] = false103collect_report_item_parameter104@text = nil105when "ReportItem"106vuln = collect_vuln_from_report_item107if vuln.nil?108@state[:page_request] = @state[:page_response] = nil109return110end111handle_parse_warnings &block112if @state[:vuln_info][:refs].nil?113report_web_vuln(&block)114else115report_other_vuln(&block)116end117@state[:page_request] = @state[:page_response] = nil118when "Banner"119@state[:has_text] = false120collect_and_report_banner121when "Os"122@state[:has_text] = false123report_os_fingerprint124when "LoginSequence" # This comes up later in the report anyway125when "Crawler"126report_starturl_web_site(&block)127when "FullURL"128@state[:has_text] = false129report_web_site(@text,&block)130@text = nil131when "Inputs"132report_web_form(&block)133when "Request"134@state[:has_text] = false135collect_page_request136@text = nil137when "Response"138@state[:has_text] = false139collect_page_response140@text = nil141report_web_page(&block)142end143@state[:current_tag].delete name144end145146def collect_page_response147return unless in_tag("TechnicalDetails")148return unless in_tag("ReportItem")149return unless @text150return if @text.to_s.empty?151@state[:page_response] = @text152end153154def collect_page_request155return unless in_tag("TechnicalDetails")156return unless in_tag("ReportItem")157return unless @text158return if @text.to_s.empty?159@state[:page_request] = @text160end161162def collect_scan_name163return unless in_tag("Scan")164return if in_tag("ReportItems")165return if in_tag("Crawler")166return unless @text167return if @text.strip.empty?168@state[:scan_name] = @text.strip169end170171def collect_host172return unless in_tag("Scan")173return unless @text174return if @text.strip.empty?175uri = URI.parse(@text) rescue nil176return unless uri177address = resolve_scan_starturl_address(uri)178@report_data[:host] = address179@report_data[:state] = Msf::HostState::Alive180end181182def collect_service_from_url183return unless @report_data[:host]184return unless in_tag("Scan")185return unless @text186return if @text.strip.empty?187uri = URI.parse(@text) rescue nil188return unless uri189@state[:starturl_uri] = uri190@report_data[:ports] ||= []191@report_data[:ports] << @state[:starturl_port]192end193194def collect_service_from_kbitem_text195return unless @host_object196return unless in_tag("Scan")197return unless in_tag("KBase")198return unless in_tag("KBItem")199return unless @text200return if @text.strip.empty?201return unless @text =~ /server is running/202matched = / (?<name>\w+) server is running on (?<proto>\w+) port (?<portnum>\d+)\./.match(@text)203@report_data[:ports] ||= []204@report_data[:ports] << matched[:portnum]205return matched206end207208def collect_vuln_from_report_item209@state[:vuln_info] = nil210return unless @host_object211return unless in_tag("Scan")212return unless in_tag("ReportItems")213return unless in_tag("ReportItem")214return unless @state[:report_item][:name]215return unless @state[:report_item][:severity]216return unless @state[:report_item][:severity].downcase == "high"217218@state[:vuln_info] = {}219@state[:vuln_info][:name] = @state[:report_item][:name]220if @state[:page_request_verb].nil? && @state[:report_item][:name] =~ /deprecated/221# Treating this as a regular vuln, not web-specific222@state[:vuln_info][:refs] = ["ACX-#{@state[:report_item][:reference_url]}"]223unless @state[:report_item_cwe].nil?224@state[:vuln_info][:refs][0] << ",#{@state[:report_item][:cwe]}"225end226end227@state[:vuln_info][:severity] = @state[:report_item][:severity].downcase228@state[:vuln_info][:cwe] = @state[:report_item][:cwe]229return @state[:vuln_info]230end231232def collect_and_report_banner233return unless (svc = @state[:starturl_service_object]) # Yes i want assignment234return unless @text235return if @text.strip.empty?236return unless in_tag("Scan")237svc_info = {238:host => svc.host,239:port => svc.port,240:proto => svc.proto,241:info => @text.strip242}243db_report(:service, svc_info)244@text = nil245end246247def collect_report_item_name248return unless in_tag("ReportItem")249return unless @text250return if @text.strip.empty?251@state[:report_item][:name] = @text252end253254def collect_report_item_severity255return unless in_tag("ReportItem")256return unless @text257return if @text.strip.empty?258@state[:report_item][:severity] = @text259end260261def collect_report_item_cwe262return unless in_tag("ReportItem")263return unless @text264return if @text.strip.empty?265@state[:report_item][:cwe] = @text266end267268def collect_report_item_reference_url269return unless in_tag("ReportItem")270return unless in_tag("References")271return unless in_tag("Reference")272return unless @text273return if @text.strip.empty?274@state[:report_item][:reference_url] = @text275end276277def collect_report_item_parameter278return unless in_tag("ReportItem")279return unless @text280return if @text.strip.empty?281@state[:report_item][:parameter] = @text282end283284# @state[:fullurl] is set by report_web_site285def record_variable(attrs)286return unless in_tag("Inputs")287return unless @state[:fullurl].kind_of? URI288method = attr_hash(attrs)["Type"]289return unless method290return if method.strip.empty?291@state[:form_variables] ||= []292@state[:form_variables] << [attr_hash(attrs)["Name"],method]293end294295def record_crawler(attrs)296return unless in_tag("Scan")297return unless @state[:starturl_service_object]298starturl = attr_hash(attrs)["StartUrl"]299return unless starturl300@state[:crawler_starturl] = starturl301end302303def report_web_form(&block)304return unless in_tag("SiteFiles")305return unless @state[:web_site]306return unless @state[:fullurl].kind_of? URI307return unless @state[:form_variables].kind_of? Array308return if @state[:form_variables].empty?309method = parse_method(@state[:form_variables].first[1])310vars = @state[:form_variables].map {|x| x[0]}311form_info = {}312form_info[:web_site] = @state[:web_site]313form_info[:path] = @state[:fullurl].path314form_info[:query] = @state[:fullurl].query315form_info[:method] = method316form_info[:params] = vars317url = @state[:fullurl].to_s318db.emit(:web_form,url,&block) if block319db_report(:web_form,form_info)320@state[:fullurl] = nil321@state[:form_variables] = nil322end323324def report_web_page(&block)325return if should_skip_this_page326return unless @state[:web_site]327@state[:page_request_verb] = nil328return unless @state[:page_request]329return if @state[:page_request].strip.empty?330verb,path,query_string = parse_request(@state[:page_request])331return unless path332@state[:page_request_verb] = verb333web_page_info = {}334if @state[:page_response].strip.blank?335web_page_info[:code] = ""336web_page_info[:headers] = {}337web_page_info[:body] = ""338else339parsed_response = parse_response(@state[:page_response])340return unless parsed_response341web_page_info[:code] = parsed_response[:code].to_i342web_page_info[:headers] = parsed_response[:headers]343web_page_info[:body] = parsed_response[:body]344end345web_page_info[:web_site] = @state[:web_site]346web_page_info[:path] = path347web_page_info[:query] = query_string || ""348url = ""349url << @state[:web_site].service.name.to_s << "://"350url << @state[:web_site].vhost.to_s << ":"351url << path352uri = URI.parse(url) rescue nil353return unless uri # Sanity checker354db.emit(:web_page, url, &block) if block355web_page_object = db_report(:web_page,web_page_info)356@state[:web_page] = web_page_object357end358359def report_web_vuln(&block)360return if should_skip_this_page361return unless @state[:web_page]362return unless @state[:web_site]363return unless @state[:vuln_info]364365web_vuln_info = {}366web_vuln_info[:web_site] = @state[:web_site]367web_vuln_info[:path] = @state[:web_page][:path]368web_vuln_info[:query] = @state[:web_page][:query]369web_vuln_info[:method] = @state[:page_request_verb]370web_vuln_info[:pname] = ""371if @state[:page_response].blank?372web_vuln_info[:proof] = "<empty response>"373else374web_vuln_info[:proof] = @state[:page_response]375end376web_vuln_info[:risk] = 5377web_vuln_info[:params] = []378unless @state[:report_item][:parameter].blank?379# Acunetix only lists a single parameter...380web_vuln_info[:params] << [ @state[:report_item][:parameter].to_s, "" ]381end382web_vuln_info[:category] = "imported"383web_vuln_info[:confidence] = 100384web_vuln_info[:name] = @state[:vuln_info][:name]385386db.emit(:web_vuln, web_vuln_info[:name], &block) if block387vuln = db_report(:web_vuln, web_vuln_info)388end389390def report_other_vuln(&block)391return if should_skip_this_page392return unless @state[:vuln_info]393394db.emit(:vuln, @state[:vuln_info][:name], &block) if block395db_report(:vuln, @state[:vuln_info].merge(:host => @host_object))396end397398# Reasons why we shouldn't collect a particular web page.399def should_skip_this_page400if @state[:report_item][:name] =~ /Unrestricted File Upload/401# This means that the page being collected is something the402# auditor put there, so it's not useful to report on.403return true404end405return false406end407408# XXX Rex::Proto::Http::Packet seems broken for409# actually parsing requests and responses, but all I410# need are the headers anyway411def parse_request(request)412headers = Rex::Proto::Http::Packet::Header.new413headers.from_s request.dup # It's destructive.414return unless headers.cmd_string415verb,req = headers.cmd_string.split(/\s+/)416return unless verb417return unless req418path,query_string = req.split(/\?/)[0,2]419return verb,path,query_string420end421422def parse_response(response)423headers = Rex::Proto::Http::Packet::Header.new424headers.from_s response.dup # It's destructive.425return unless headers.cmd_string426http,code,msg = headers.cmd_string.split(/\s+/)427return unless code428return unless code.to_i.to_s == code429parsed = {}430parsed[:code] = code431parsed[:headers] = {}432headers.each do |k,v|433parsed[:headers][k.to_s.downcase] = []434parsed[:headers][k.to_s.downcase] << v435end436parsed[:body] = "" # We never seem to get this from Acunetix437parsed438end439440# Don't cause the web report to die just because we can't tell441# what method was used -- default to GET. Sometimes it's just "POST," and442# sometimes it's "URL encoded POST," and sometimes it might be something443# else.444def parse_method(meth)445verbs = "(GET|POST|PATH)"446real_method = meth.match(/^\s*#{verbs}/)447real_method ||= meth.match(/\s*#{verbs}\s*$/)448( real_method && real_method[1] ) ? real_method[1] : "GET"449end450451def report_host(&block)452return unless @report_data[:host]453return unless in_tag("Scan")454if host_is_okay455db.emit(:address,@report_data[:host],&block) if block456host_info = @report_data.merge(:workspace => @args[:workspace])457db_report(:host,host_info)458end459end460461# The service is super important, so we hang on to it for the462# rest of the scan.463def report_starturl_service(&block)464return unless @host_object465return unless @state[:starturl_uri]466name = @state[:starturl_uri].scheme467port = @state[:starturl_uri].port468addr = @host_object.address469svc = {470:host => @host_object,471:port => port,472:name => name.dup,473:proto => "tcp"474}475if name and port476db.emit(:service,[addr,port].join(":"),&block) if block477@state[:starturl_service_object] = db_report(:service,svc)478end479end480481def report_kbitem_service(service,&block)482return unless @host_object483return unless @state[:starturl_uri]484addr = @host_object.address485svc = {486:host => @host_object,487:port => service[:portnum].to_i,488:name => service[:name].dup.downcase,489:proto => service[:proto].dup.downcase490}491if service[:name] and service[:portnum]492db.emit(:service,[addr,service[:portnum]].join(":"),&block) if block493db_report(:service,svc)494end495end496497def report_web_site(url,&block)498return unless in_tag("Crawler")499return unless url500return if url.strip.empty?501uri = URI.parse(url) rescue nil502return unless uri503host = uri.host504port = uri.port505scheme = uri.scheme506return unless scheme[/^https?/]507return unless (host && port && scheme)508address = resolve_address(host)509return unless address510# If we didn't create the service, we don't care about the site511service_object = db.services(:workspace => @args[:workspace],512:hosts => {address: address},513:proto => 'tcp',514:port => port).first515return unless service_object516web_site_info = {517:workspace => @args[:workspace],518:service => service_object,519:vhost => host,520:ssl => (scheme == "https")521}522@state[:web_site] = db_report(:web_site,web_site_info)523@state[:fullurl] = uri524end525526def report_starturl_web_site(&block)527return unless @state[:crawler_starturl]528starturl = @state[:crawler_starturl].dup529report_web_site(starturl,&block)530end531532def report_os_fingerprint533return unless @state[:starturl_service_object]534return unless @text535return if @text.strip.empty?536return unless in_tag("Scan")537host = @state[:starturl_service_object].host538fp_note = {539:workspace => host.workspace,540:host => host,541:type => 'host.os.acunetix_fingerprint',542:data => {:os => @text}543}544db_report(:note, fp_note)545@text = nil546end547548def resolve_port(uri)549@state[:port] = uri.port550unless @state[:port]551@parse_warnings << "Could not determine a port for '#{@state[:scan_name]}'"552end553@state[:port] = uri.port554end555556def resolve_address(host)557return @resolv_cache[host] if @resolv_cache[host]558address = Rex::Socket.resolv_to_dotted(host) rescue nil559@resolv_cache[host] = address560return address561end562563def resolve_scan_starturl_address(uri)564if uri.host565address = resolve_address(uri.host)566unless address567@parse_warnings << "Could not resolve address for '#{uri.host}', skipping '#{@state[:scan_name]}'"568end569else570@parse_warnings << "Could not determine a host for '#{@state[:scan_name]}'"571end572address573end574575def handle_parse_warnings(&block)576return if @parse_warnings.empty?577@parse_warnings.each do |pwarn|578db.emit(:warning, pwarn, &block) if block579end580end581582end583end584end585586587588