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/appscan_document.rb
Views: 11777
# -*- coding: binary -*-1require "rex/parser/nokogiri_doc_mixin"23module Rex4module Parser56# If Nokogiri is available, define AppScan document class.7load_nokogiri && class AppscanDocument < Nokogiri::XML::SAX::Document89include NokogiriDocMixin1011# The resolver prefers your local /etc/hosts (or windows equiv), but will12# fall back to regular DNS. It retains a cache for the import to avoid13# spamming your network with DNS requests.14attr_reader :resolv_cache1516# If name resolution of the host fails out completely, you will not be17# able to import that Scan task. Other scan tasks in the same report18# should be unaffected.19attr_reader :parse_warning2021def start_document22@parse_warnings = []23@resolv_cache = {}24end2526def start_element(name=nil,attrs=[])27attrs = normalize_attrs(attrs)28block = @block29@state[:current_tag][name] = true30case name31when "Issue" # Start of the stuff we want32collect_issue(attrs)33when "Entity" # Start of the stuff we want34collect_entity(attrs)35when "Severity", "Url", "OriginalHttpTraffic"36@state[:has_text] = true37end38end3940def end_element(name=nil)41block = @block42case name43when "Issue" # Wrap it up44record_issue45# Reset the state once we close an issue46@state = @state.select do47|k| [:current_tag, :web_sites].include? k48end49when "Url" # Populates @state[:web_site]50@state[:has_text] = false51record_url52@text = nil53report_web_site(&block)54handle_parse_warnings(&block)55when "Severity"56@state[:has_text] = false57record_risk58@text = nil59when "OriginalHttpTraffic" # Request and response60@state[:has_text] = false61record_request_and_response62report_service_info63page_info = report_web_page(&block)64if page_info65form_info = report_web_form(page_info,&block)66if form_info67report_web_vuln(form_info,&block)68end69end70@text = nil71end72@state[:current_tag].delete name73end7475def report_web_vuln(form_info,&block)76return unless(in_issue && has_text)77return unless form_info.kind_of? Hash78return unless @state[:issue]79return unless @state[:issue]["Noise"]80return unless @state[:issue]["Noise"].to_s.downcase == "false"81return unless @state[:issue][:vuln_param]82web_vuln_info = {}83web_vuln_info[:web_site] = form_info[:web_site]84web_vuln_info[:path] = form_info[:path]85web_vuln_info[:query] = form_info[:query]86web_vuln_info[:method] = form_info[:method]87web_vuln_info[:params] = form_info[:params]88web_vuln_info[:pname] = @state[:issue][:vuln_param]89web_vuln_info[:proof] = "unknown" # TODO: pick this up from <Difference> maybe?90web_vuln_info[:risk] = @state[:issue][:risk]91web_vuln_info[:name] = @state[:issue]["IssueTypeID"]92web_vuln_info[:category] = "imported"93web_vuln_info[:confidence] = 100 # Seems pretty binary, noise or not94db.emit(:web_vuln, web_vuln_info[:name], &block) if block95web_vuln = db_report(:web_vuln, web_vuln_info)96end9798def collect_entity(attrs)99return unless in_issue100return unless @state[:issue].kind_of? Hash101ent_hash = attr_hash(attrs)102return unless ent_hash103return unless ent_hash["Type"].to_s.downcase == "parameter"104@state[:issue][:vuln_param] = ent_hash["Name"]105end106107def report_web_form(page_info,&block)108return unless(in_issue && has_text)109return unless page_info.kind_of? Hash110return unless @state[:request_body]111return if @state[:request_body].strip.empty?112web_form_info = {}113web_form_info[:web_site] = page_info[:web_site]114web_form_info[:path] = page_info[:path]115web_form_info[:query] = page_info[:query]116web_form_info[:method] = @state[:request_headers].cmd_string.split(/\s+/)[0]117parsed_params = parse_params(@state[:request_body])118return unless parsed_params119return if parsed_params.empty?120web_form_info[:params] = parsed_params121web_form = db_report(:web_form, web_form_info)122@state[:web_forms] ||= []123unless @state[:web_forms].include? web_form124db.emit(:web_form, @state[:uri].to_s, &block) if block125@state[:web_forms] << web_form126end127web_form_info128end129130def parse_params(request_body)131return unless request_body132pairs = request_body.split(/&/)133params = []134pairs.each do |pair|135param,value = pair.split("=",2)136params << [param,""] # Can't tell what's default137end138params139end140141def report_web_page(&block)142return unless(in_issue && has_text)143return unless @state[:web_site].present?144return unless @state[:response_headers].present?145return unless @state[:uri].present?146web_page_info = {}147web_page_info[:web_site] = @state[:web_site]148web_page_info[:path] = @state[:uri].path149web_page_info[:body] = @state[:response_body].to_s150web_page_info[:query] = @state[:uri].query151code = @state[:response_headers].cmd_string.split(/\s+/)[1]152return unless code153web_page_info[:code] = code154parsed_headers = {}155@state[:response_headers].each do |k,v|156parsed_headers[k.to_s.downcase] ||= []157parsed_headers[k.to_s.downcase] << v158end159return if parsed_headers.empty?160web_page_info[:headers] = parsed_headers161web_page = db_report(:web_page, web_page_info)162@state[:web_pages] ||= []163unless @state[:web_pages].include? web_page164db.emit(:web_page, @state[:uri].to_s, &block) if block165@state[:web_pages] << web_page166end167web_page_info168end169170def report_service_info171return unless(in_issue && has_text)172return unless @state[:web_site]173return unless @state[:response_headers]174banner = @state[:response_headers]["server"]175return unless banner176service = @state[:web_site].service177return unless service.info.to_s.empty?178service_info = {179:host => service.host,180:port => service.port,181:proto => service.proto,182:info => banner183}184db_report(:service, service_info)185end186187def record_request_and_response188return unless(in_issue && has_text)189return unless @state[:web_site].present?190really_original_traffic = unindent_and_crlf(@text)191request_headers, request_body, response_headers, response_body = really_original_traffic.split(/\r\n\r\n/)192return unless(request_headers && response_headers)193req_header = Rex::Proto::Http::Packet::Header.new194res_header = Rex::Proto::Http::Packet::Header.new195req_header.from_s request_headers.lstrip196res_header.from_s response_headers.lstrip197if response_body.to_s.empty?198response_body = ''199end200@state[:request_headers] = req_header201@state[:request_body] = request_body.lstrip202@state[:response_headers] = res_header203@state[:response_body] = response_body.lstrip204end205206# Appscan tab-indents which makes parsing a little difficult. They207# also don't record CRLFs, just LFs.208def unindent_and_crlf(text)209second_line = text.split(/\r*\n/)[1]210indent_level = second_line.size - second_line.lstrip.size211unindented_text_lines = []212text.split(/\r*\n/).each do |line|213if line =~ /^\t{#{indent_level}}/214unindented_line = line[indent_level,line.size]215unindented_text_lines << unindented_line216else217unindented_text_lines << line218end219end220unindented_text_lines.join("\r\n")221end222223def record_risk224return unless(in_issue && has_text)225@state[:issue] ||= {}226@state[:issue][:risk] = map_severity_to_risk227end228229def map_severity_to_risk230case @text.to_s.downcase231when "high" ; 5232when "medium" ; 3233when "low" ; 1234else ; 0235end236end237238# TODO239def record_issue240return unless in_issue241return unless @report_data[:issue].kind_of? Hash242return unless @state[:web_site]243return if @state[:issue]["Noise"].to_s.downcase == "true"244end245246def collect_issue(attrs)247return unless in_issue248@state[:issue] = {}249@state[:issue].merge! attr_hash(attrs)250end251252def report_web_site(&block)253return unless @state[:uri]254uri = @state[:uri]255hostname = uri.host # Assume the first one is the real hostname256address = resolve_issue_url_address(uri)257return unless address258unless @resolv_cache.values.include? address259db.emit(:address, address, &block) if block260end261port = resolve_port(uri)262return unless port263scheme = uri.scheme264return unless scheme265web_site_info = {:workspace => @args[:workspace]}266web_site_info[:vhost] = hostname267service_obj = check_for_existing_service(address,port)268if service_obj269web_site_info[:service] = service_obj270else271web_site_info[:host] = address272web_site_info[:port] = port273web_site_info[:ssl] = scheme == "https"274end275web_site_obj = db_report(:web_site, web_site_info)276@state[:web_sites] ||= []277unless @state[:web_sites].include? web_site_obj278url = "#{uri.scheme}://#{uri.host}:#{uri.port}"279db.emit(:web_site, url, &block) if block280db.report_import_note(@args[:workspace], web_site_obj.service.host)281@state[:web_sites] << web_site_obj282end283@state[:service] = service_obj || web_site_obj.service284@state[:host] = (service_obj || web_site_obj.service).host285@state[:web_site] = web_site_obj286end287288def check_for_existing_service(address,port)289# only one result can be returned, as the +port+ field restricts potential results to a single service290db.services(:workspace => @args[:workspace],291:hosts => {address: address},292:proto => 'tcp',293:port => port).first294end295296def resolve_port(uri)297@state[:port] = uri.port298unless @state[:port]299@parse_warnings << "Could not determine a port for '#{@state[:scan_name]}'"300end301return @state[:port]302end303304def resolve_address(host)305return @resolv_cache[host] if @resolv_cache[host]306address = Rex::Socket.resolv_to_dotted(host) rescue nil307@resolv_cache[host] = address308if address309block = @block310db.emit(:address, address, &block) if block311end312return address313end314315# Alias this316def resolve_issue_url_address(uri)317if uri.host318address = resolve_address(uri.host)319unless address320@parse_warnings << "Could not resolve address for '#{uri.host}', skipping."321end322else323@parse_warnings << "Could not determine a host for this import."324end325address326end327328def handle_parse_warnings(&block)329return if @parse_warnings.empty?330@parse_warnings.each do |pwarn|331db.emit(:warning, pwarn, &block) if block332end333end334335def record_url336return unless in_issue337return unless has_text338uri = URI.parse(@text) rescue nil339return unless uri340@state[:uri] = uri341end342343def in_issue344return false unless in_tag("Issue")345return false unless in_tag("Issues")346return false unless in_tag("XmlReport")347return true348end349350def has_text351return false unless @text352return false if @text.strip.empty?353@text = @text.strip354end355356end357358end359end360361362363