257 lines
7.6 KiB
Ruby
257 lines
7.6 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 MbsaDocument < Nokogiri::XML::SAX::Document
|
|
|
|
include NokogiriDocMixin
|
|
|
|
# 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
|
|
@state[:current_tag][name] = true
|
|
case name
|
|
when "SecScan"
|
|
record_host(attrs)
|
|
when "IP" # TODO: Check to see if IPList/IP is useful to import
|
|
when "Check" # A list of MBSA checks. They have an ID and a Name.
|
|
record_check(attrs)
|
|
when "Advice" # Check advice. Free form text about the check
|
|
@state[:has_text] = true
|
|
when "Detail" # Check/Detail is where missing fixes are.
|
|
record_detail(attrs)
|
|
when "UpdateData" # Info about installed/missing hotfixes
|
|
record_updatedata(attrs)
|
|
when "Title" # MSB Title
|
|
@state[:has_text] = true
|
|
when "InformationURL" # Only use this if we don't have a Bulletin ID
|
|
@state[:has_text] = true
|
|
end
|
|
end
|
|
|
|
# This breaks xml-encoded characters, so need to append
|
|
def characters(text)
|
|
return unless @state[:has_text]
|
|
@text ||= ""
|
|
@text << text
|
|
end
|
|
|
|
# When we exit a tag, this is triggered.
|
|
def end_element(name=nil)
|
|
block = @block
|
|
case name
|
|
when "SecScan" # 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_vulns(host_object,&block)
|
|
end
|
|
# Reset the state once we close a host
|
|
@state.delete_if {|k| k != :current_tag}
|
|
when "Check"
|
|
collect_check_data
|
|
when "Advice"
|
|
@state[:has_text] = false
|
|
collect_advice_data
|
|
when "Detail"
|
|
collect_detail_data
|
|
when "UpdateData"
|
|
collect_updatedata
|
|
when "Title"
|
|
@state[:has_text] = false
|
|
collect_title
|
|
when "InformationURL"
|
|
collect_url
|
|
@state[:has_text] = false
|
|
end
|
|
@state[:current_tag].delete name
|
|
end
|
|
|
|
def report_fingerprint(host_object)
|
|
return unless host_object.kind_of? ::Mdm::Host
|
|
return unless @report_data[:os_fingerprint]
|
|
fp_note = @report_data[:os_fingerprint].merge(
|
|
{
|
|
:workspace => host_object.workspace,
|
|
:host => host_object
|
|
})
|
|
db_report(:note, fp_note)
|
|
end
|
|
|
|
def collect_url
|
|
return unless in_tag("References")
|
|
return unless in_tag("UpdateData")
|
|
return unless in_tag("Detail")
|
|
return unless in_tag("Check")
|
|
@state[:update][:url] = @text.to_s.strip
|
|
@text = nil
|
|
end
|
|
|
|
def report_vulns(host_object, &block)
|
|
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|
|
|
next unless vuln[:refs]
|
|
if vuln[:refs].empty?
|
|
next
|
|
end
|
|
if block
|
|
db.emit(:vuln, ["Missing #{vuln[:name]}",1], &block) if block
|
|
end
|
|
db_report(:vuln, vuln.merge(:host => host_object))
|
|
end
|
|
end
|
|
|
|
def collect_title
|
|
return unless in_tag("SecScan")
|
|
return unless in_tag("Check")
|
|
collect_bulletin_title
|
|
@text = nil
|
|
end
|
|
|
|
def collect_bulletin_title
|
|
return unless @state[:check_state]["ID"] == 500.to_s
|
|
return unless in_tag("UpdateData")
|
|
return unless @state[:update]
|
|
return if @text.to_s.strip.empty?
|
|
@state[:update]["Title"] = @text.to_s.strip
|
|
end
|
|
|
|
def collect_updatedata
|
|
return unless in_tag("SecScan")
|
|
return unless in_tag("Check")
|
|
return unless in_tag("Detail")
|
|
collect_missing_update
|
|
@state[:updates] = {}
|
|
end
|
|
|
|
def collect_missing_update
|
|
return unless @state[:check_state]["ID"] == 500.to_s
|
|
return if @state[:update]["IsInstalled"] == "true"
|
|
@report_data[:missing_updates] ||= []
|
|
this_update = {}
|
|
this_update[:name] = @state[:update]["Title"].to_s.strip
|
|
this_update[:refs] = []
|
|
if @state[:update]["BulletinID"].empty?
|
|
this_update[:refs] << "URL-#{@state[:update][:url]}"
|
|
else
|
|
this_update[:refs] << "MSB-#{@state[:update]["BulletinID"]}"
|
|
end
|
|
@report_data[:missing_updates] << this_update
|
|
end
|
|
|
|
# So far, just care about Host OS
|
|
# There is assuredly more interesting things going on in here.
|
|
def collect_advice_data
|
|
return unless in_tag("SecScan")
|
|
return unless in_tag("Check")
|
|
collect_os_name
|
|
@text = nil
|
|
end
|
|
|
|
def collect_os_name
|
|
return unless @state[:check_state]["ID"] == 10101.to_s
|
|
return unless @text
|
|
return if @text.strip.empty?
|
|
os_match = @text.match(/Computer is running (.*)/)
|
|
return unless os_match
|
|
os_info = os_match[1]
|
|
os_vendor = os_info[/Microsoft/]
|
|
os_family = os_info[/Windows/]
|
|
os_version = os_info[/(XP|2000 Advanced Server|2000|2003|2008|SBS|Vista|7 .* Edition|7)/]
|
|
if os_info
|
|
@report_data[:os_fingerprint] = {}
|
|
@report_data[:os_fingerprint][:type] = "host.os.mbsa_fingerprint"
|
|
@report_data[:os_fingerprint][:data] = {
|
|
:os_vendor => os_vendor,
|
|
:os_family => os_family,
|
|
:os_version => os_version,
|
|
:os_accuracy => 100,
|
|
:os_match => os_info.gsub(/\x2e$/,"")
|
|
}
|
|
end
|
|
end
|
|
|
|
def collect_detail_data
|
|
return unless in_tag("SecScan")
|
|
return unless in_tag("Check")
|
|
if @report_data[:missing_updates]
|
|
@report_data[:vulns] = @report_data[:missing_updates]
|
|
end
|
|
end
|
|
|
|
def collect_check_data
|
|
return unless in_tag("SecScan")
|
|
@state[:check_state] = {}
|
|
end
|
|
|
|
def collect_host_data
|
|
return unless @state[:address]
|
|
return if @state[:address].strip.empty?
|
|
@report_data[:host] = @state[:address].strip
|
|
if @state[:hostname] && !@state[:hostname].empty?
|
|
@report_data[:name] = @state[:hostname]
|
|
end
|
|
@report_data[:state] = Msf::HostState::Alive
|
|
end
|
|
|
|
def report_host(&block)
|
|
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 record_updatedata(attrs)
|
|
return unless in_tag("SecScan")
|
|
return unless in_tag("Check")
|
|
return unless in_tag("Detail")
|
|
update_attrs = attr_hash(attrs)
|
|
@state[:update] = attr_hash(attrs)
|
|
end
|
|
|
|
def record_host(attrs)
|
|
host_attrs = attr_hash(attrs)
|
|
@state[:address] = host_attrs["IP"]
|
|
@state[:hostname] = host_attrs["Machine"]
|
|
end
|
|
|
|
def record_check(attrs)
|
|
return unless in_tag("SecScan")
|
|
@state[:check_state] = attr_hash(attrs)
|
|
end
|
|
|
|
def record_detail(attrs)
|
|
return unless in_tag("SecScan")
|
|
return unless in_tag("Check")
|
|
@state[:detail_state] = attr_hash(attrs)
|
|
end
|
|
|
|
# We need to override the usual host_is_okay because MBSA apparently
|
|
# doesn't report on open ports at all.
|
|
def host_is_okay
|
|
return false unless @report_data[:host]
|
|
return false unless valid_ip(@report_data[:host])
|
|
return false unless @report_data[:state] == Msf::HostState::Alive
|
|
if @args[:blacklist]
|
|
return false if @args[:blacklist].include?(@report_data[:host])
|
|
end
|
|
return true
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
end
|
|
|