diff --git a/lib/msf/core/db.rb b/lib/msf/core/db.rb index 70491ad96c..5d75dff177 100644 --- a/lib/msf/core/db.rb +++ b/lib/msf/core/db.rb @@ -1100,6 +1100,8 @@ class DBManager return import_nessus_xml(data, wspace) when "NessusClientData_v2" return import_nessus_xml_v2(data, wspace) + when "SCAN" + return import_qualys_xml(data, wspace) else # Give up if we haven't hit the root tag in the first few lines break if line_count > 10 @@ -1493,6 +1495,85 @@ class DBManager end end + # + # Import Qualys' xml output + # + def import_qualys_xml_file(filename, wspace=workspace) + f = File.open(filename, 'r') + data = f.read(f.stat.size) + import_qualys_xml(data, wspace) + end + + def import_qualys_xml(data, wspace=workspace) + + doc = REXML::Document.new(data) + doc.elements.each('/SCAN/IP') do |host| + addr = host.attributes['value'] + hname = host.attributes['name'] || '' + + report_host(:workspace => wspace, :host => addr, :name => hname, :state => Msf::HostState::Alive) + + if host.elements["OS"] + hos = host.elements["OS"].text + report_note( + :workspace => wspace, + :host => addr, + :type => 'host.os.qualys_fingerprint', + :data => { + :os => hos + } + ) + end + + # Open TCP Services List (Qualys ID 82023) + services_tcp = host.elements["SERVICES/CAT/SERVICE[@number='82023']/RESULT"] + if services_tcp + services_tcp.text.scan(/([0-9]+)\t(.*?)\t.*?\t([^\t\n]*)/) do |match| + if match[2] == nil or match[2].strip == 'unknown' + name = match[1].strip + else + name = match[2].strip + end + handle_qualys(wspace, addr, match[0].to_s, 'tcp', 0, nil, nil, name) + end + end + # Open UDP Services List (Qualys ID 82004) + services_udp = host.elements["SERVICES/CAT/SERVICE[@number='82004']/RESULT"] + if services_udp + services_udp.text.scan(/([0-9]+)\t(.*?)\t.*?\t([^\t\n]*)/) do |match| + if match[2] == nil or match[2].strip == 'unknown' + name = match[1].strip + else + name = match[2].strip + end + handle_qualys(wspace, addr, match[0].to_s, 'udp', 0, nil, nil, name) + end + end + + # VULNS are confirmed, PRACTICES are unconfirmed vulnerabilities + host.elements.each('VULNS/CAT | PRACTICES/CAT') do |cat| + port = cat.attributes['port'] + protocol = cat.attributes['protocol'] + cat.elements.each('VULN | PRACTICE') do |vuln| + refs = [] + qid = vuln.attributes['number'] + severity = vuln.attributes['severity'] + vuln.elements.each('VENDOR_REFERENCE_LIST/VENDOR_REFERENCE') do |ref| + refs.push(ref.elements['ID'].text.to_s) + end + vuln.elements.each('CVE_ID_LIST/CVE_ID') do |ref| + refs.push('CVE-' + /C..-([0-9\-]{9})/.match(ref.elements['ID'].text.to_s)[1]) + end + vuln.elements.each('BUGTRAQ_ID_LIST/BUGTRAQ_ID') do |ref| + refs.push('BID-' + ref.elements['ID'].text.to_s) + end + + handle_qualys(wspace, addr, port, protocol, qid, severity, refs) + end + end + end + end + def import_ip_list_file(filename, wspace=workspace) f = File.open(filename, 'r') data = f.read(f.stat.size) @@ -1510,6 +1591,7 @@ class DBManager data = f.read(f.stat.size) import_amap_log(data, wspace) end + def import_amap_mlog(data, wspace) data.each_line do |line| next if line =~ /^#/ @@ -1643,6 +1725,31 @@ protected :refs => refs) end + # + # Qualys report parsing/handling + # + def handle_qualys(wspace, addr, port, protocol, qid, severity, refs, name=nil) + + port = port.to_i + + info = { :workspace => wspace, :host => addr, :port => port, :proto => protocol } + if name and name != 'unknown' + info[:name] = name + end + + report_service(info) + + return if qid == 0 + + report_vuln( + :workspace => wspace, + :host => addr, + :port => port, + :proto => protocol, + :name => 'QUALYS-' + qid, + :refs => refs) + end + def process_nexpose_data_sxml_refs(vuln) refs = [] vid = vuln.attributes['id'].to_s.downcase diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index ee9077fc83..01faf075c5 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -61,6 +61,7 @@ class Db "db_import_nessus_nbe" => "Import a Nessus scan result file (NBE)", "db_import_nessus_xml" => "Import a Nessus scan result file (NESSUS)", "db_import_nmap_xml" => "Import a Nmap scan results file (-oX)", + "db_import_qualys_xml" => "Import a Qualys scan results file (XML)", "db_nmap" => "Executes nmap and records the output automatically", } @@ -456,9 +457,9 @@ class Db when '-X' targ_exc << OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(args.shift) when '-PI' - port_inc = Rex::Socket.portspec_crack(args.shift) + port_inc = Rex::Socket.portspec_to_portlist(args.shift) when '-PX' - port_exc = Rex::Socket.portspec_crack(args.shift) + port_exc = Rex::Socket.portspec_to_portlist(args.shift) when '-m' regx = args.shift when '-R' @@ -929,6 +930,22 @@ class Db framework.db.import_nmap_xml_file(args[0]) end + # + # Import Qualys XML files + # + def cmd_db_import_qualys_xml(*args) + if not (args and args.length == 1) + print_status("Usage: db_import_qualys_xml ") + return + end + + if not File.readable?(args[0]) + print_status("Could not read the Qualys file") + return + end + framework.db.import_qualys_xml_file(args[0]) + end + # # Import IP List from a file # diff --git a/lib/rex/socket.rb b/lib/rex/socket.rb index 8a29946053..6a0dca6262 100644 --- a/lib/rex/socket.rb +++ b/lib/rex/socket.rb @@ -350,11 +350,16 @@ module Socket [ (~((2 ** (32 - bitmask)) - 1)) & 0xffffffff ].pack('N').unpack('CCCC').join('.') end + + def self.portspec_crack(pspec) + portspec_to_portlist(pspec) + end + # # Converts a port specification like "80,21-23,443" into a sorted, # unique array of valid port numbers like [21,22,23,80,443] # - def self.portspec_crack(pspec) + def self.portspec_to_portlist(pspec) ports = [] # Build ports array from port specification @@ -370,7 +375,38 @@ module Socket end # Sort, and remove dups and invalid ports - ports.sort.uniq.delete_if { |p| p < 0 or p > 65535 } + ports.sort.uniq.delete_if { |p| p < 1 or p > 65535 } + end + + # + # Converts a port list like [1,2,3,4,5,100] into a + # range specification like "1-5,100" + # + def self.portlist_to_portspec(parr) + ranges = [] + range = [] + lastp = nil + + parr.uniq.sort{|a,b| a<=>b}.map{|a| a.to_i}.each do |n| + next if (n < 1 or n > 65535) + if not lastp + range = [n] + lastp = n + next + end + + if lastp == n - 1 + range << n + else + ranges << range + range = [n] + end + lastp = n + end + + ranges << range + ranges.delete(nil) + ranges.uniq.map{|x| x.length == 1 ? "#{x[0]}" : "#{x[0]}-#{x[-1]}"}.join(",") end ## @@ -396,18 +432,18 @@ module Socket # # Create a TCP socket pair. # - # sf: This create a socket pair using native ruby sockets and will work + # sf: This create a socket pair using native ruby sockets and will work # on Windows where ::Socket.pair is not implemented. # Note: OpenSSL requires native ruby sockets for its io. # def self.tcp_socket_pair lsock = nil rsock = nil - laddr = '127.0.0.1' + laddr = '127.0.0.1' lport = 0 threads = [] mutex = ::Mutex.new - + threads << ::Thread.new { server = nil mutex.synchronize { @@ -424,31 +460,31 @@ module Socket lsock, saddr = server.accept server.close } - + threads.each { |t| t.join } - + return [lsock, rsock] end - + # # Create a UDP socket pair using native ruby UDP sockets. # def self.udp_socket_pair - laddr = '127.0.0.1' - + laddr = '127.0.0.1' + lsock = ::UDPSocket.new lsock.bind( laddr, 0 ) - + rsock = ::UDPSocket.new rsock.bind( laddr, 0 ) - + rsock.connect( *lsock.addr.values_at(3,1) ) - + lsock.connect( *rsock.addr.values_at(3,1) ) - + return [lsock, rsock] end - + ## # # Class initialization