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

292 lines
8.5 KiB
Ruby

# -*- 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]+([^?]+)/n
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