use a stream parser to import NeXpose raw xml, short-circuit find_or_create_ref to avoid going to the task queue when possible
git-svn-id: file:///home/svn/framework3/trunk@9199 4d416f70-5f16-0410-b530-b9f4589650daunstable
parent
39a06a88c3
commit
1ab399d0ea
|
@ -1,4 +1,5 @@
|
|||
require 'rex/parser/nmap_xml'
|
||||
require 'rex/parser/nexpose_xml'
|
||||
|
||||
module Msf
|
||||
|
||||
|
@ -618,6 +619,11 @@ class DBManager
|
|||
#
|
||||
# opts must contain
|
||||
# :host -- the host where this vulnerability resides
|
||||
# :name -- the scanner-specific id of the vuln (e.g. NEXPOSE-cifs-acct-password-never-expires)
|
||||
#
|
||||
# opts can contain
|
||||
# :data -- a human readable description of the vuln, free-form text
|
||||
# :refs -- an array of Ref objects or string names of references
|
||||
#
|
||||
def report_vuln(opts)
|
||||
return if not active
|
||||
|
@ -703,6 +709,9 @@ class DBManager
|
|||
#
|
||||
def find_or_create_ref(opts)
|
||||
ret = {}
|
||||
ret[:ref] = get_ref(opts[:name])
|
||||
return ret[:ref] if ret[:ref]
|
||||
|
||||
task = queue(Proc.new {
|
||||
ref = Ref.find_or_initialize_by_name(opts[:name])
|
||||
if ref and ref.changed?
|
||||
|
@ -1360,9 +1369,6 @@ class DBManager
|
|||
#
|
||||
# Nexpose Raw XML
|
||||
#
|
||||
# XXX At some point we'll want to make this a stream parser for dealing
|
||||
# with large results files
|
||||
#
|
||||
def import_nexpose_rawxml_file(filename, wspace=workspace)
|
||||
f = File.open(filename, 'r')
|
||||
data = f.read(f.stat.size)
|
||||
|
@ -1370,9 +1376,149 @@ class DBManager
|
|||
end
|
||||
|
||||
def import_nexpose_rawxml(data, wspace=workspace)
|
||||
# Use a stream parser instead of a tree parser so we can deal with
|
||||
# huge results files without running out of memory.
|
||||
parser = Rex::Parser::NexposeXMLStreamParser.new
|
||||
|
||||
raise RuntimeError, "NeXpose RAW-XML is not currently supported, please use SimpleXML"
|
||||
# Since all the Refs have to be in the database before we can use them
|
||||
# in a Vuln, we store all the hosts until we finish parsing and only
|
||||
# then put everything in the database. This is memory-intensive for
|
||||
# large files, but should be much less so than a tree parser.
|
||||
#
|
||||
# This method is also considerably faster than parsing through the tree
|
||||
# looking for references every time we hit a vuln.
|
||||
hosts = []
|
||||
vulns = []
|
||||
|
||||
# The callback merely populates our in-memory table of hosts and vulns
|
||||
parser.callback = Proc.new { |type, value|
|
||||
case type
|
||||
when :host
|
||||
hosts.push(value)
|
||||
when :vuln
|
||||
vulns.push(value)
|
||||
end
|
||||
}
|
||||
|
||||
REXML::Document.parse_stream(data, parser)
|
||||
|
||||
vuln_refs = nexpose_refs_to_hash(vulns)
|
||||
hosts.each do |host|
|
||||
nexpose_host(host, vuln_refs)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Takes an array of vuln hashes, as returned by the NeXpose rawxml stream
|
||||
# parser, like:
|
||||
# [
|
||||
# {"id"=>"winreg-notes-protocol-handler", severity="8", "refs"=>[{"source"=>"BID", "value"=>"10600"}, ...]}
|
||||
# {"id"=>"windows-zotob-c", severity="8", "refs"=>[{"source"=>"BID", "value"=>"14513"}, ...]}
|
||||
# ]
|
||||
# and transforms it into a hash of vuln references keyed on vuln id, like:
|
||||
# { "windows-zotob-c" => [{"source"=>"BID", "value"=>"14513"}, ...] }
|
||||
#
|
||||
# This method ignores all attributes other than the vuln's NeXpose ID and
|
||||
# references (including title, severity, et cetera).
|
||||
#
|
||||
def nexpose_refs_to_hash(vulns)
|
||||
refs = {}
|
||||
vulns.each do |vuln|
|
||||
vuln["refs"].each do |ref|
|
||||
refs[vuln['id']] ||= []
|
||||
if ref['source'] == 'BID'
|
||||
refs[vuln['id']].push('BID-' + ref["value"])
|
||||
elsif ref['source'] == 'CVE'
|
||||
# value is CVE-$ID
|
||||
refs[vuln['id']].push(ref["value"])
|
||||
elsif ref['source'] == 'MS'
|
||||
refs[vuln['id']].push('MSB-MS-' + ref["value"])
|
||||
elsif ref['source'] == 'URL'
|
||||
refs[vuln['id']].push('URL-' + ref["value"])
|
||||
#else
|
||||
# $stdout.puts("Unknown source: #{ref["source"]}")
|
||||
end
|
||||
end
|
||||
end
|
||||
refs
|
||||
end
|
||||
|
||||
def nexpose_host(h, vuln_refs, wspace=workspace)
|
||||
data = {:workspace => wspace}
|
||||
if h["addr"]
|
||||
addr = h["addr"]
|
||||
else
|
||||
# Can't report it if it doesn't have an IP
|
||||
return
|
||||
end
|
||||
data[:host] = addr
|
||||
if (h["hardware-address"])
|
||||
data[:mac] = h["hardware-address"]
|
||||
end
|
||||
data[:state] = (h["status"] == "alive") ? Msf::HostState::Alive : Msf::HostState::Dead
|
||||
|
||||
# Since we only have one name field per host in the database, just
|
||||
# take the first one.
|
||||
if (h["names"] and h["names"].first)
|
||||
data[:name] = h["names"].first
|
||||
end
|
||||
|
||||
data[:os_name] = h["os_family"] if h["os_family"]
|
||||
data[:os_flavor] = h["os_product"] if h["os_family"]
|
||||
data[:arch] = h["arch"] if h["arch"]
|
||||
|
||||
if (data[:state] != Msf::HostState::Dead)
|
||||
report_host(data)
|
||||
end
|
||||
|
||||
if h["os_vendor"]
|
||||
note = {
|
||||
:workspace => wspace,
|
||||
:host => addr,
|
||||
:type => 'host.os.nexpose_fingerprint',
|
||||
:data => {
|
||||
:os_vendor => h["os_vendor"],
|
||||
:os_family => h["os_family"],
|
||||
:os_product => h["os_product"],
|
||||
:os_certainty => h["os_certainty"]
|
||||
}
|
||||
}
|
||||
|
||||
report_note(note)
|
||||
end
|
||||
|
||||
# Put all the ports, regardless of state, into the db.
|
||||
h["endpoints"].each { |p|
|
||||
extra = ""
|
||||
extra << p["product"] + " " if p["product"]
|
||||
extra << "(" + p["certainty"] + " certainty) " if p["certainty"]
|
||||
|
||||
data = {}
|
||||
data[:workspace] = wspace
|
||||
data[:proto] = p["protocol"].downcase
|
||||
data[:port] = p["port"].to_i
|
||||
data[:state] = p["status"]
|
||||
data[:host] = addr
|
||||
data[:info] = extra if not extra.empty?
|
||||
if p["name"] != "<unknown>"
|
||||
data[:name] = p["name"]
|
||||
end
|
||||
report_service(data)
|
||||
}
|
||||
|
||||
h["vulns"].each_pair { |k,v|
|
||||
next if v["status"] != "vulnerable-exploited" and v["status"] != "vulnerable-version"
|
||||
|
||||
data = {}
|
||||
data[:workspace] = wspace
|
||||
data[:host] = addr
|
||||
data[:name] = "NEXPOSE-" + v["id"]
|
||||
data[:refs] = vuln_refs[v["id"]]
|
||||
report_vuln(data)
|
||||
}
|
||||
end
|
||||
|
||||
=begin
|
||||
doc = rexmlify(data)
|
||||
doc.elements.each('/NexposeReport/nodes/node') do |host|
|
||||
addr = host.attributes['address']
|
||||
|
@ -1431,6 +1577,7 @@ class DBManager
|
|||
end
|
||||
end
|
||||
end
|
||||
=end
|
||||
|
||||
#
|
||||
# Import Nmap's -oX xml output
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
module Rex
|
||||
module Parser
|
||||
class NexposeXMLStreamParser
|
||||
|
||||
attr_accessor :callback
|
||||
|
||||
def initialize(callback = nil)
|
||||
reset_state
|
||||
self.callback = callback if callback
|
||||
end
|
||||
|
||||
def reset_state
|
||||
@state = :generic_state
|
||||
@host = { "status" => nil, "endpoints" => [], "names" => [], "vulns" => {} }
|
||||
@vuln = { "refs" => [] }
|
||||
end
|
||||
|
||||
def tag_start(name, attributes)
|
||||
case name
|
||||
when "node"
|
||||
@host["hardware-address"] = attributes["hardware-address"]
|
||||
@host["addr"] = attributes["address"]
|
||||
@host["status"] = attributes["status"]
|
||||
when "os"
|
||||
# Take only the highest certainty
|
||||
if not @host["os_certainty"] or (@host["os_certainty"].to_f < attributes["certainty"].to_f)
|
||||
@host["os_vendor"] = attributes["vendor"]
|
||||
@host["os_family"] = attributes["family"]
|
||||
@host["os_product"] = attributes["product"]
|
||||
@host["arch"] = attributes["arch"]
|
||||
@host["os_certainty"] = attributes["certainty"]
|
||||
end
|
||||
when "name"
|
||||
#@host["names"].push attributes["name"]
|
||||
@state = :in_name
|
||||
when "endpoint"
|
||||
# This is a port in NeXpose parlance
|
||||
@host["endpoints"].push(attributes)
|
||||
when "service"
|
||||
@state = :in_service
|
||||
# Store any service info with the associated port. There shouldn't
|
||||
# be any collisions on attribute names here, so just merge them.
|
||||
@host["endpoints"].last.merge!(attributes)
|
||||
when "fingerprint"
|
||||
if @state == :in_service
|
||||
@host["endpoints"].last.merge!(attributes)
|
||||
end
|
||||
when "test"
|
||||
if attributes["status"] == "vulnerable-exploited" or attributes["status"] == "vulnerable-version"
|
||||
@host["vulns"][attributes["id"]] = attributes.dup
|
||||
end
|
||||
when "vulnerability"
|
||||
@vuln.merge! attributes
|
||||
when "reference"
|
||||
@state = :in_reference
|
||||
@vuln["refs"].push attributes
|
||||
end
|
||||
end
|
||||
|
||||
def text(str)
|
||||
case @state
|
||||
when :in_name
|
||||
@host["names"].push str
|
||||
when :in_reference
|
||||
@vuln["refs"].last["value"] = str
|
||||
end
|
||||
end
|
||||
|
||||
def tag_end(name)
|
||||
case name
|
||||
when "node"
|
||||
callback.call(:host, @host) if callback
|
||||
reset_state
|
||||
when "vulnerability"
|
||||
callback.call(:vuln, @vuln) if callback
|
||||
reset_state
|
||||
when "service","reference"
|
||||
@state = :generic_state
|
||||
end
|
||||
end
|
||||
|
||||
# We don't need these methods, but they're necessary to keep REXML happy
|
||||
def xmldecl(version, encoding, standalone); end
|
||||
def cdata; end
|
||||
def comment(str); end
|
||||
def instruction(name, instruction); end
|
||||
def attlist; end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
__END__
|
||||
|
||||
<node address="10.1.1.10" status="alive" hardware-address="0007371F3BE8">
|
||||
<names>
|
||||
<name>NETBIOSNAME</name>
|
||||
<name>hostname.example.com</name>
|
||||
</names>
|
||||
<fingerprints>
|
||||
<os certainty="1.00" device-class="Domain controller" vendor="Microsoft" family="Windows" product="Windows Server 2003, Standard Edition" version="SP2" arch="x86"/>
|
||||
<os certainty="0.85" device-class="General" vendor="Microsoft" family="Windows" product="Windows Server 2003"/>
|
||||
<os certainty="0.70" vendor="Microsoft" family="Windows" product="Windows Server 2003"/>
|
||||
</fingerprints>
|
||||
<software>
|
||||
<fingerprint certainty="1.00" vendor="Acronis" product="Acronis True Image Echo Server" version="9.5.8163"/>
|
||||
<fingerprint certainty="1.00" vendor="Acronis" product="Acronis Universal Restore for Acronis True Image Echo Server" version="9.5.8076"/>
|
||||
<fingerprint certainty="1.00" software-class="Internet Client" vendor="Microsoft" family="Internet Explorer" product="Internet Explorer" version="7.0.5730.11"/>
|
||||
<fingerprint certainty="1.00" software-class="Database Client" vendor="Microsoft" family="MDAC" product="MDAC" version="2.8"/>
|
||||
<fingerprint certainty="1.00" software-class="Media Client" vendor="Microsoft" family="Windows Media Player" product="Windows Media Player" version="10.0.0.3997"/>
|
||||
<fingerprint certainty="1.00" vendor="MySolutions NORDIC" product="NSClient++ (Win32)" version="0.3.4.0"/>
|
||||
<fingerprint certainty="1.00" vendor="Symantec Corporation" product="LiveUpdate 3.1 (Symantec Corporation)" version="3.1.0.99"/>
|
||||
<fingerprint certainty="1.00" vendor="Symantec Corporation" product="Symantec AntiVirus" version="10.1.5000.5"/>
|
||||
</software>
|
||||
<tests>
|
||||
<test status="not-vulnerable" id="backdoor-ckb.cfaae1e6">
|
||||
|
||||
<endpoint protocol="tcp" port="139" status="open">
|
||||
<services>
|
||||
<service name="CIFS">
|
||||
<fingerprints>
|
||||
<fingerprint certainty="1.00" product="Windows Server 2003 R2 5.2"/>
|
||||
</fingerprints>
|
||||
<tests>
|
||||
</tests>
|
||||
</service>
|
||||
</services>
|
||||
</endpoint>
|
||||
</node>
|
||||
|
Loading…
Reference in New Issue