d45cdd61aa
Since Ruby 2.1, the respond_to? method is more strict because it does not check protected methods. So when you use send(), clearly you're ignoring this type of access control. The patch is meant to preserve this behavior to avoid potential breakage. Resolve #4507
343 lines
10 KiB
Ruby
343 lines
10 KiB
Ruby
# -*- coding: binary -*-
|
|
require "rex/parser/nokogiri_doc_mixin"
|
|
|
|
module Rex
|
|
module Parser
|
|
|
|
# If Nokogiri is available, define Template document class.
|
|
load_nokogiri && class FoundstoneDocument < Nokogiri::XML::SAX::Document
|
|
|
|
include NokogiriDocMixin
|
|
|
|
def start_document
|
|
@report_type_ok = true # Optimistic
|
|
end
|
|
|
|
# Triggered every time a new element is encountered. We keep state
|
|
# ourselves with the @state variable, turning things on when we
|
|
# get here (and turning things off when we exit in end_element()).
|
|
def start_element(name=nil,attrs=[])
|
|
attrs = normalize_attrs(attrs)
|
|
block = @block
|
|
return unless @report_type_ok
|
|
@state[:current_tag][name] = true
|
|
case name
|
|
when "ReportInfo"
|
|
check_for_correct_report_type(attrs,&block)
|
|
when "Host"
|
|
record_host(attrs)
|
|
when "Service"
|
|
record_service(attrs)
|
|
when "Port", "Protocol", "Banner"
|
|
@state[:has_text] = true
|
|
when "Vuln" # under VulnsFound, ignore risk 0 things
|
|
record_vuln(attrs)
|
|
when "Risk" # for Vuln
|
|
@state[:has_text] = true
|
|
when "CVE" # Under Vuln
|
|
@state[:has_text] = true
|
|
end
|
|
end
|
|
|
|
# When we exit a tag, this is triggered.
|
|
def end_element(name=nil)
|
|
block = @block
|
|
return unless @report_type_ok
|
|
case name
|
|
when "Host" # Wrap it up
|
|
collect_host_data
|
|
host_object = report_host &block
|
|
if host_object
|
|
db.report_import_note(@args[:wspace],host_object)
|
|
report_fingerprint(host_object)
|
|
report_services(host_object)
|
|
report_vulns(host_object)
|
|
end
|
|
# Reset the state once we close a host
|
|
@state.delete_if {|k| k != :current_tag}
|
|
@report_data = {:wspace => args[:wspace]}
|
|
when "Port"
|
|
@state[:has_text] = false
|
|
collect_port
|
|
when "Protocol"
|
|
@state[:has_text] = false
|
|
collect_protocol
|
|
when "Banner"
|
|
collect_banner
|
|
@state[:has_text] = false
|
|
when "Service"
|
|
collect_service
|
|
when "Vuln"
|
|
collect_vuln
|
|
when "Risk"
|
|
@state[:has_text] = false
|
|
collect_risk
|
|
when "CVE"
|
|
@state[:has_text] = false
|
|
collect_cve
|
|
end
|
|
@state[:current_tag].delete name
|
|
end
|
|
|
|
# Nothing technically stopping us from parsing this as well,
|
|
# but saving this for later
|
|
def check_for_correct_report_type(attrs,&block)
|
|
report_type = attr_hash(attrs)["ReportType"]
|
|
if report_type == "Network Inventory"
|
|
@report_type_ok = true
|
|
else
|
|
if report_type == "Risk Data"
|
|
msg = "The Foundstone/Mcafee report type '#{report_type}' is not currently supported"
|
|
msg << ",\nso no data will be imported. Please use the 'Network Inventory' report instead."
|
|
else
|
|
msg = ".\nThe Foundstone/Macafee report type '#{report_type}' is unsupported."
|
|
end
|
|
db.emit(:warning,msg,&block) if block
|
|
@report_type_ok = false
|
|
end
|
|
end
|
|
|
|
def collect_risk
|
|
return unless in_tag("VulnsFound")
|
|
return unless in_tag("HostData")
|
|
return unless in_tag("Host")
|
|
risk = @text.to_s.to_i
|
|
@state[:vuln][:risk] = risk
|
|
@text = nil
|
|
end
|
|
|
|
def collect_cve
|
|
return unless in_tag("VulnsFound")
|
|
return unless in_tag("HostData")
|
|
return unless in_tag("Host")
|
|
cve = @text.to_s
|
|
@state[:vuln][:cves] ||= []
|
|
@state[:vuln][:cves] << cve unless cve == "CVE-MAP-NOMATCH"
|
|
@text = nil
|
|
end
|
|
|
|
# Determines if we should keep the vuln or not. Note that
|
|
# we cannot tie them to a service.
|
|
def collect_vuln
|
|
return unless in_tag("VulnsFound")
|
|
return unless in_tag("HostData")
|
|
return unless in_tag("Host")
|
|
return if @state[:vuln][:risk] == 0
|
|
@report_data[:vulns] ||= []
|
|
vuln_hash = {}
|
|
vuln_hash[:name] = @state[:vuln]["VulnName"]
|
|
refs = []
|
|
refs << "FID-#{@state[:vuln]["id"]}"
|
|
if @state[:vuln][:cves]
|
|
@state[:vuln][:cves].each {|cve| refs << cve}
|
|
end
|
|
vuln_hash[:refs] = refs
|
|
@report_data[:vulns] << vuln_hash
|
|
end
|
|
|
|
# These are per host.
|
|
def record_vuln(attrs)
|
|
return unless in_tag("VulnsFound")
|
|
return unless in_tag("HostData")
|
|
return unless in_tag("Host")
|
|
@state[:vulns] ||= []
|
|
|
|
@state[:vuln] = attr_hash(attrs) # id and VulnName
|
|
end
|
|
|
|
def record_service(attrs)
|
|
return unless in_tag("ServicesFound")
|
|
return unless in_tag("Host")
|
|
@state[:service] = attr_hash(attrs)
|
|
end
|
|
|
|
def collect_port
|
|
return unless in_tag("Service")
|
|
return unless in_tag("ServicesFound")
|
|
return unless in_tag("Host")
|
|
return if @text.nil? || @text.empty?
|
|
@state[:service][:port] = @text.strip
|
|
@text = nil
|
|
end
|
|
|
|
def collect_protocol
|
|
return unless in_tag("Service")
|
|
return unless in_tag("ServicesFound")
|
|
return unless in_tag("Host")
|
|
return if @text.nil? || @text.empty?
|
|
@state[:service][:proto] = @text.strip
|
|
@text = nil
|
|
end
|
|
|
|
def collect_banner
|
|
return unless in_tag("Service")
|
|
return unless in_tag("ServicesFound")
|
|
return unless in_tag("Host")
|
|
return if @text.nil? || @text.empty?
|
|
banner = normalize_foundstone_banner(@state[:service]["ServiceName"],@text)
|
|
unless banner.nil? || banner.empty?
|
|
@state[:service][:banner] = banner
|
|
end
|
|
@text = nil
|
|
end
|
|
|
|
def collect_service
|
|
return unless in_tag("ServicesFound")
|
|
return unless in_tag("Host")
|
|
return unless @state[:service][:port]
|
|
@report_data[:ports] ||= []
|
|
port_hash = {}
|
|
port_hash[:port] = @state[:service][:port]
|
|
port_hash[:proto] = @state[:service][:proto]
|
|
port_hash[:info] = @state[:service][:banner]
|
|
port_hash[:name] = db.nmap_msf_service_map(@state[:service]["ServiceName"])
|
|
@report_data[:ports] << port_hash
|
|
end
|
|
|
|
def record_host(attrs)
|
|
return unless in_tag("HostData")
|
|
@state[:host] = attr_hash(attrs)
|
|
end
|
|
|
|
def collect_host_data
|
|
@report_data[:host] = @state[:host]["IPAddress"]
|
|
if @state[:host]["NBName"] && !@state[:host]["NBName"].empty?
|
|
@report_data[:name] = @state[:host]["NBName"]
|
|
elsif @state[:host]["DNSName"] && !@state[:host]["DNSName"].empty?
|
|
@report_data[:name] = @state[:host]["DNSName"]
|
|
end
|
|
if @state[:host]["OSName"] && !@state[:host]["OSName"].empty?
|
|
@report_data[:os_fingerprint] = @state[:host]["OSName"]
|
|
end
|
|
@report_data[:state] = Msf::HostState::Alive
|
|
@report_data[:mac] = @state[:mac] if @state[:mac]
|
|
end
|
|
|
|
def report_host(&block)
|
|
return unless in_tag("HostData")
|
|
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
|
|
|
|
def report_fingerprint(host_object)
|
|
fp_note = {
|
|
:workspace => host_object.workspace,
|
|
:host => host_object,
|
|
:type => 'host.os.foundstone_fingerprint',
|
|
:data => {:os => @report_data[:os_fingerprint] }
|
|
}
|
|
db_report(:note, fp_note)
|
|
end
|
|
|
|
def report_services(host_object)
|
|
return unless in_tag("HostData")
|
|
return unless host_object.kind_of? ::Mdm::Host
|
|
return unless @report_data[:ports]
|
|
return if @report_data[:ports].empty?
|
|
@report_data[:ports].each do |svc|
|
|
db_report(:service, svc.merge(:host => host_object))
|
|
end
|
|
end
|
|
|
|
def report_vulns(host_object)
|
|
return unless in_tag("HostData")
|
|
return unless host_object.kind_of? ::Mdm::Host
|
|
return unless @report_data[:vulns]
|
|
return if @report_data[:vulns].empty?
|
|
@report_data[:vulns].each do |vuln|
|
|
db_report(:vuln, vuln.merge(:host => host_object))
|
|
end
|
|
end
|
|
|
|
# Foundstone's banners are pretty free-form
|
|
# and often not just banners. Clean them up
|
|
# for the :info field, delegate off for other
|
|
# protocol data we can use.
|
|
def normalize_foundstone_banner(service,banner)
|
|
return "" if(banner.nil? || banner.strip.empty?)
|
|
if first_line_only? service
|
|
return (first_line banner)
|
|
elsif needs_more_processing? service
|
|
return process_service(service,banner)
|
|
else
|
|
return (first_line banner)
|
|
end
|
|
end
|
|
|
|
# Services where we only care about the first
|
|
# line of the banner tag.
|
|
def first_line_only?(service)
|
|
svcs = %w{
|
|
vnc ftp ftps smtp oracle-tns nntp ssh ntp
|
|
}
|
|
9.times {|i| svcs << "vnc-#{i}"}
|
|
svcs.include? service
|
|
end
|
|
|
|
# Services where we need to do more processing
|
|
# before handing the banner back.
|
|
def needs_more_processing?(service)
|
|
svcs = %w{
|
|
microsoft-ds loc-srv http https sunrpc netbios-ns
|
|
}
|
|
svcs.include? service
|
|
end
|
|
|
|
def first_line(str)
|
|
str.split("\n").first.to_s.strip
|
|
end
|
|
|
|
# XXX: Actually implement more of these
|
|
def process_service(service,banner)
|
|
meth = "process_service_#{service.gsub("-","_")}"
|
|
if self.respond_to?(meth, true)
|
|
self.send meth, banner
|
|
else
|
|
return (first_line banner)
|
|
end
|
|
end
|
|
|
|
# XXX: Register a proper netbios note as the regular
|
|
# scanner does.
|
|
def process_service_netbios_ns(banner)
|
|
mac_regex = /[0-9A-Fa-f:]{17}/
|
|
@state[:mac] = banner[mac_regex]
|
|
first_line banner
|
|
end
|
|
|
|
# XXX: Make this behave more like the smb scanner
|
|
def process_service_microsoft_ds(banner)
|
|
lm_regex = /Native LAN Manager/
|
|
lm_banner = nil
|
|
banner.each_line { |line|
|
|
if line[lm_regex]
|
|
lm_banner = line
|
|
break
|
|
end
|
|
}
|
|
lm_banner || first_line(banner)
|
|
end
|
|
|
|
def process_service_http(banner)
|
|
server = nil
|
|
banner.each_line do |line|
|
|
if line =~ /^Server:\s+(.*)/
|
|
server = $1
|
|
break
|
|
end
|
|
end
|
|
server || first_line(banner)
|
|
end
|
|
|
|
alias :process_service_https :process_service_http
|
|
alias :process_service_rtsp :process_service_http
|
|
|
|
end
|
|
|
|
end
|
|
end
|
|
|