# -*- coding: binary -*- require "rex/parser/nokogiri_doc_mixin" module Rex module Parser # If Nokogiri is available, define Burp Session document class. # # Burp Session XML files actually provide a lot, but since it also # provides the originating url, we can pull most of the detail from # the URI object. load_nokogiri && class BurpSessionDocument < 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 # Since we try to resolve every time we hit a new web page, need to # hang on to our misses. Presume that it's a permanent enough failure # that it won't get fixed during this particular import attr_reader :missed_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_warning def start_document @parse_warnings = [] @parse_warned = [] @resolv_cache = {} @missed_cache = [] end def start_element(name=nil,attrs=[]) attrs = normalize_attrs(attrs) block = @block @state[:current_tag][name] = true case name when "host", "port", "protocol", "path" @state[:has_text] = true when "status" @state[:has_text] = true when "response" @state[:has_text] = true end end def end_element(name=nil) block = @block case name when "item" # Wrap up this item, but keep resolved web sites collect_uri report_web_site(&block) handle_parse_warnings(&block) report_web_page(&block) report_web_service_info report_web_host_info # Reset the state once we close a host @state = @state.select {|k| [:current_tag, :web_sites].include? k} when "host" @state[:has_text] = false collect_host @text = nil when "port" @state[:has_text] = false collect_port @text = nil when "protocol" @state[:has_text] = false collect_protocol @text = nil when "path" @state[:has_text] = false collect_path_and_query @text = nil when "status" @state[:has_text] = false collect_status @text = nil when "response" @state[:has_text] = false collect_response @text = nil end @state[:current_tag].delete name end def collect_host return unless in_item return unless has_text @state[:host] = @text end def collect_port return unless in_item return unless has_text return unless @text.to_i.to_s == @text.to_s @state[:port] = @text.to_i end def collect_protocol return unless in_item return unless has_text @state[:protocol] = @text end def collect_path_and_query return unless in_item return unless has_text path,query = @text.split(/\?+/,2) return unless path if query @state[:query] = "?#{query}" # Can be nil end if path =~ /https?:[\x5c\x2f][\x5c\x2f]+[^\x5c\x2f][^\x5c\x2f]+([^?]+)/ real_path = "/#{$1}" else real_path = path end @state[:path] = real_path end def collect_status return unless in_item return unless has_text return unless @text.to_i.to_s == @text @state[:status] = @text.to_i end def collect_uri return unless in_item return unless @state[:host] return unless @state[:port] return unless @state[:protocol] return unless @state[:path] url = @state[:protocol].to_s url << "://" url << @state[:host].to_s url << ":" url << @state[:port].to_s url << @state[:path] if @state[:query] url << "?" url << @state[:query] end @state[:uri] = URI.parse(url) rescue nil end def report_web_host_info return unless @state[:web_site] return unless @state[:uri].kind_of? URI::HTTP return unless @state[:web_site].service.host.name.to_s.empty? host_info = {:workspace => @args[:wspace]} host_info[:address] = @state[:web_site].service.host.address host_info[:name] = @state[:uri].host report_db(:host, host_info) end def report_web_service_info return unless @state[:web_site] return unless @state[:service_info] return unless @state[:web_site].service.info.to_s.empty? service_info = {} service_info[:host] = @state[:web_site].service.host service_info[:port] = @state[:web_site].service.port service_info[:proto] = @state[:web_site].service.proto service_info[:info] = @state[:service_info] db_report(:service, service_info) end def report_web_page(&block) return unless @state[:uri].kind_of? URI::HTTP return unless @state[:status] return unless @state[:web_site] return unless @state[:response_headers].kind_of? Hash headers = {} @state[:response_headers].each do |k,v| headers[k.to_s.downcase] ||= [] headers[k.to_s.downcase] << v end if headers["server"].kind_of? Array @state[:service_info] = headers["server"].first end return unless @state[:response_body] web_page_info = {:workspace => @args[:wspace]} web_page_info[:web_site] = @state[:web_site] web_page_info[:code] = @state[:status] web_page_info[:path] = @state[:uri].path web_page_info[:headers] = headers web_page_info[:body] = @state[:response_body] web_page_info[:query] = @state[:uri].query url = @state[:uri].to_s.gsub(/\?.*/,"") db.emit(:web_page, url, &block) if block db_report(:web_page, web_page_info) end def report_web_site(&block) return unless @state[:uri].kind_of? URI::HTTP vhost = @state[:uri].host web_site_info = {:workspace => @args[:wspace]} web_site_info[:vhost] = vhost address = resolve_vhost_address(@state[:uri]) return unless address web_site_info[:host] = address web_site_info[:port] = @state[:uri].port web_site_info[:ssl] = @state[:uri].kind_of? URI::HTTPS web_site_obj = db_report(:web_site, web_site_info) return unless web_site_obj @state[:web_sites] ||= [] url = "#{@state[:uri].scheme}://#{@state[:uri].host}:#{@state[:uri].port}" unless @state[:web_sites].include? web_site_obj db.emit(:web_site, url, &block) @state[:web_sites] << web_site_obj end @state[:web_site] = web_site_obj end def collect_response return unless in_item return unless has_text response_text = @text.dup response_header_text,response_body_text = response_text.split(/\r*\n\r*\n/n,2) return unless response_header_text response_header = Rex::Proto::Http::Packet::Header.new response_header.from_s response_header_text @state[:response_headers] = response_header @state[:response_body] = response_body_text end def in_item return false unless in_tag("item") return false unless in_tag("items") return true end def has_text return false unless @text return false if @text.strip.empty? @text = @text.strip end def handle_parse_warnings(&block) return if @parse_warnings.empty? return unless block @parse_warnings.each_with_index do |pwarn,i| unless @parse_warned.include? i db.emit(:warning, pwarn, &block) @parse_warned << i end end end def resolve_address(host) return @resolv_cache[host] if @resolv_cache[host] return false if @missed_cache.include? host address = Rex::Socket.resolv_to_dotted(host) rescue nil @resolv_cache[host] = address if address block = @block db.emit(:address, address, &block) if block else @missed_cache << host end return address end # Alias this def resolve_vhost_address(uri) if uri.host address = resolve_address(uri.host) case address when false return false when nil @parse_warnings << "Could not resolve address for '#{uri.host}', skipping." end else @parse_warnings << "Could not determine a host for this import." end address end end end end