Path: blob/master/lib/rex/parser/nmap_xml.rb
27991 views
# -*- coding: binary -*-12require 'rexml/document'3require 'rexml/streamlistener'45module Rex6module Parser78#9# Stream parser for nmap -oX xml output10#11# Yields a hash representing each host found in the xml stream. Each host12# will look something like the following:13# {14# "status" => "up",15# "addrs" => { "ipv4" => "192.168.0.1", "mac" => "00:0d:87:a1:df:72" },16# "ports" => [17# { "portid" => "22", "state" => "closed", ... },18# { "portid" => "80", "state" => "open", ... },19# ...20# ]21# }22#23# Usage:24# parser = NmapXMLStreamParser.new { |host|25# # do stuff with the host26# }27# REXML::Document.parse_stream(File.new(nmap_xml), parser)28# -- or --29# parser = NmapXMLStreamParser.new30# parser.on_found_host = Proc.new { |host|31# # do stuff with the host32# }33# REXML::Document.parse_stream(File.new(nmap_xml), parser)34#35# This parser does not maintain state as well as a tree parser, so malformed36# xml will trip it up. Nmap shouldn't ever output malformed xml, so it's not37# a big deal.38#39class NmapXMLStreamParser40include REXML::StreamListener4142#43# Callback for processing each found host44#45attr_accessor :on_found_host4647#48# Create a new stream parser for NMAP XML output49#50# If given a block, it will be stored in +on_found_host+, otherwise you51# need to set it explicitly, e.g.:52# parser = NmapXMLStreamParser.new53# parser.on_found_host = Proc.new { |host|54# # do stuff with the host55# }56# REXML::Document.parse_stream(File.new(nmap_xml), parser)57#58def initialize(&block)59reset_state60on_found_host = block if block61end6263def reset_state64@host = { "status" => nil, "addrs" => {}, "ports" => [], "scripts" => {} }65@state = nil66end6768def tag_start(name, attributes)69begin70case name71when "address"72@host["addrs"][attributes["addrtype"]] = attributes["addr"]73if (attributes["addrtype"] =~ /ipv[46]/)74@host["addr"] = attributes["addr"]75end76when "osclass"77# If there is more than one, take the highest accuracy. In case of78# a tie, this will have the effect of taking the last one in the79# list. Last is really no better than first but nmap appears to80# put OSes in chronological order, at least for Windows.81# Accordingly, this will report XP instead of 2000, 7 instead of82# Vista, etc, when each has the same accuracy.83if (@host["os_accuracy"].to_i <= attributes["accuracy"].to_i)84@host["os_vendor"] = attributes["vendor"]85@host["os_family"] = attributes["osfamily"]86@host["os_version"] = attributes["osgen"]87@host["os_accuracy"] = attributes["accuracy"]88end89when "osmatch"90if(attributes["accuracy"].to_i == 100)91@host["os_match"] = attributes["name"]92end93when "uptime"94@host["last_boot"] = attributes["lastboot"]95when "hostname"96if(attributes["type"] == "PTR")97@host["reverse_dns"] = attributes["name"]98end99when "status"100# <status> refers to the liveness of the host; values are "up" or "down"101@host["status"] = attributes["state"]102@host["status_reason"] = attributes["reason"]103when "port"104@host["ports"].push(attributes)105when "state"106# <state> refers to the state of a port; values are "open", "closed", or "filtered"107@host["ports"].last["state"] = attributes["state"]108when "service"109# Store any service and script info with the associated port. There shouldn't110# be any collisions on attribute names here, so just merge them.111@host["ports"].last.merge!(attributes)112when "script"113# Associate scripts under a port tag with the appropriate port.114# Other scripts from <hostscript> tags can only be associated with115# the host and scripts from <postscript> tags don't really belong116# to anything, so ignore them117if @state == :in_port_tag118@host["ports"].last["scripts"] ||= {}119@host["ports"].last["scripts"][attributes["id"]] = attributes["output"]120elsif @host121@host["scripts"] ||= {}122@host["scripts"][attributes["id"]] = attributes["output"]123else124# post scripts are used for things like comparing all the found125# ssh keys to see if multiple hosts have the same key126# fingerprint. Ignore them.127end128when "trace"129@host["trace"] = {"port" => attributes["port"], "proto" => attributes["proto"], "hops" => [] }130when "hop"131if @host["trace"]132@host["trace"]["hops"].push(attributes)133end134end135rescue NoMethodError => err136raise err unless err.message =~ /NilClass/137end138end139140def tag_end(name)141case name142when "port"143@state = nil144when "host"145on_found_host.call(@host) if on_found_host146reset_state147end148end149end150151end152end153154155156