4050 lines
106 KiB
Ruby
4050 lines
106 KiB
Ruby
require 'rex/parser/nmap_xml'
|
|
require 'rex/parser/nexpose_xml'
|
|
require 'rex/parser/retina_xml'
|
|
require 'rex/parser/netsparker_xml'
|
|
|
|
require 'rex/socket'
|
|
require 'zip'
|
|
require 'uri'
|
|
require 'tmpdir'
|
|
require 'fileutils'
|
|
|
|
module Msf
|
|
|
|
###
|
|
#
|
|
# The states that a host can be in.
|
|
#
|
|
###
|
|
module HostState
|
|
#
|
|
# The host is alive.
|
|
#
|
|
Alive = "alive"
|
|
#
|
|
# The host is dead.
|
|
#
|
|
Dead = "down"
|
|
#
|
|
# The host state is unknown.
|
|
#
|
|
Unknown = "unknown"
|
|
end
|
|
|
|
###
|
|
#
|
|
# The states that a service can be in.
|
|
#
|
|
###
|
|
module ServiceState
|
|
Open = "open"
|
|
Closed = "closed"
|
|
Filtered = "filtered"
|
|
Unknown = "unknown"
|
|
end
|
|
|
|
###
|
|
#
|
|
# Events that can occur in the host/service database.
|
|
#
|
|
###
|
|
module DatabaseEvent
|
|
|
|
#
|
|
# Called when an existing host's state changes
|
|
#
|
|
def on_db_host_state(host, ostate)
|
|
end
|
|
|
|
#
|
|
# Called when an existing service's state changes
|
|
#
|
|
def on_db_service_state(host, port, ostate)
|
|
end
|
|
|
|
#
|
|
# Called when a new host is added to the database. The host parameter is
|
|
# of type Host.
|
|
#
|
|
def on_db_host(host)
|
|
end
|
|
|
|
#
|
|
# Called when a new client is added to the database. The client
|
|
# parameter is of type Client.
|
|
#
|
|
def on_db_client(client)
|
|
end
|
|
|
|
#
|
|
# Called when a new service is added to the database. The service
|
|
# parameter is of type Service.
|
|
#
|
|
def on_db_service(service)
|
|
end
|
|
|
|
#
|
|
# Called when an applicable vulnerability is found for a service. The vuln
|
|
# parameter is of type Vuln.
|
|
#
|
|
def on_db_vuln(vuln)
|
|
end
|
|
|
|
#
|
|
# Called when a new reference is created.
|
|
#
|
|
def on_db_ref(ref)
|
|
end
|
|
|
|
end
|
|
|
|
class DBImportError < RuntimeError
|
|
end
|
|
|
|
###
|
|
#
|
|
# The DB module ActiveRecord definitions for the DBManager
|
|
#
|
|
###
|
|
class DBManager
|
|
|
|
def ipv4_validator(addr)
|
|
return false unless addr.kind_of? String
|
|
addr =~ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
end
|
|
|
|
# Takes a space-delimited set of ips and ranges, and subjects
|
|
# them to RangeWalker for validation. Returns true or false.
|
|
def validate_ips(ips)
|
|
ret = true
|
|
begin
|
|
ips.split(' ').each {|ip|
|
|
unless Rex::Socket::RangeWalker.new(ip).ranges
|
|
ret = false
|
|
break
|
|
end
|
|
}
|
|
rescue
|
|
ret = false
|
|
end
|
|
return ret
|
|
end
|
|
|
|
|
|
#
|
|
# Determines if the database is functional
|
|
#
|
|
def check
|
|
res = Host.find(:first)
|
|
end
|
|
|
|
|
|
def default_workspace
|
|
Workspace.default
|
|
end
|
|
|
|
def find_workspace(name)
|
|
Workspace.find_by_name(name)
|
|
end
|
|
|
|
#
|
|
# Creates a new workspace in the database
|
|
#
|
|
def add_workspace(name)
|
|
Workspace.find_or_create_by_name(name)
|
|
end
|
|
|
|
def workspaces
|
|
Workspace.find(:all)
|
|
end
|
|
|
|
#
|
|
# Wait for all pending write to finish
|
|
#
|
|
def sync
|
|
task = queue( Proc.new { } )
|
|
task.wait
|
|
end
|
|
|
|
#
|
|
# Find a host. Performs no database writes.
|
|
#
|
|
def get_host(opts)
|
|
if opts.kind_of? Host
|
|
return opts
|
|
elsif opts.kind_of? String
|
|
raise RuntimeError, "This invokation of get_host is no longer supported: #{caller}"
|
|
else
|
|
address = opts[:addr] || opts[:address] || opts[:host] || return
|
|
return address if address.kind_of? Host
|
|
end
|
|
wspace = opts.delete(:workspace) || workspace
|
|
host = wspace.hosts.find_by_address(address)
|
|
return host
|
|
end
|
|
|
|
#
|
|
# Exactly like report_host but waits for the database to create a host and returns it.
|
|
#
|
|
def find_or_create_host(opts)
|
|
report_host(opts.merge({:wait => true}))
|
|
end
|
|
|
|
#
|
|
# Report a host's attributes such as operating system and service pack
|
|
#
|
|
# The opts parameter MUST contain
|
|
# :host -- the host's ip address
|
|
#
|
|
# The opts parameter can contain:
|
|
# :state -- one of the Msf::HostState constants
|
|
# :os_name -- one of the Msf::OperatingSystems constants
|
|
# :os_flavor -- something like "XP" or "Gentoo"
|
|
# :os_sp -- something like "SP2"
|
|
# :os_lang -- something like "English", "French", or "en-US"
|
|
# :arch -- one of the ARCH_* constants
|
|
# :mac -- the host's MAC address
|
|
#
|
|
def report_host(opts)
|
|
return if not active
|
|
addr = opts.delete(:host) || return
|
|
|
|
# Ensure the host field updated_at is changed on each report_host()
|
|
if addr.kind_of? Host
|
|
queue( Proc.new { addr.updated_at = addr.created_at; addr.save! } )
|
|
return addr
|
|
end
|
|
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
|
|
if opts[:host_mac]
|
|
opts[:mac] = opts.delete(:host_mac)
|
|
end
|
|
|
|
unless ipv4_validator(addr)
|
|
raise ::ArgumentError, "Invalid IP address in report_host(): #{addr}"
|
|
end
|
|
|
|
ret = {}
|
|
task = queue( Proc.new {
|
|
if opts[:comm] and opts[:comm].length > 0
|
|
host = wspace.hosts.find_or_initialize_by_address_and_comm(addr, opts[:comm])
|
|
else
|
|
host = wspace.hosts.find_or_initialize_by_address(addr)
|
|
end
|
|
|
|
opts.each { |k,v|
|
|
if (host.attribute_names.include?(k.to_s))
|
|
host[k] = v
|
|
else
|
|
dlog("Unknown attribute for Host: #{k}")
|
|
end
|
|
}
|
|
host.info = host.info[0,Host.columns_hash["info"].limit] if host.info
|
|
|
|
# Set default fields if needed
|
|
host.state = HostState::Alive if not host.state
|
|
host.comm = '' if not host.comm
|
|
host.workspace = wspace if not host.workspace
|
|
|
|
# Always save the host, helps track updates
|
|
msf_import_timestamps(opts,host)
|
|
host.save!
|
|
|
|
ret[:host] = host
|
|
} )
|
|
if wait
|
|
return nil if task.wait != :done
|
|
return ret[:host]
|
|
end
|
|
return task
|
|
end
|
|
|
|
#
|
|
# Iterates over the hosts table calling the supplied block with the host
|
|
# instance of each entry.
|
|
#
|
|
def each_host(wspace=workspace, &block)
|
|
wspace.hosts.each do |host|
|
|
block.call(host)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns a list of all hosts in the database
|
|
#
|
|
def hosts(wspace = workspace, only_up = false, addresses = nil)
|
|
conditions = {}
|
|
conditions[:state] = [Msf::HostState::Alive, Msf::HostState::Unknown] if only_up
|
|
conditions[:address] = addresses if addresses
|
|
wspace.hosts.all(:conditions => conditions, :order => :address)
|
|
end
|
|
|
|
|
|
|
|
def find_or_create_service(opts)
|
|
report_service(opts.merge({:wait => true}))
|
|
end
|
|
|
|
#
|
|
# Record a service in the database.
|
|
#
|
|
# opts must contain
|
|
# :host -- the host where this service is running
|
|
# :port -- the port where this service listens
|
|
# :proto -- the transport layer protocol (e.g. tcp, udp)
|
|
#
|
|
# opts may contain
|
|
# :name -- the application layer protocol (e.g. ssh, mssql, smb)
|
|
#
|
|
def report_service(opts)
|
|
return if not active
|
|
addr = opts.delete(:host) || return
|
|
hname = opts.delete(:host_name)
|
|
hmac = opts.delete(:host_mac)
|
|
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
|
|
hopts = {:workspace => wspace, :host => addr}
|
|
hopts[:name] = hname if hname
|
|
hopts[:mac] = hmac if hmac
|
|
report_host(hopts)
|
|
|
|
ret = {}
|
|
|
|
task = queue(Proc.new {
|
|
host = get_host(:workspace => wspace, :address => addr)
|
|
if host
|
|
host.updated_at = host.created_at
|
|
host.state = HostState::Alive
|
|
host.save!
|
|
end
|
|
|
|
proto = opts[:proto] || 'tcp'
|
|
opts[:name].downcase! if (opts[:name])
|
|
|
|
service = host.services.find_or_initialize_by_port_and_proto(opts[:port].to_i, proto)
|
|
opts.each { |k,v|
|
|
if (service.attribute_names.include?(k.to_s))
|
|
service[k] = v
|
|
else
|
|
dlog("Unknown attribute for Service: #{k}")
|
|
end
|
|
}
|
|
if (service.state == nil)
|
|
service.state = ServiceState::Open
|
|
end
|
|
if (service and service.changed?)
|
|
msf_import_timestamps(opts,service)
|
|
service.save!
|
|
end
|
|
ret[:service] = service
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:service]
|
|
end
|
|
return task
|
|
end
|
|
|
|
def get_service(wspace, host, proto, port)
|
|
host = get_host(:workspace => wspace, :address => host)
|
|
return if not host
|
|
return host.services.find_by_proto_and_port(proto, port)
|
|
end
|
|
|
|
#
|
|
# Iterates over the services table calling the supplied block with the
|
|
# service instance of each entry.
|
|
#
|
|
def each_service(wspace=workspace, &block)
|
|
services(wspace).each do |service|
|
|
block.call(service)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns a list of all services in the database
|
|
#
|
|
def services(wspace = workspace, only_up = false, proto = nil, addresses = nil, ports = nil, names = nil)
|
|
conditions = {}
|
|
conditions[:state] = [ServiceState::Open] if only_up
|
|
conditions[:proto] = proto if proto
|
|
conditions["hosts.address"] = addresses if addresses
|
|
conditions[:port] = ports if ports
|
|
conditions[:name] = names if names
|
|
wspace.services.all(:include => :host, :conditions => conditions, :order => "hosts.address, port")
|
|
end
|
|
|
|
|
|
def get_client(opts)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
host = get_host(:workspace => wspace, :host => opts[:host]) || return
|
|
client = host.clients.find(:first, :conditions => {:ua_string => opts[:ua_string]})
|
|
return client
|
|
end
|
|
|
|
def find_or_create_client(opts)
|
|
report_client(opts.merge({:wait => true}))
|
|
end
|
|
|
|
#
|
|
# Report a client running on a host.
|
|
#
|
|
# opts must contain
|
|
# :ua_string -- the value of the User-Agent header
|
|
# :host -- the host where this client connected from, can be an ip address or a Host object
|
|
#
|
|
# opts can contain
|
|
# :ua_name -- one of the Msf::HttpClients constants
|
|
# :ua_ver -- detected version of the given client
|
|
# :campaign -- an id or Campaign object
|
|
#
|
|
# Returns a Client.
|
|
#
|
|
def report_client(opts)
|
|
return if not active
|
|
addr = opts.delete(:host) || return
|
|
wspace = opts.delete(:workspace) || workspace
|
|
report_host(:workspace => wspace, :host => addr)
|
|
wait = opts.delete(:wait)
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
host = get_host(:workspace => wspace, :host => addr)
|
|
client = host.clients.find_or_initialize_by_ua_string(opts[:ua_string])
|
|
|
|
campaign = opts.delete(:campaign)
|
|
if campaign
|
|
case campaign
|
|
when Campaign
|
|
opts[:campaign_id] = campaign.id
|
|
else
|
|
opts[:campaign_id] = campaign
|
|
end
|
|
end
|
|
|
|
opts.each { |k,v|
|
|
if (client.attribute_names.include?(k.to_s))
|
|
client[k] = v
|
|
else
|
|
dlog("Unknown attribute for Client: #{k}")
|
|
end
|
|
}
|
|
if (client and client.changed?)
|
|
client.save!
|
|
end
|
|
ret[:client] = client
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:client]
|
|
end
|
|
return task
|
|
end
|
|
|
|
#
|
|
# This method iterates the vulns table calling the supplied block with the
|
|
# vuln instance of each entry.
|
|
#
|
|
def each_vuln(wspace=workspace,&block)
|
|
wspace.vulns.each do |vulns|
|
|
block.call(vulns)
|
|
end
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all vulnerabilities in the database
|
|
#
|
|
def vulns(wspace=workspace)
|
|
wspace.vulns
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all credentials in the database
|
|
#
|
|
def creds(wspace=workspace)
|
|
Cred.find(
|
|
:all,
|
|
:include => {:service => :host}, # That's some magic right there.
|
|
:conditions => ["hosts.workspace_id = ?", wspace.id]
|
|
)
|
|
end
|
|
|
|
#
|
|
# This method returns a list of all exploited hosts in the database.
|
|
#
|
|
def exploited_hosts(wspace=workspace)
|
|
wspace.exploited_hosts
|
|
end
|
|
|
|
#
|
|
# This method iterates the notes table calling the supplied block with the
|
|
# note instance of each entry.
|
|
#
|
|
def each_note(wspace=workspace, &block)
|
|
wspace.notes.each do |note|
|
|
block.call(note)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Find or create a note matching this type/data
|
|
#
|
|
def find_or_create_note(opts)
|
|
report_note(opts.merge({:wait => true}))
|
|
end
|
|
|
|
#
|
|
# Report a Note to the database. Notes can be tied to a Workspace, Host, or Service.
|
|
#
|
|
# opts MUST contain
|
|
# :data -- whatever it is you're making a note of
|
|
# :type -- The type of note, e.g. smb_peer_os
|
|
#
|
|
# opts can contain
|
|
# :workspace -- the workspace to associate with this Note
|
|
# :host -- an IP address or a Host object to associate with this Note
|
|
# :service -- a Service object to associate with this Note
|
|
# :port -- along with :host and proto, a service to associate with this Note
|
|
# :proto -- along with :host and port, a service to associate with this Note
|
|
# :update -- what to do in case a similar Note exists, see below
|
|
#
|
|
# The :update option can have the following values:
|
|
# :unique -- allow only a single Note per +host+/+type+ pair
|
|
# :unique_data -- like :uniqe, but also compare +data+
|
|
# :insert -- always insert a new Note even if one with identical values exists
|
|
#
|
|
# If the provided :host is an IP address and does not exist in the
|
|
# database, it will be created. If :workspace, :host and :service are all
|
|
# omitted, the new Note will be associated with the current workspace.
|
|
#
|
|
def report_note(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
seen = opts.delete(:seen) || false
|
|
crit = opts.delete(:critical) || false
|
|
host = nil
|
|
addr = nil
|
|
# Report the host so it's there for the Proc to use below
|
|
if opts[:host]
|
|
if opts[:host].kind_of? Host
|
|
host = opts[:host]
|
|
else
|
|
report_host({:workspace => wspace, :host => opts[:host]})
|
|
addr = opts[:host]
|
|
end
|
|
# Do the same for a service if that's also included.
|
|
if (opts[:port])
|
|
proto = nil
|
|
case opts[:proto].to_s.downcase # Catch incorrect usages
|
|
when 'tcp','udp'
|
|
proto = opts[:proto]
|
|
when 'dns','snmp','dhcp'
|
|
proto = 'udp'
|
|
sname = opts[:proto]
|
|
else
|
|
proto = 'tcp'
|
|
sname = opts[:proto]
|
|
end
|
|
sopts = {
|
|
:workspace => wspace,
|
|
:host => opts[:host],
|
|
:port => opts[:port],
|
|
:proto => proto
|
|
}
|
|
sopts[:name] = sname if sname
|
|
report_service(sopts)
|
|
end
|
|
end
|
|
# Update Modes can be :unique, :unique_data, :insert
|
|
mode = opts[:update] || :unique
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
if addr and not host
|
|
host = get_host(:workspace => wspace, :host => addr)
|
|
end
|
|
if host and (opts[:port] and opts[:proto])
|
|
service = get_service(wspace, host, opts[:proto], opts[:port])
|
|
elsif opts[:service] and opts[:service].kind_of? Service
|
|
service = opts[:service]
|
|
end
|
|
|
|
if host
|
|
host.updated_at = host.created_at
|
|
host.state = HostState::Alive
|
|
host.save!
|
|
end
|
|
|
|
ntype = opts.delete(:type) || opts.delete(:ntype) || (raise RuntimeError, "A note :type or :ntype is required")
|
|
data = opts[:data] || (raise RuntimeError, "Note :data is required")
|
|
method = nil
|
|
args = []
|
|
note = nil
|
|
|
|
conditions = { :ntype => ntype }
|
|
conditions[:host_id] = host[:id] if host
|
|
conditions[:service_id] = service[:id] if service
|
|
|
|
notes = wspace.notes.find(:all, :conditions => conditions)
|
|
|
|
case mode
|
|
when :unique
|
|
# Only one note of this type should exist, make a new one if it
|
|
# isn't there. If it is, grab it and overwrite its data.
|
|
if notes.empty?
|
|
note = wspace.notes.new(conditions)
|
|
else
|
|
note = notes[0]
|
|
end
|
|
note.data = data
|
|
when :unique_data
|
|
# Don't make a new Note with the same data as one that already
|
|
# exists for the given: type and (host or service)
|
|
notes.each do |n|
|
|
# Compare the deserialized data from the table to the raw
|
|
# data we're looking for. Because of the serialization we
|
|
# can't do this easily or reliably in SQL.
|
|
if n.data == data
|
|
note = n
|
|
break
|
|
end
|
|
end
|
|
if not note
|
|
# We didn't find one with the data we're looking for, make
|
|
# a new one.
|
|
note = wspace.notes.new(conditions.merge(:data => data))
|
|
end
|
|
else
|
|
# Otherwise, assume :insert, which means always make a new one
|
|
note = wspace.notes.new
|
|
if host
|
|
note.host_id = host[:id]
|
|
end
|
|
if opts[:service] and opts[:service].kind_of? Service
|
|
note.service_id = opts[:service][:id]
|
|
end
|
|
note.seen = seen
|
|
note.critical = crit
|
|
note.ntype = ntype
|
|
note.data = data
|
|
end
|
|
msf_import_timestamps(opts,note)
|
|
note.save!
|
|
|
|
ret[:note] = note
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:note]
|
|
end
|
|
return task
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all notes in the database
|
|
#
|
|
def notes(wspace=workspace)
|
|
wspace.notes
|
|
end
|
|
|
|
# report_auth_info used to create a note, now it creates
|
|
# an entry in the creds table. It's much more akin to
|
|
# report_vuln() now.
|
|
#
|
|
# opts must contain
|
|
# :host -- an IP address
|
|
# :port -- a port number
|
|
#
|
|
# opts can contain
|
|
# :user -- the username
|
|
# :pass -- the password, or path to ssh_key
|
|
# :ptype -- the type of password (password, hash, or ssh_key)
|
|
# :proto -- a transport name for the port
|
|
# :sname -- service name
|
|
# :active -- by default, a cred is active, unless explicitly false
|
|
# :proof -- data used to prove the account is actually active.
|
|
#
|
|
# Sources: Credentials can be sourced from another credential, or from
|
|
# a vulnerability. For example, if an exploit was used to dump the
|
|
# smb_hashes, and this credential comes from there, the source_id would
|
|
# be the Vuln id (as reported by report_vuln) and the type would be "Vuln".
|
|
#
|
|
# :source_id -- The Vuln or Cred id of the source of this cred.
|
|
# :source_type -- Either Vuln or Cred
|
|
#
|
|
# TODO: This is written somewhat host-centric, when really the
|
|
# Service is the thing. Need to revisit someday.
|
|
def report_auth_info(opts={})
|
|
return if not active
|
|
raise ArgumentError.new("Missing required option :host") if opts[:host].nil?
|
|
raise ArgumentError.new("Invalid address for :host") unless validate_ips(opts[:host])
|
|
raise ArgumentError.new("Missing required option :port") if opts[:port].nil?
|
|
host = opts.delete(:host)
|
|
ptype = opts.delete(:type) || "password"
|
|
token = [opts.delete(:user), opts.delete(:pass)]
|
|
sname = opts.delete(:sname)
|
|
port = opts.delete(:port)
|
|
proto = opts.delete(:proto) || "tcp"
|
|
proof = opts.delete(:proof)
|
|
source_id = opts.delete(:source_id)
|
|
source_type = opts.delete(:source_type)
|
|
# Nil is true for active.
|
|
active = (opts[:active] || opts[:active].nil?) ? true : false
|
|
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
|
|
# Service management; assume the user knows what
|
|
# he's talking about.
|
|
unless service = get_service(wspace, host, proto, port)
|
|
report_service(:host => host, :port => port, :proto => proto, :name => sname, :workspace => wspace)
|
|
end
|
|
|
|
ret = {}
|
|
task = queue( Proc.new {
|
|
|
|
# Get the service
|
|
service ||= get_service(wspace, host, proto, port)
|
|
|
|
# Create the cred by username only (so we can change passwords)
|
|
cred = service.creds.find_or_initialize_by_user_and_ptype(token[0] || "", ptype)
|
|
|
|
# Update with the password
|
|
cred.pass = (token[1] || "")
|
|
|
|
# Annotate the credential
|
|
cred.ptype = ptype
|
|
cred.active = active
|
|
|
|
# Update the source ID only if there wasn't already one.
|
|
if source_id and !cred.source_id
|
|
cred.source_id = source_id
|
|
cred.source_type = source_type if source_type
|
|
end
|
|
|
|
# Safe proof (lazy way) -- doesn't chop expanded
|
|
# characters correctly, but shouldn't ever be a problem.
|
|
unless proof.nil?
|
|
proof = Rex::Text.to_hex_ascii(proof)
|
|
proof = proof[0,4096]
|
|
end
|
|
cred.proof = proof
|
|
|
|
# Update the timestamp
|
|
if cred.changed?
|
|
msf_import_timestamps(opts,cred)
|
|
cred.save!
|
|
end
|
|
|
|
# Ensure the updated_at is touched any time report_auth_info is called
|
|
# except when it's set explicitly (as it is for imports)
|
|
unless opts[:updated_at] || opts["updated_at"]
|
|
cred.updated_at = Time.now.utc
|
|
cred.save!
|
|
end
|
|
|
|
ret[:cred] = cred
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:cred]
|
|
end
|
|
return task
|
|
end
|
|
|
|
alias :report_cred :report_auth_info
|
|
|
|
#
|
|
# Find or create a credential matching this type/data
|
|
#
|
|
def find_or_create_cred(opts)
|
|
report_auth_info(opts.merge({:wait => true}))
|
|
end
|
|
|
|
#
|
|
# This method iterates the creds table calling the supplied block with the
|
|
# cred instance of each entry.
|
|
#
|
|
def each_cred(wspace=workspace,&block)
|
|
wspace.creds.each do |cred|
|
|
block.call(cred)
|
|
end
|
|
end
|
|
|
|
def each_exploited_host(wspace=workspace,&block)
|
|
wspace.exploited_hosts.each do |eh|
|
|
block.call(eh)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Find or create a vuln matching this service/name
|
|
#
|
|
def find_or_create_vuln(opts)
|
|
report_vuln(opts.merge({:wait => true}))
|
|
end
|
|
|
|
#
|
|
# 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
|
|
# :info -- 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
|
|
raise ArgumentError.new("Missing required option :host") if opts[:host].nil?
|
|
raise ArgumentError.new("Deprecated data column for vuln, use .info instead") if opts[:data]
|
|
name = opts[:name] || return
|
|
info = opts[:info]
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
rids = nil
|
|
if opts[:refs]
|
|
rids = []
|
|
opts[:refs].each do |r|
|
|
if r.respond_to? :ctx_id
|
|
r = r.ctx_id + '-' + r.ctx_val
|
|
end
|
|
rids << find_or_create_ref(:name => r)
|
|
end
|
|
end
|
|
|
|
host = nil
|
|
addr = nil
|
|
if opts[:host].kind_of? Host
|
|
host = opts[:host]
|
|
else
|
|
report_host({:workspace => wspace, :host => opts[:host]})
|
|
addr = opts[:host]
|
|
end
|
|
|
|
ret = {}
|
|
task = queue( Proc.new {
|
|
if host
|
|
host.updated_at = host.created_at
|
|
host.state = HostState::Alive
|
|
host.save!
|
|
else
|
|
host = get_host(:workspace => wspace, :address => addr)
|
|
end
|
|
|
|
if info
|
|
vuln = host.vulns.find_or_initialize_by_name_and_info(name, info, :include => :refs)
|
|
else
|
|
vuln = host.vulns.find_or_initialize_by_name(name, :include => :refs)
|
|
end
|
|
|
|
if opts[:port]
|
|
proto = nil
|
|
case opts[:proto].to_s.downcase # Catch incorrect usages, as in report_note
|
|
when 'tcp','udp'
|
|
proto = opts[:proto]
|
|
when 'dns','snmp','dhcp'
|
|
proto = 'udp'
|
|
sname = opts[:proto]
|
|
else
|
|
proto = 'tcp'
|
|
sname = opts[:proto]
|
|
end
|
|
vuln.service = host.services.find_or_create_by_port_and_proto(opts[:port], proto)
|
|
end
|
|
|
|
if rids
|
|
vuln.refs << (rids - vuln.refs)
|
|
end
|
|
|
|
if vuln.changed?
|
|
msf_import_timestamps(opts,vuln)
|
|
vuln.save!
|
|
end
|
|
ret[:vuln] = vuln
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:vuln]
|
|
end
|
|
return task
|
|
end
|
|
|
|
def get_vuln(wspace, host, service, name, data='')
|
|
raise RuntimeError, "Not workspace safe: #{caller.inspect}"
|
|
vuln = nil
|
|
if (service)
|
|
vuln = Vuln.find(:first, :conditions => [ "name = ? and service_id = ? and host_id = ?", name, service.id, host.id])
|
|
else
|
|
vuln = Vuln.find(:first, :conditions => [ "name = ? and host_id = ?", name, host.id])
|
|
end
|
|
|
|
return vuln
|
|
end
|
|
|
|
#
|
|
# Find or create a reference matching this name
|
|
#
|
|
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?
|
|
ref.save!
|
|
end
|
|
ret[:ref] = ref
|
|
})
|
|
return nil if task.wait() != :done
|
|
return ret[:ref]
|
|
end
|
|
def get_ref(name)
|
|
Ref.find_by_name(name)
|
|
end
|
|
|
|
def report_exploit(opts={})
|
|
return if not active
|
|
raise ArgumentError.new("Missing required option :host") if opts[:host].nil?
|
|
wait = opts[:wait]
|
|
wspace = opts.delete(:workspace) || workspace
|
|
host = nil
|
|
addr = nil
|
|
sname = opts.delete(:sname)
|
|
port = opts.delete(:port)
|
|
proto = opts.delete(:proto) || "tcp"
|
|
name = opts.delete(:name)
|
|
payload = opts.delete(:payload)
|
|
session_uuid = opts.delete(:session_uuid)
|
|
|
|
if opts[:host].kind_of? Host
|
|
host = opts[:host]
|
|
else
|
|
report_host({:workspace => wspace, :host => opts[:host]})
|
|
addr = opts[:host]
|
|
end
|
|
|
|
if opts[:service].kind_of? Service
|
|
service = opts[:service]
|
|
elsif port
|
|
report_service(:host => host, :port => port, :proto => proto, :name => sname)
|
|
service = get_service(wspace, host, proto, port)
|
|
else
|
|
service = nil
|
|
end
|
|
|
|
ret = {}
|
|
|
|
task = queue(
|
|
Proc.new {
|
|
if host
|
|
host.updated_at = host.created_at
|
|
host.state = HostState::Alive
|
|
host.save!
|
|
else
|
|
host = get_host(:workspace => wspace, :address => addr)
|
|
end
|
|
exploit_info = {
|
|
:workspace => wspace,
|
|
:host_id => host.id,
|
|
:name => name,
|
|
:payload => payload,
|
|
}
|
|
exploit_info[:service_id] = service.id if service
|
|
exploit_info[:session_uuid] = session_uuid if session_uuid
|
|
exploit_record = ExploitedHost.create(exploit_info)
|
|
exploit_record.save!
|
|
|
|
ret[:exploit] = exploit_record
|
|
}
|
|
)
|
|
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:exploit]
|
|
end
|
|
return task
|
|
|
|
end
|
|
|
|
|
|
#
|
|
# Deletes a host and associated data matching this address/comm
|
|
#
|
|
def del_host(wspace, address, comm='')
|
|
host = wspace.hosts.find_by_address_and_comm(address, comm)
|
|
host.destroy if host
|
|
end
|
|
|
|
#
|
|
# Deletes a port and associated vulns matching this port
|
|
#
|
|
def del_service(wspace, address, proto, port, comm='')
|
|
|
|
host = get_host(:workspace => wspace, :address => address)
|
|
return unless host
|
|
|
|
host.services.all(:conditions => {:proto => proto, :port => port}).each { |s| s.destroy }
|
|
end
|
|
|
|
#
|
|
# Find a reference matching this name
|
|
#
|
|
def has_ref?(name)
|
|
Ref.find_by_name(name)
|
|
end
|
|
|
|
#
|
|
# Find a vulnerability matching this name
|
|
#
|
|
def has_vuln?(name)
|
|
Vuln.find_by_name(name)
|
|
end
|
|
|
|
#
|
|
# Look for an address across all comms
|
|
#
|
|
def has_host?(wspace,addr)
|
|
wspace.hosts.find_by_address(addr)
|
|
end
|
|
|
|
def events(wspace=workspace)
|
|
wspace.events.find :all, :order => 'created_at ASC'
|
|
end
|
|
|
|
def report_event(opts = {})
|
|
return if not active
|
|
wspace = opts.delete(:workspace) || workspace
|
|
uname = opts.delete(:username)
|
|
|
|
if opts[:host]
|
|
report_host(:workspace => wspace, :host => opts[:host])
|
|
end
|
|
framework.db.queue(Proc.new {
|
|
opts[:host] = get_host(:workspace => wspace, :host => opts[:host]) if opts[:host]
|
|
Event.create(opts.merge(:workspace_id => wspace[:id], :username => uname))
|
|
})
|
|
end
|
|
|
|
#
|
|
# Loot collection
|
|
#
|
|
#
|
|
# This method iterates the loot table calling the supplied block with the
|
|
# instance of each entry.
|
|
#
|
|
def each_loot(wspace=workspace, &block)
|
|
wspace.loots.each do |note|
|
|
block.call(note)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Find or create a loot matching this type/data
|
|
#
|
|
def find_or_create_loot(opts)
|
|
report_loot(opts.merge({:wait => true}))
|
|
end
|
|
|
|
def report_loot(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
path = opts.delete(:path) || (raise RuntimeError, "A loot :path is required")
|
|
|
|
host = nil
|
|
addr = nil
|
|
|
|
# Report the host so it's there for the Proc to use below
|
|
if opts[:host]
|
|
if opts[:host].kind_of? Host
|
|
host = opts[:host]
|
|
else
|
|
report_host({:workspace => wspace, :host => opts[:host]})
|
|
addr = opts[:host]
|
|
end
|
|
end
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
|
|
if addr and not host
|
|
host = get_host(:workspace => wspace, :host => addr)
|
|
end
|
|
|
|
ltype = opts.delete(:type) || opts.delete(:ltype) || (raise RuntimeError, "A loot :type or :ltype is required")
|
|
ctype = opts.delete(:ctype) || opts.delete(:content_type) || 'text/plain'
|
|
name = opts.delete(:name)
|
|
info = opts.delete(:info)
|
|
data = opts[:data]
|
|
loot = wspace.loots.new
|
|
|
|
if host
|
|
loot.host_id = host[:id]
|
|
end
|
|
if opts[:service] and opts[:service].kind_of? Service
|
|
loot.service_id = opts[:service][:id]
|
|
end
|
|
|
|
loot.path = path
|
|
loot.ltype = ltype
|
|
loot.content_type = ctype
|
|
loot.data = data
|
|
loot.name = name if name
|
|
loot.info = info if info
|
|
msf_import_timestamps(opts,loot)
|
|
loot.save!
|
|
|
|
if !opts[:created_at]
|
|
if host
|
|
host.updated_at = host.created_at
|
|
host.state = HostState::Alive
|
|
host.save!
|
|
end
|
|
end
|
|
|
|
ret[:loot] = loot
|
|
})
|
|
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:loot]
|
|
end
|
|
return task
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all loot in the database
|
|
#
|
|
def loots(wspace=workspace)
|
|
wspace.loots
|
|
end
|
|
|
|
#
|
|
# Find or create a task matching this type/data
|
|
#
|
|
def find_or_create_task(opts)
|
|
report_task(opts.merge({:wait => true}))
|
|
end
|
|
|
|
def report_task(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
path = opts.delete(:path) || (raise RuntimeError, "A task :path is required")
|
|
|
|
ret = {}
|
|
this_task = queue(Proc.new {
|
|
|
|
user = opts.delete(:user)
|
|
desc = opts.delete(:desc)
|
|
error = opts.delete(:error)
|
|
info = opts.delete(:info)
|
|
mod = opts.delete(:mod)
|
|
options = opts.delete(:options)
|
|
prog = opts.delete(:prog)
|
|
result = opts.delete(:result)
|
|
completed_at = opts.delete(:completed_at)
|
|
task = wspace.tasks.new
|
|
|
|
task.created_by = user
|
|
task.description = desc
|
|
task.error = error if error
|
|
task.info = info
|
|
task.module = mod
|
|
task.options = options
|
|
task.path = path
|
|
task.progress = prog
|
|
task.result = result if result
|
|
msf_import_timestamps(opts,task)
|
|
# Having blank completed_ats, while accurate, will cause unstoppable tasks.
|
|
if completed_at.nil? || completed_at.empty?
|
|
task.completed_at = opts[:updated_at]
|
|
else
|
|
task.completed_at = completed_at
|
|
end
|
|
task.save!
|
|
|
|
ret[:task] = task
|
|
})
|
|
|
|
if wait
|
|
return nil if this_task.wait() != :done
|
|
return ret[:task]
|
|
end
|
|
return this_task
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all tasks in the database
|
|
#
|
|
def tasks(wspace=workspace)
|
|
wspace.tasks
|
|
end
|
|
|
|
|
|
#
|
|
# Find or create a task matching this type/data
|
|
#
|
|
def find_or_create_report(opts)
|
|
report_report(opts.merge({:wait => true}))
|
|
end
|
|
|
|
def report_report(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
path = opts.delete(:path) || (raise RuntimeError, "A report :path is required")
|
|
|
|
ret = {}
|
|
this_task = queue(Proc.new {
|
|
|
|
user = opts.delete(:user)
|
|
options = opts.delete(:options)
|
|
rtype = opts.delete(:rtype)
|
|
report = wspace.reports.new
|
|
|
|
report.created_by = user
|
|
report.options = options
|
|
report.rtype = rtype
|
|
report.path = path
|
|
msf_import_timestamps(opts,report)
|
|
report.save!
|
|
|
|
ret[:task] = report
|
|
})
|
|
|
|
if wait
|
|
return nil if this_task.wait() != :done
|
|
return ret[:task]
|
|
end
|
|
return this_task
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all reports in the database
|
|
#
|
|
def reports(wspace=workspace)
|
|
wspace.reports
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Support methods
|
|
#
|
|
|
|
#
|
|
# Report a Web Site to the database. WebSites must be tied to an existing Service
|
|
#
|
|
# opts MUST contain
|
|
# :service* -- the service object this site should be associated with
|
|
# :vhost -- the virtual host name for this particular web site`
|
|
|
|
# If service is NOT specified, the following values are mandatory
|
|
# :host -- the ip address of the server hosting the web site
|
|
# :port -- the port number of the associated web site
|
|
# :ssl -- whether or not SSL is in use on this port
|
|
#
|
|
# These values will be used to create new host and service records
|
|
|
|
#
|
|
# opts can contain
|
|
# :options -- a hash of options for accessing this particular web site
|
|
|
|
#
|
|
# Duplicate records for a given host, port, vhost combination will be overwritten
|
|
#
|
|
|
|
def report_web_site(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
vhost = opts.delete(:vhost)
|
|
|
|
addr = nil
|
|
port = nil
|
|
name = nil
|
|
serv = nil
|
|
|
|
if opts[:service] and opts[:service].kind_of?(Service)
|
|
serv = opts[:service]
|
|
else
|
|
addr = opts[:host]
|
|
port = opts[:port]
|
|
name = opts[:ssl] ? 'https' : 'http'
|
|
if not (addr and port)
|
|
raise ArgumentError, "report_web_site requires service OR host/port/ssl"
|
|
end
|
|
end
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
|
|
host = serv ? serv.host : find_or_create_host(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:state => Msf::HostState::Alive
|
|
)
|
|
|
|
if host.name.to_s.empty?
|
|
host.name = vhost
|
|
host.save!
|
|
end
|
|
|
|
serv = serv ? serv : find_or_create_service(
|
|
:workspace => wspace,
|
|
:host => host,
|
|
:port => port,
|
|
:proto => 'tcp',
|
|
:state => 'open'
|
|
)
|
|
|
|
# Change the service name if it is blank or it has
|
|
# been explicitly specified.
|
|
if opts.keys.include?(:ssl) or serv.name.to_s.empty?
|
|
name = opts[:ssl] ? 'https' : 'http'
|
|
serv.name = name
|
|
serv.save!
|
|
end
|
|
|
|
host.updated_at = host.created_at
|
|
host.state = HostState::Alive
|
|
host.save!
|
|
|
|
vhost ||= host.address
|
|
|
|
site = WebSite.find_or_initialize_by_vhost_and_service_id(vhost, serv[:id])
|
|
site.options = opts[:options] if opts[:options]
|
|
|
|
# XXX:
|
|
msf_import_timestamps(opts, site)
|
|
site.save!
|
|
|
|
ret[:web_site] = site
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:web_site]
|
|
end
|
|
return task
|
|
end
|
|
|
|
|
|
#
|
|
# Report a Web Page to the database. WebPage must be tied to an existing Web Site
|
|
#
|
|
# opts MUST contain
|
|
# :web_site* -- the web site object that this page should be associated with
|
|
# :path -- the virtual host name for this particular web site
|
|
# :code -- the http status code from requesting this page
|
|
# :headers -- this is a HASH of headers (lowercase name as key) of ARRAYs of values
|
|
# :body -- the document body of the server response
|
|
# :query -- the query string after the path
|
|
|
|
# If web_site is NOT specified, the following values are mandatory
|
|
# :host -- the ip address of the server hosting the web site
|
|
# :port -- the port number of the associated web site
|
|
# :vhost -- the virtual host for this particular web site
|
|
# :ssl -- whether or not SSL is in use on this port
|
|
#
|
|
# These values will be used to create new host, service, and web_site records
|
|
#
|
|
# opts can contain
|
|
# :cookie -- the Set-Cookie headers, merged into a string
|
|
# :auth -- the Authorization headers, merged into a string
|
|
# :ctype -- the Content-Type headers, merged into a string
|
|
# :mtime -- the timestamp returned from the server of the last modification time
|
|
# :location -- the URL that a redirect points to
|
|
#
|
|
# Duplicate records for a given web_site, path, and query combination will be overwritten
|
|
#
|
|
|
|
def report_web_page(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
|
|
path = opts[:path]
|
|
code = opts[:code].to_i
|
|
body = opts[:body].to_s
|
|
query = opts[:query].to_s
|
|
headers = opts[:headers]
|
|
site = nil
|
|
|
|
if not (path and code and body and headers)
|
|
raise ArgumentError, "report_web_page requires the path, query, code, body, and headers parameters"
|
|
end
|
|
|
|
if opts[:web_site] and opts[:web_site].kind_of?(WebSite)
|
|
site = opts.delete(:web_site)
|
|
else
|
|
site = report_web_site(
|
|
:workspace => wspace,
|
|
:host => opts[:host], :port => opts[:port],
|
|
:vhost => opts[:host], :ssl => opts[:ssl],
|
|
:wait => true
|
|
)
|
|
if not site
|
|
raise ArgumentError, "report_web_page was unable to create the associated web site"
|
|
end
|
|
end
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
page = WebPage.find_or_initialize_by_web_site_id_and_path_and_query(site[:id], path, query)
|
|
page.code = code
|
|
page.body = body
|
|
page.headers = headers
|
|
page.cookie = opts[:cookie] if opts[:cookie]
|
|
page.auth = opts[:auth] if opts[:auth]
|
|
page.mtime = opts[:mtime] if opts[:mtime]
|
|
page.ctype = opts[:ctype] if opts[:ctype]
|
|
page.location = opts[:location] if opts[:location]
|
|
msf_import_timestamps(opts, page)
|
|
page.save!
|
|
|
|
ret[:web_page] = page
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:web_page]
|
|
end
|
|
return task
|
|
end
|
|
|
|
|
|
#
|
|
# Report a Web Form to the database. WebForm must be tied to an existing Web Site
|
|
#
|
|
# opts MUST contain
|
|
# :web_site* -- the web site object that this page should be associated with
|
|
# :path -- the virtual host name for this particular web site
|
|
# :query -- the query string that is appended to the path (not valid for GET)
|
|
# :method -- the form method, one of GET, POST, or PATH
|
|
# :params -- an ARRAY of all parameters and values specified in the form
|
|
#
|
|
# If web_site is NOT specified, the following values are mandatory
|
|
# :host -- the ip address of the server hosting the web site
|
|
# :port -- the port number of the associated web site
|
|
# :vhost -- the virtual host for this particular web site
|
|
# :ssl -- whether or not SSL is in use on this port
|
|
#
|
|
#
|
|
# Duplicate records for a given web_site, path, method, and params combination will be overwritten
|
|
#
|
|
|
|
def report_web_form(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
|
|
path = opts[:path]
|
|
meth = opts[:method].to_s.upcase
|
|
para = opts[:params]
|
|
quer = opts[:query].to_s
|
|
site = nil
|
|
|
|
if not (path and meth)
|
|
raise ArgumentError, "report_web_form requires the path and method parameters"
|
|
end
|
|
|
|
if not %W{GET POST PATH}.include?(meth)
|
|
raise ArgumentError, "report_web_form requires the method to be one of GET, POST, PATH"
|
|
end
|
|
|
|
if opts[:web_site] and opts[:web_site].kind_of?(WebSite)
|
|
site = opts.delete(:web_site)
|
|
else
|
|
site = report_web_site(
|
|
:workspace => wspace,
|
|
:host => opts[:host], :port => opts[:port],
|
|
:vhost => opts[:host], :ssl => opts[:ssl],
|
|
:wait => true
|
|
)
|
|
if not site
|
|
raise ArgumentError, "report_web_form was unable to create the associated web site"
|
|
end
|
|
end
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
|
|
# Since one of our serialized fields is used as a unique parameter, we must do the final
|
|
# comparisons through ruby and not SQL.
|
|
|
|
form = nil
|
|
WebForm.find_all_by_web_site_id_and_path_and_method_and_query(site[:id], path, meth, quer).each do |xform|
|
|
if xform.params == para
|
|
form = xform
|
|
break
|
|
end
|
|
end
|
|
|
|
if not form
|
|
form = WebForm.new
|
|
form.web_site_id = site[:id]
|
|
form.path = path
|
|
form.method = meth
|
|
form.params = para
|
|
form.query = quer
|
|
end
|
|
|
|
msf_import_timestamps(opts, form)
|
|
form.save!
|
|
|
|
ret[:web_form] = form
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:web_form]
|
|
end
|
|
return task
|
|
end
|
|
|
|
|
|
#
|
|
# Report a Web Vuln to the database. WebVuln must be tied to an existing Web Site
|
|
#
|
|
# opts MUST contain
|
|
# :web_site* -- the web site object that this page should be associated with
|
|
# :path -- the virtual host name for this particular web site
|
|
# :query -- the query string appended to the path (not valid for GET method flaws)
|
|
# :method -- the form method, one of GET, POST, or PATH
|
|
# :params -- an ARRAY of all parameters and values specified in the form
|
|
# :pname -- the specific field where the vulnerability occurs
|
|
# :proof -- the string showing proof of the vulnerability
|
|
# :risk -- an INTEGER value from 0 to 5 indicating the risk (5 is highest)
|
|
# :name -- the string indicating the type of vulnerability
|
|
#
|
|
# If web_site is NOT specified, the following values are mandatory
|
|
# :host -- the ip address of the server hosting the web site
|
|
# :port -- the port number of the associated web site
|
|
# :vhost -- the virtual host for this particular web site
|
|
# :ssl -- whether or not SSL is in use on this port
|
|
#
|
|
#
|
|
# Duplicate records for a given web_site, path, method, pname, and name combination will be overwritten
|
|
#
|
|
|
|
def report_web_vuln(opts)
|
|
return if not active
|
|
wait = opts.delete(:wait)
|
|
wspace = opts.delete(:workspace) || workspace
|
|
|
|
path = opts[:path]
|
|
meth = opts[:method]
|
|
para = opts[:params] || []
|
|
quer = opts[:query].to_s
|
|
pname = opts[:pname]
|
|
proof = opts[:proof]
|
|
risk = opts[:risk].to_i
|
|
name = opts[:name].to_s.strip
|
|
blame = opts[:blame].to_s.strip
|
|
desc = opts[:description].to_s.strip
|
|
conf = opts[:confidence].to_i
|
|
cat = opts[:category].to_s.strip
|
|
|
|
site = nil
|
|
|
|
if not (path and meth and proof and pname)
|
|
raise ArgumentError, "report_web_vuln requires the path, method, proof, risk, name, params, and pname parameters. Received #{opts.inspect}"
|
|
end
|
|
|
|
if not %W{GET POST PATH}.include?(meth)
|
|
raise ArgumentError, "report_web_vuln requires the method to be one of GET, POST, PATH. Received '#{meth}'"
|
|
end
|
|
|
|
if risk < 0 or risk > 5
|
|
raise ArgumentError, "report_web_vuln requires the risk to be between 0 and 5 (inclusive). Received '#{risk}'"
|
|
end
|
|
|
|
if conf < 0 or conf > 100
|
|
raise ArgumentError, "report_web_vuln requires the confidence to be between 1 and 100 (inclusive). Received '#{conf}'"
|
|
end
|
|
|
|
if cat.empty?
|
|
raise ArgumentError, "report_web_vuln requires the category to be a valid string"
|
|
end
|
|
|
|
if name.empty?
|
|
raise ArgumentError, "report_web_vuln requires the name to be a valid string"
|
|
end
|
|
|
|
if opts[:web_site] and opts[:web_site].kind_of?(WebSite)
|
|
site = opts.delete(:web_site)
|
|
else
|
|
site = report_web_site(
|
|
:workspace => wspace,
|
|
:host => opts[:host], :port => opts[:port],
|
|
:vhost => opts[:host], :ssl => opts[:ssl],
|
|
:wait => true
|
|
)
|
|
if not site
|
|
raise ArgumentError, "report_web_form was unable to create the associated web site"
|
|
end
|
|
end
|
|
|
|
ret = {}
|
|
task = queue(Proc.new {
|
|
|
|
meth = meth.to_s.upcase
|
|
|
|
vuln = WebVuln.find_or_initialize_by_web_site_id_and_path_and_method_and_pname_and_category_and_query(site[:id], path, meth, pname, cat, quer)
|
|
vuln.name = name
|
|
vuln.risk = risk
|
|
vuln.params = para
|
|
vuln.proof = proof.to_s
|
|
vuln.category = cat
|
|
vuln.blame = blame
|
|
vuln.description = desc
|
|
vuln.confidence = conf
|
|
msf_import_timestamps(opts, vuln)
|
|
vuln.save!
|
|
|
|
ret[:web_vuln] = vuln
|
|
})
|
|
if wait
|
|
return nil if task.wait() != :done
|
|
return ret[:web_vuln]
|
|
end
|
|
return task
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Selected host
|
|
#
|
|
def selected_host
|
|
selhost = WmapTarget.find(:first, :conditions => ["selected != 0"] )
|
|
if selhost
|
|
return selhost.host
|
|
else
|
|
return
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Selected port
|
|
#
|
|
def selected_port
|
|
WmapTarget.find(:first, :conditions => ["selected != 0"] ).port
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Selected ssl
|
|
#
|
|
def selected_ssl
|
|
WmapTarget.find(:first, :conditions => ["selected != 0"] ).ssl
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Selected id
|
|
#
|
|
def selected_id
|
|
WmapTarget.find(:first, :conditions => ["selected != 0"] ).object_id
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table identifiying possible targets
|
|
# This method wiil be remove on second phase of db merging.
|
|
#
|
|
def each_distinct_target(&block)
|
|
request_distinct_targets.each do |target|
|
|
block.call(target)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method returns a list of all possible targets available in requests
|
|
# This method wiil be remove on second phase of db merging.
|
|
#
|
|
def request_distinct_targets
|
|
WmapRequest.find(:all, :select => 'DISTINCT host,address,port,ssl')
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table returning a list of all requests of a specific target
|
|
#
|
|
def each_request_target_with_path(&block)
|
|
target_requests('AND wmap_requests.path IS NOT NULL').each do |req|
|
|
block.call(req)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table returning a list of all requests of a specific target
|
|
#
|
|
def each_request_target_with_query(&block)
|
|
target_requests('AND wmap_requests.query IS NOT NULL').each do |req|
|
|
block.call(req)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table returning a list of all requests of a specific target
|
|
#
|
|
def each_request_target_with_body(&block)
|
|
target_requests('AND wmap_requests.body IS NOT NULL').each do |req|
|
|
block.call(req)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table returning a list of all requests of a specific target
|
|
#
|
|
def each_request_target_with_headers(&block)
|
|
target_requests('AND wmap_requests.headers IS NOT NULL').each do |req|
|
|
block.call(req)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table returning a list of all requests of a specific target
|
|
#
|
|
def each_request_target(&block)
|
|
target_requests('').each do |req|
|
|
block.call(req)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method returns a list of all requests from target
|
|
#
|
|
def target_requests(extra_condition)
|
|
WmapRequest.find(:all, :conditions => ["wmap_requests.host = ? AND wmap_requests.port = ? #{extra_condition}",selected_host,selected_port])
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the requests table calling the supplied block with the
|
|
# request instance of each entry.
|
|
#
|
|
def each_request(&block)
|
|
requests.each do |request|
|
|
block.call(request)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method allows to query directly the requests table. To be used mainly by modules
|
|
#
|
|
def request_sql(host,port,extra_condition)
|
|
WmapRequest.find(:all, :conditions => ["wmap_requests.host = ? AND wmap_requests.port = ? #{extra_condition}",host,port])
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This methods returns a list of all targets in the database
|
|
#
|
|
def requests
|
|
WmapRequest.find(:all)
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This method iterates the targets table calling the supplied block with the
|
|
# target instance of each entry.
|
|
#
|
|
def each_target(&block)
|
|
targets.each do |target|
|
|
block.call(target)
|
|
end
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This methods returns a list of all targets in the database
|
|
#
|
|
def targets
|
|
WmapTarget.find(:all)
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# This methods deletes all targets from targets table in the database
|
|
#
|
|
def delete_all_targets
|
|
WmapTarget.delete_all
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Find a target matching this id
|
|
#
|
|
def get_target(id)
|
|
target = WmapTarget.find(:first, :conditions => [ "id = ?", id])
|
|
return target
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Create a target
|
|
#
|
|
def create_target(host,port,ssl,sel)
|
|
tar = WmapTarget.create(
|
|
:host => host,
|
|
:address => host,
|
|
:port => port,
|
|
:ssl => ssl,
|
|
:selected => sel
|
|
)
|
|
#framework.events.on_db_target(rec)
|
|
end
|
|
|
|
|
|
#
|
|
# WMAP
|
|
# Create a request (by hand)
|
|
#
|
|
def create_request(host,port,ssl,meth,path,headers,query,body,respcode,resphead,response)
|
|
req = WmapRequest.create(
|
|
:host => host,
|
|
:address => host,
|
|
:port => port,
|
|
:ssl => ssl,
|
|
:meth => meth,
|
|
:path => path,
|
|
:headers => headers,
|
|
:query => query,
|
|
:body => body,
|
|
:respcode => respcode,
|
|
:resphead => resphead,
|
|
:response => response
|
|
)
|
|
#framework.events.on_db_request(rec)
|
|
end
|
|
|
|
#
|
|
# WMAP
|
|
# Quick way to query the database (used by wmap_sql)
|
|
#
|
|
def sql_query(sqlquery)
|
|
ActiveRecord::Base.connection.select_all(sqlquery)
|
|
end
|
|
|
|
|
|
# Returns a REXML::Document from the given data.
|
|
def rexmlify(data)
|
|
doc = data.kind_of?(REXML::Document) ? data : REXML::Document.new(data)
|
|
end
|
|
|
|
# Handles timestamps from Metasploit Express imports.
|
|
def msf_import_timestamps(opts,obj)
|
|
obj.created_at = opts["created_at"] if opts["created_at"]
|
|
obj.created_at = opts[:created_at] if opts[:created_at]
|
|
obj.updated_at = opts["updated_at"] ? opts["updated_at"] : obj.created_at
|
|
obj.updated_at = opts[:updated_at] ? opts[:updated_at] : obj.created_at
|
|
return obj
|
|
end
|
|
|
|
##
|
|
#
|
|
# Import methods
|
|
#
|
|
##
|
|
|
|
#
|
|
# Generic importer that automatically determines the file type being
|
|
# imported. Since this looks for vendor-specific strings in the given
|
|
# file, there shouldn't be any false detections, but no guarantees.
|
|
#
|
|
def import_file(args={}, &block)
|
|
filename = args[:filename] || args['filename']
|
|
wspace = args[:wspace] || args['wspace'] || workspace
|
|
@import_filedata = {}
|
|
@import_filedata[:filename] = filename
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
if data[0,4] == "PK\x03\x04"
|
|
data = Zip::ZipFile.open(filename)
|
|
end
|
|
if block
|
|
import(args.merge(:data => data)) { |type,data| yield type,data }
|
|
else
|
|
import(args.merge(:data => data))
|
|
end
|
|
|
|
end
|
|
|
|
# A dispatcher method that figures out the data's file type,
|
|
# and sends it off to the appropriate importer. Note that
|
|
# import_file_detect will raise an error if the filetype
|
|
# is unknown.
|
|
def import(args={}, &block)
|
|
data = args[:data] || args['data']
|
|
wspace = args[:wspace] || args['wspace'] || workspace
|
|
unless data.kind_of? Zip::ZipFile
|
|
di = data.index("\n")
|
|
raise DBImportError.new("Could not automatically determine file type") if not di
|
|
end
|
|
ftype = import_filetype_detect(data)
|
|
yield(:filetype, @import_filedata[:type]) if block
|
|
|
|
self.send "import_#{ftype}".to_sym, args, &block
|
|
end
|
|
|
|
|
|
# Returns one of: :nexpose_simplexml :nexpose_rawxml :nmap_xml :openvas_xml
|
|
# :nessus_xml :nessus_xml_v2 :qualys_xml :msf_xml :nessus_nbe :amap_mlog
|
|
# :amap_log :ip_list, :msf_zip
|
|
# If there is no match, an error is raised instead.
|
|
def import_filetype_detect(data)
|
|
if data.kind_of? Zip::ZipFile
|
|
@import_filedata ||= {}
|
|
@import_filedata[:zip_filename] = File.split(data.to_s).last
|
|
@import_filedata[:zip_basename] = @import_filedata[:zip_filename].gsub(/\.zip$/,"")
|
|
@import_filedata[:zip_entry_names] = data.entries.map {|x| x.name}
|
|
@import_filedata[:zip_xml] = @import_filedata[:zip_entry_names].grep(/^(.*)_[0-9]+\.xml$/).first
|
|
@import_filedata[:zip_wspace] = $1
|
|
@import_filedata[:type] = "Metasploit ZIP Report"
|
|
return :msf_zip if @import_filedata[:zip_xml]
|
|
end
|
|
di = data.index("\n")
|
|
firstline = data[0, di]
|
|
@import_filedata ||= {}
|
|
if (firstline.index("<NeXposeSimpleXML"))
|
|
@import_filedata[:type] = "NeXpose Simple XML"
|
|
return :nexpose_simplexml
|
|
elsif (firstline.index("<NexposeReport"))
|
|
@import_filedata[:type] = "NeXpose XML Report"
|
|
return :nexpose_rawxml
|
|
elsif (firstline.index("<scanJob>"))
|
|
@import_filedata[:type] = "Retina XML"
|
|
return :retina_xml
|
|
elsif (firstline.index("<?xml"))
|
|
# it's xml, check for root tags we can handle
|
|
line_count = 0
|
|
data.each_line { |line|
|
|
line =~ /<([a-zA-Z0-9\-\_]+)[ >]/
|
|
case $1
|
|
when "nmaprun"
|
|
@import_filedata[:type] = "Nmap XML"
|
|
return :nmap_xml
|
|
when "openvas-report"
|
|
@import_filedata[:type] = "OpenVAS Report"
|
|
return :openvas_xml
|
|
when "NessusClientData"
|
|
@import_filedata[:type] = "Nessus XML (v1)"
|
|
return :nessus_xml
|
|
when "NessusClientData_v2"
|
|
@import_filedata[:type] = "Nessus XML (v2)"
|
|
return :nessus_xml_v2
|
|
when "SCAN"
|
|
@import_filedata[:type] = "Qualys XML"
|
|
return :qualys_xml
|
|
when /MetasploitExpressV[1234]/
|
|
@import_filedata[:type] = "Metasploit XML"
|
|
return :msf_xml
|
|
when /MetasploitV4/
|
|
@import_filedata[:type] = "Metasploit XML"
|
|
return :msf_xml
|
|
when /netsparker/
|
|
@import_filedata[:type] = "NetSparker XML"
|
|
return :netsparker_xml
|
|
else
|
|
# Give up if we haven't hit the root tag in the first few lines
|
|
break if line_count > 10
|
|
end
|
|
line_count += 1
|
|
}
|
|
elsif (firstline.index("timestamps|||scan_start"))
|
|
@import_filedata[:type] = "Nessus NBE Report"
|
|
# then it's a nessus nbe
|
|
return :nessus_nbe
|
|
elsif (firstline.index("# amap v"))
|
|
# then it's an amap mlog
|
|
@import_filedata[:type] = "Amap Log -m"
|
|
return :amap_mlog
|
|
elsif (firstline.index("amap v"))
|
|
# then it's an amap log
|
|
@import_filedata[:type] = "Amap Log"
|
|
return :amap_log
|
|
elsif (firstline =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)
|
|
# then its an IP list
|
|
@import_filedata[:type] = "IP Address List"
|
|
return :ip_list
|
|
elsif (data[0,1024].index("<netsparker"))
|
|
@import_filedata[:type] = "NetSparker XML"
|
|
return :netsparker_xml
|
|
end
|
|
|
|
raise DBImportError.new("Could not automatically determine file type")
|
|
end
|
|
|
|
# Boils down the validate_import_file to a boolean
|
|
def validate_import_file(data)
|
|
begin
|
|
import_filetype_detect(data)
|
|
rescue DBImportError
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
#
|
|
# Nexpose Simple XML
|
|
#
|
|
# XXX At some point we'll want to make this a stream parser for dealing
|
|
# with large results files
|
|
#
|
|
def import_nexpose_simplexml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_nexpose_simplexml(args.merge(:data => data))
|
|
end
|
|
|
|
# Import a Metasploit XML file.
|
|
def import_msf_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_msf_xml(args.merge(:data => data))
|
|
end
|
|
|
|
# Import a Metasploit Express ZIP file. Note that this requires
|
|
# a fair bit of filesystem manipulation, and is very much tied
|
|
# up with the Metasploit Express ZIP file format export (for
|
|
# obvious reasons). In the event directories exist, they will
|
|
# be reused. If target files exist, they will be overwritten.
|
|
#
|
|
# XXX: Refactor so it's not quite as sanity-blasting.
|
|
def import_msf_zip(args={}, &block)
|
|
data = args[:data]
|
|
wpsace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
new_tmp = ::File.join(Dir::tmpdir,"msf",@import_filedata[:zip_basename])
|
|
if ::File.exists? new_tmp
|
|
unless (::File.directory?(new_tmp) && ::File.writable?(new_tmp))
|
|
raise DBImportError.new("Could not extract zip file to #{new_tmp}")
|
|
end
|
|
else
|
|
FileUtils.mkdir_p(new_tmp)
|
|
end
|
|
@import_filedata[:zip_tmp] = new_tmp
|
|
|
|
@import_filedata[:zip_tmp_subdirs] = @import_filedata[:zip_entry_names].map {|x| ::File.split(x)}.map {|x| x[0]}.uniq.reject {|x| x == "."}
|
|
|
|
@import_filedata[:zip_tmp_subdirs].each {|sub|
|
|
tmp_subdirs = ::File.join(@import_filedata[:zip_tmp],sub)
|
|
if File.exists? tmp_subdirs
|
|
unless (::File.directory?(tmp_subdirs) && File.writable?(tmp_subdirs))
|
|
raise DBImportError.new("Could not extract zip file to #{tmp_subdirs}")
|
|
end
|
|
else
|
|
::FileUtils.mkdir(tmp_subdirs)
|
|
end
|
|
}
|
|
|
|
data.entries.each do |e|
|
|
target = ::File.join(@import_filedata[:zip_tmp],e.name)
|
|
::File.unlink target if ::File.exists?(target) # Yep. Deleted.
|
|
data.extract(e,target)
|
|
if target =~ /^.*.xml$/
|
|
@import_filedata[:zip_extracted_xml] = target
|
|
end
|
|
end
|
|
|
|
# This will kick the newly-extracted XML file through
|
|
# the import_file process all over again.
|
|
if @import_filedata[:zip_extracted_xml]
|
|
new_args = args.dup
|
|
new_args[:filename] = @import_filedata[:zip_extracted_xml]
|
|
new_args[:data] = nil
|
|
new_args[:ifd] = @import_filedata.dup
|
|
if block
|
|
import_file(new_args, &block)
|
|
else
|
|
import_file(new_args)
|
|
end
|
|
end
|
|
|
|
# Kick down to all the MSFX ZIP specific items
|
|
if block
|
|
import_msf_collateral(new_args, &block)
|
|
else
|
|
import_msf_collateral(new_args)
|
|
end
|
|
end
|
|
|
|
# Imports loot, tasks, and reports from an MSF ZIP report.
|
|
# XXX: This function is stupidly long. It needs to be refactored.
|
|
def import_msf_collateral(args={}, &block)
|
|
data = ::File.open(args[:filename], "rb") {|f| f.read(f.stat.size)}
|
|
wspace = args[:wspace] || args['wspace'] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
basedir = args[:basedir] || args['basedir'] || ::File.join(Msf::Config.install_root, "data", "msf")
|
|
|
|
allow_yaml = false
|
|
btag = nil
|
|
|
|
doc = rexmlify(data)
|
|
if doc.elements["MetasploitExpressV1"]
|
|
m_ver = 1
|
|
allow_yaml = true
|
|
btag = "MetasploitExpressV1"
|
|
elsif doc.elements["MetasploitExpressV2"]
|
|
m_ver = 2
|
|
allow_yaml = true
|
|
btag = "MetasploitExpressV2"
|
|
elsif doc.elements["MetasploitExpressV3"]
|
|
m_ver = 3
|
|
btag = "MetasploitExpressV3"
|
|
elsif doc.elements["MetasploitExpressV4"]
|
|
m_ver = 4
|
|
btag = "MetasploitExpressV4"
|
|
elsif doc.elements["MetasploitV4"]
|
|
m_ver = 4
|
|
btag = "MetasploitV4"
|
|
else
|
|
m_ver = nil
|
|
end
|
|
unless m_ver and btag
|
|
raise DBImportError.new("Unsupported Metasploit XML document format")
|
|
end
|
|
|
|
host_info = {}
|
|
doc.elements.each("/#{btag}/hosts/host") do |host|
|
|
host_info[host.elements["id"].text.to_s.strip] = nils_for_nulls(host.elements["address"].text.to_s.strip)
|
|
end
|
|
|
|
# Import Loot
|
|
doc.elements.each("/#{btag}/loots/loot") do |loot|
|
|
next if bl.include? host_info[loot.elements["host-id"].text.to_s.strip]
|
|
loot_info = {}
|
|
loot_info[:host] = host_info[loot.elements["host-id"].text.to_s.strip]
|
|
loot_info[:workspace] = args[:wspace]
|
|
loot_info[:ctype] = nils_for_nulls(loot.elements["content-type"].text.to_s.strip)
|
|
loot_info[:info] = nils_for_nulls(unserialize_object(loot.elements["info"], allow_yaml))
|
|
loot_info[:ltype] = nils_for_nulls(loot.elements["ltype"].text.to_s.strip)
|
|
loot_info[:name] = nils_for_nulls(loot.elements["name"].text.to_s.strip)
|
|
loot_info[:created_at] = nils_for_nulls(loot.elements["created-at"].text.to_s.strip)
|
|
loot_info[:updated_at] = nils_for_nulls(loot.elements["updated-at"].text.to_s.strip)
|
|
loot_info[:name] = nils_for_nulls(loot.elements["name"].text.to_s.strip)
|
|
loot_info[:orig_path] = nils_for_nulls(loot.elements["path"].text.to_s.strip)
|
|
tmp = args[:ifd][:zip_tmp]
|
|
loot_info[:orig_path].gsub!(/^\./,tmp) if loot_info[:orig_path]
|
|
if !loot.elements["service-id"].text.to_s.strip.empty?
|
|
unless loot.elements["service-id"].text.to_s.strip == "NULL"
|
|
loot_info[:service] = loot.elements["service-id"].text.to_s.strip
|
|
end
|
|
end
|
|
|
|
# Only report loot if we actually have it.
|
|
# TODO: Copypasta. Seperate this out.
|
|
if ::File.exists? loot_info[:orig_path]
|
|
loot_dir = ::File.join(basedir,"loot")
|
|
loot_file = ::File.split(loot_info[:orig_path]).last
|
|
if ::File.exists? loot_dir
|
|
unless (::File.directory?(loot_dir) && ::File.writable?(loot_dir))
|
|
raise DBImportError.new("Could not move files to #{loot_dir}")
|
|
end
|
|
else
|
|
::FileUtils.mkdir_p(loot_dir)
|
|
end
|
|
new_loot = ::File.join(loot_dir,loot_file)
|
|
loot_info[:path] = new_loot
|
|
if ::File.exists?(new_loot)
|
|
::File.unlink new_loot # Delete it, and don't report it.
|
|
else
|
|
report_loot(loot_info) # It's new, so report it.
|
|
end
|
|
::FileUtils.copy(loot_info[:orig_path], new_loot)
|
|
yield(:msf_loot, new_loot) if block
|
|
end
|
|
end
|
|
|
|
# Import Tasks
|
|
doc.elements.each("/#{btag}/tasks/task") do |task|
|
|
task_info = {}
|
|
task_info[:workspace] = args[:wspace]
|
|
# Should user be imported (original) or declared (the importing user)?
|
|
task_info[:user] = nils_for_nulls(task.elements["created-by"].text.to_s.strip)
|
|
task_info[:desc] = nils_for_nulls(task.elements["description"].text.to_s.strip)
|
|
task_info[:info] = nils_for_nulls(unserialize_object(task.elements["info"], allow_yaml))
|
|
task_info[:mod] = nils_for_nulls(task.elements["module"].text.to_s.strip)
|
|
task_info[:options] = nils_for_nulls(task.elements["options"].text.to_s.strip)
|
|
task_info[:prog] = nils_for_nulls(task.elements["progress"].text.to_s.strip).to_i
|
|
task_info[:created_at] = nils_for_nulls(task.elements["created-at"].text.to_s.strip)
|
|
task_info[:updated_at] = nils_for_nulls(task.elements["updated-at"].text.to_s.strip)
|
|
if !task.elements["completed-at"].text.to_s.empty?
|
|
task_info[:completed_at] = nils_for_nulls(task.elements["completed-at"].text.to_s.strip)
|
|
end
|
|
if !task.elements["error"].text.to_s.empty?
|
|
task_info[:error] = nils_for_nulls(task.elements["error"].text.to_s.strip)
|
|
end
|
|
if !task.elements["result"].text.to_s.empty?
|
|
task_info[:result] = nils_for_nulls(task.elements["result"].text.to_s.strip)
|
|
end
|
|
task_info[:orig_path] = nils_for_nulls(task.elements["path"].text.to_s.strip)
|
|
tmp = args[:ifd][:zip_tmp]
|
|
task_info[:orig_path].gsub!(/^\./,tmp) if task_info[:orig_path]
|
|
|
|
# Only report a task if we actually have it.
|
|
# TODO: Copypasta. Seperate this out.
|
|
if ::File.exists? task_info[:orig_path]
|
|
tasks_dir = ::File.join(basedir,"tasks")
|
|
task_file = ::File.split(task_info[:orig_path]).last
|
|
if ::File.exists? tasks_dir
|
|
unless (::File.directory?(tasks_dir) && ::File.writable?(tasks_dir))
|
|
raise DBImportError.new("Could not move files to #{tasks_dir}")
|
|
end
|
|
else
|
|
::FileUtils.mkdir_p(tasks_dir)
|
|
end
|
|
new_task = ::File.join(tasks_dir,task_file)
|
|
task_info[:path] = new_task
|
|
if ::File.exists?(new_task)
|
|
::File.unlink new_task # Delete it, and don't report it.
|
|
else
|
|
report_task(task_info) # It's new, so report it.
|
|
end
|
|
::FileUtils.copy(task_info[:orig_path], new_task)
|
|
yield(:msf_task, new_task) if block
|
|
end
|
|
end
|
|
|
|
# Import Reports
|
|
doc.elements.each("/#{btag}/reports/report") do |report|
|
|
report_info = {}
|
|
report_info[:workspace] = args[:wspace]
|
|
# Should user be imported (original) or declared (the importing user)?
|
|
report_info[:user] = nils_for_nulls(report.elements["created-by"].text.to_s.strip)
|
|
report_info[:options] = nils_for_nulls(report.elements["options"].text.to_s.strip)
|
|
report_info[:rtype] = nils_for_nulls(report.elements["rtype"].text.to_s.strip)
|
|
report_info[:created_at] = nils_for_nulls(report.elements["created-at"].text.to_s.strip)
|
|
report_info[:updated_at] = nils_for_nulls(report.elements["updated-at"].text.to_s.strip)
|
|
|
|
report_info[:orig_path] = nils_for_nulls(report.elements["path"].text.to_s.strip)
|
|
tmp = args[:ifd][:zip_tmp]
|
|
report_info[:orig_path].gsub!(/^\./,tmp) if report_info[:orig_path]
|
|
|
|
# Only report a report if we actually have it.
|
|
# TODO: Copypasta. Seperate this out.
|
|
if ::File.exists? report_info[:orig_path]
|
|
reports_dir = ::File.join(basedir,"reports")
|
|
report_file = ::File.split(report_info[:orig_path]).last
|
|
if ::File.exists? reports_dir
|
|
unless (::File.directory?(reports_dir) && ::File.writable?(reports_dir))
|
|
raise DBImportError.new("Could not move files to #{reports_dir}")
|
|
end
|
|
else
|
|
::FileUtils.mkdir_p(reports_dir)
|
|
end
|
|
new_report = ::File.join(reports_dir,report_file)
|
|
report_info[:path] = new_report
|
|
if ::File.exists?(new_report)
|
|
::File.unlink new_report
|
|
else
|
|
report_report(report_info)
|
|
end
|
|
::FileUtils.copy(report_info[:orig_path], new_report)
|
|
yield(:msf_report, new_report) if block
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
# For each host, step through services, notes, and vulns, and import
|
|
# them.
|
|
# TODO: loot, tasks, and reports
|
|
def import_msf_xml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
allow_yaml = false
|
|
btag = nil
|
|
|
|
doc = rexmlify(data)
|
|
if doc.elements["MetasploitExpressV1"]
|
|
m_ver = 1
|
|
allow_yaml = true
|
|
btag = "MetasploitExpressV1"
|
|
elsif doc.elements["MetasploitExpressV2"]
|
|
m_ver = 2
|
|
allow_yaml = true
|
|
btag = "MetasploitExpressV2"
|
|
elsif doc.elements["MetasploitExpressV3"]
|
|
m_ver = 3
|
|
btag = "MetasploitExpressV3"
|
|
elsif doc.elements["MetasploitExpressV4"]
|
|
m_ver = 4
|
|
btag = "MetasploitExpressV4"
|
|
elsif doc.elements["MetasploitV4"]
|
|
m_ver = 4
|
|
btag = "MetasploitV4"
|
|
else
|
|
m_ver = nil
|
|
end
|
|
unless m_ver and btag
|
|
raise DBImportError.new("Unsupported Metasploit XML document format")
|
|
end
|
|
|
|
doc.elements.each("/#{btag}/hosts/host") do |host|
|
|
host_data = {}
|
|
host_data[:workspace] = wspace
|
|
host_data[:host] = nils_for_nulls(host.elements["address"].text.to_s.strip)
|
|
if bl.include? host_data[:host]
|
|
next
|
|
else
|
|
yield(:address,host_data[:host]) if block
|
|
end
|
|
host_data[:host_mac] = nils_for_nulls(host.elements["mac"].text.to_s.strip)
|
|
if host.elements["comm"].text
|
|
host_data[:comm] = nils_for_nulls(host.elements["comm"].text.to_s.strip)
|
|
end
|
|
%W{created-at updated-at name state os-flavor os-lang os-name os-sp purpose}.each { |datum|
|
|
if host.elements[datum].text
|
|
host_data[datum.gsub('-','_')] = nils_for_nulls(host.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
host_address = host_data[:host].dup # Preserve after report_host() deletes
|
|
report_host(host_data)
|
|
host.elements.each('services/service') do |service|
|
|
service_data = {}
|
|
service_data[:workspace] = wspace
|
|
service_data[:host] = host_address
|
|
service_data[:port] = nils_for_nulls(service.elements["port"].text.to_s.strip).to_i
|
|
service_data[:proto] = nils_for_nulls(service.elements["proto"].text.to_s.strip)
|
|
%W{created-at updated-at name state info}.each { |datum|
|
|
if service.elements[datum].text
|
|
if datum == "info"
|
|
service_data["info"] = nils_for_nulls(unserialize_object(service.elements[datum], false))
|
|
else
|
|
service_data[datum.gsub("-","_")] = nils_for_nulls(service.elements[datum].text.to_s.strip)
|
|
end
|
|
end
|
|
}
|
|
report_service(service_data)
|
|
end
|
|
host.elements.each('notes/note') do |note|
|
|
note_data = {}
|
|
note_data[:workspace] = wspace
|
|
note_data[:host] = host_address
|
|
note_data[:type] = nils_for_nulls(note.elements["ntype"].text.to_s.strip)
|
|
note_data[:data] = nils_for_nulls(unserialize_object(note.elements["data"], allow_yaml))
|
|
|
|
if note.elements["critical"].text
|
|
note_data[:critical] = true unless note.elements["critical"].text.to_s.strip == "NULL"
|
|
end
|
|
if note.elements["seen"].text
|
|
note_data[:seen] = true unless note.elements["critical"].text.to_s.strip == "NULL"
|
|
end
|
|
%W{created-at updated-at}.each { |datum|
|
|
if note.elements[datum].text
|
|
note_data[datum.gsub("-","_")] = nils_for_nulls(note.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
report_note(note_data)
|
|
end
|
|
host.elements.each('vulns/vuln') do |vuln|
|
|
vuln_data = {}
|
|
vuln_data[:workspace] = wspace
|
|
vuln_data[:host] = host_address
|
|
vuln_data[:info] = nils_for_nulls(unserialize_object(vuln.elements["info"], allow_yaml))
|
|
vuln_data[:name] = nils_for_nulls(vuln.elements["name"].text.to_s.strip)
|
|
%W{created-at updated-at}.each { |datum|
|
|
if vuln.elements[datum].text
|
|
vuln_data[datum.gsub("-","_")] = nils_for_nulls(vuln.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
report_vuln(vuln_data)
|
|
end
|
|
host.elements.each('creds/cred') do |cred|
|
|
cred_data = {}
|
|
cred_data[:workspace] = wspace
|
|
cred_data[:host] = host_address
|
|
%W{port ptype sname proto proof active user pass}.each {|datum|
|
|
if cred.elements[datum].respond_to? :text
|
|
cred_data[datum.intern] = nils_for_nulls(cred.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
%W{created-at updated-at}.each { |datum|
|
|
if cred.elements[datum].respond_to? :text
|
|
cred_data[datum.gsub("-","_")] = nils_for_nulls(cred.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
if cred_data[:pass] == "<masked>"
|
|
cred_data[:pass] = ""
|
|
cred_data[:active] = false
|
|
elsif cred_data[:pass] == "*BLANK PASSWORD*"
|
|
cred_data[:pass] = ""
|
|
end
|
|
report_cred(cred_data.merge(:wait => true))
|
|
end
|
|
end
|
|
|
|
# Import web sites
|
|
doc.elements.each("/#{btag}/web_sites/web_site") do |web|
|
|
info = {}
|
|
info[:workspace] = wspace
|
|
|
|
%W{host port vhost ssl comments}.each do |datum|
|
|
if web.elements[datum].respond_to? :text
|
|
info[datum.intern] = nils_for_nulls(web.elements[datum].text.to_s.strip)
|
|
end
|
|
end
|
|
|
|
info[:options] = nils_for_nulls(unserialize_object(web.elements["options"], allow_yaml)) if web.elements["options"].respond_to?(:text)
|
|
info[:ssl] = (info[:ssl] and info[:ssl].to_s.strip.downcase == "true") ? true : false
|
|
|
|
%W{created-at updated-at}.each { |datum|
|
|
if web.elements[datum].text
|
|
info[datum.gsub("-","_")] = nils_for_nulls(web.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
|
|
report_web_site(info)
|
|
end
|
|
|
|
%W{page form vuln}.each do |wtype|
|
|
doc.elements.each("/#{btag}/web_#{wtype}s/web_#{wtype}") do |web|
|
|
info = {}
|
|
info[:workspace] = wspace
|
|
info[:host] = nils_for_nulls(web.elements["host"].text.to_s.strip) if web.elements["host"].respond_to?(:text)
|
|
info[:port] = nils_for_nulls(web.elements["port"].text.to_s.strip) if web.elements["port"].respond_to?(:text)
|
|
info[:ssl] = nils_for_nulls(web.elements["ssl"].text.to_s.strip) if web.elements["ssl"].respond_to?(:text)
|
|
info[:vhost] = nils_for_nulls(web.elements["vhost"].text.to_s.strip) if web.elements["vhost"].respond_to?(:text)
|
|
|
|
info[:ssl] = (info[:ssl] and info[:ssl].to_s.strip.downcase == "true") ? true : false
|
|
|
|
case wtype
|
|
when "page"
|
|
%W{path code body query cookie auth ctype mtime location}.each do |datum|
|
|
if web.elements[datum].respond_to? :text
|
|
info[datum.intern] = nils_for_nulls(web.elements[datum].text.to_s.strip)
|
|
end
|
|
end
|
|
info[:headers] = nils_for_nulls(unserialize_object(web.elements["headers"], allow_yaml))
|
|
when "form"
|
|
%W{path query method}.each do |datum|
|
|
if web.elements[datum].respond_to? :text
|
|
info[datum.intern] = nils_for_nulls(web.elements[datum].text.to_s.strip)
|
|
end
|
|
end
|
|
info[:params] = nils_for_nulls(unserialize_object(web.elements["params"], allow_yaml))
|
|
when "vuln"
|
|
%W{path query method pname proof risk name blame description category confidence}.each do |datum|
|
|
if web.elements[datum].respond_to? :text
|
|
info[datum.intern] = nils_for_nulls(web.elements[datum].text.to_s.strip)
|
|
end
|
|
end
|
|
info[:params] = nils_for_nulls(unserialize_object(web.elements["params"], allow_yaml))
|
|
info[:risk] = info[:risk].to_i
|
|
info[:confidence] = info[:confidence].to_i
|
|
end
|
|
|
|
%W{created-at updated-at}.each { |datum|
|
|
if web.elements[datum].text
|
|
info[datum.gsub("-","_")] = nils_for_nulls(web.elements[datum].text.to_s.strip)
|
|
end
|
|
}
|
|
self.send("report_web_#{wtype}", info)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Convert the string "NULL" to actual nil
|
|
def nils_for_nulls(str)
|
|
str == "NULL" ? nil : str
|
|
end
|
|
|
|
def import_nexpose_simplexml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
doc = rexmlify(data)
|
|
doc.elements.each('/NeXposeSimpleXML/devices/device') do |dev|
|
|
addr = dev.attributes['address'].to_s
|
|
if bl.include? addr
|
|
next
|
|
else
|
|
yield(:address,addr) if block
|
|
end
|
|
|
|
fprint = {}
|
|
|
|
dev.elements.each('fingerprint/description') do |str|
|
|
fprint[:desc] = str.text.to_s.strip
|
|
end
|
|
dev.elements.each('fingerprint/vendor') do |str|
|
|
fprint[:vendor] = str.text.to_s.strip
|
|
end
|
|
dev.elements.each('fingerprint/family') do |str|
|
|
fprint[:family] = str.text.to_s.strip
|
|
end
|
|
dev.elements.each('fingerprint/product') do |str|
|
|
fprint[:product] = str.text.to_s.strip
|
|
end
|
|
dev.elements.each('fingerprint/version') do |str|
|
|
fprint[:version] = str.text.to_s.strip
|
|
end
|
|
dev.elements.each('fingerprint/architecture') do |str|
|
|
fprint[:arch] = str.text.to_s.upcase.strip
|
|
end
|
|
|
|
conf = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:state => Msf::HostState::Alive
|
|
}
|
|
|
|
report_host(conf)
|
|
|
|
report_note(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.nexpose_fingerprint',
|
|
:data => fprint
|
|
)
|
|
|
|
# Load vulnerabilities not associated with a service
|
|
dev.elements.each('vulnerabilities/vulnerability') do |vuln|
|
|
vid = vuln.attributes['id'].to_s.downcase
|
|
refs = process_nexpose_data_sxml_refs(vuln)
|
|
next if not refs
|
|
report_vuln(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:name => 'NEXPOSE-' + vid,
|
|
:info => vid,
|
|
:refs => refs)
|
|
end
|
|
|
|
# Load the services
|
|
dev.elements.each('services/service') do |svc|
|
|
sname = svc.attributes['name'].to_s
|
|
sprot = svc.attributes['protocol'].to_s.downcase
|
|
sport = svc.attributes['port'].to_s.to_i
|
|
next if sport == 0
|
|
|
|
name = sname.split('(')[0].strip
|
|
info = ''
|
|
|
|
svc.elements.each('fingerprint/description') do |str|
|
|
info = str.text.to_s.strip
|
|
end
|
|
|
|
if(sname.downcase != '<unknown>')
|
|
report_service(:workspace => wspace, :host => addr, :proto => sprot, :port => sport, :name => name, :info => info)
|
|
else
|
|
report_service(:workspace => wspace, :host => addr, :proto => sprot, :port => sport, :info => info)
|
|
end
|
|
|
|
# Load vulnerabilities associated with this service
|
|
svc.elements.each('vulnerabilities/vulnerability') do |vuln|
|
|
vid = vuln.attributes['id'].to_s.downcase
|
|
refs = process_nexpose_data_sxml_refs(vuln)
|
|
next if not refs
|
|
report_vuln(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:port => sport,
|
|
:proto => sprot,
|
|
:name => 'NEXPOSE-' + vid,
|
|
:info => vid,
|
|
:refs => refs)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
#
|
|
# Nexpose Raw XML
|
|
#
|
|
def import_nexpose_rawxml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_nexpose_rawxml(args.merge(:data => data))
|
|
end
|
|
|
|
def import_nexpose_rawxml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
# 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
|
|
|
|
# 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|
|
|
if bl.include? host["addr"]
|
|
next
|
|
else
|
|
yield(:address,host["addr"]) if block
|
|
#
|
|
end
|
|
nexpose_host(host, vuln_refs, wspace)
|
|
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)
|
|
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"])
|
|
# Put colons between each octet of the MAC address
|
|
data[:mac] = h["hardware-address"].gsub(':', '').scan(/../).join(':')
|
|
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
|
|
|
|
if (data[:state] != Msf::HostState::Dead)
|
|
report_host(data)
|
|
end
|
|
|
|
if h["os_family"]
|
|
note = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.nexpose_fingerprint',
|
|
:data => {
|
|
:family => h["os_family"],
|
|
:certainty => h["os_certainty"]
|
|
}
|
|
}
|
|
note[:data][:vendor] = h["os_vendor"] if h["os_vendor"]
|
|
note[:data][:product] = h["os_product"] if h["os_product"]
|
|
note[:data][:arch] = h["arch"] if h["arch"]
|
|
|
|
report_note(note)
|
|
end
|
|
|
|
h["endpoints"].each { |p|
|
|
extra = ""
|
|
extra << p["product"] + " " if p["product"]
|
|
extra << p["version"] + " " if p["version"]
|
|
|
|
# Skip port-0 endpoints
|
|
next if p["port"].to_i == 0
|
|
|
|
# XXX This should probably be handled in a more standard way
|
|
# 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[:proto] = v["protocol"].downcase if v["protocol"]
|
|
data[:port] = v["port"].to_i if v["port"]
|
|
data[:name] = "NEXPOSE-" + v["id"]
|
|
data[:refs] = vuln_refs[v["id"]]
|
|
report_vuln(data)
|
|
}
|
|
end
|
|
|
|
|
|
#
|
|
# Retina XML
|
|
#
|
|
|
|
# Process a Retina XML file
|
|
def import_retina_xml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_retina_xml(args.merge(:data => data))
|
|
end
|
|
|
|
# Process Retina XML
|
|
def import_retina_xml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
parser = Rex::Parser::RetinaXMLStreamParser.new
|
|
parser.on_found_host = Proc.new do |host|
|
|
data = {:workspace => wspace}
|
|
addr = host['address']
|
|
next if not addr
|
|
|
|
next if bl.include? addr
|
|
data[:host] = addr
|
|
|
|
if host['mac']
|
|
data[:mac] = host['mac']
|
|
end
|
|
|
|
data[:state] = Msf::HostState::Alive
|
|
|
|
if host['hostname']
|
|
data[:name] = host['hostname']
|
|
end
|
|
|
|
if host['netbios']
|
|
data[:name] = host['netbios']
|
|
end
|
|
|
|
yield(:address, data[:host]) if block
|
|
|
|
# Import Host
|
|
report_host(data)
|
|
report_import_note(wspace, addr)
|
|
|
|
# Import OS fingerprint
|
|
if host["os"]
|
|
note = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.retina_fingerprint',
|
|
:data => {
|
|
:os => host["os"]
|
|
}
|
|
}
|
|
report_note(note)
|
|
end
|
|
|
|
# Import vulnerabilities
|
|
host['vulns'].each do |vuln|
|
|
refs = vuln['refs'].map{|v| v.join("-")}
|
|
refs << "RETINA-#{vuln['rthid']}" if vuln['rthid']
|
|
|
|
vuln_info = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:name => vuln['name'],
|
|
:info => vuln['description'],
|
|
:refs => refs
|
|
}
|
|
|
|
report_vuln(vuln_info)
|
|
end
|
|
end
|
|
|
|
REXML::Document.parse_stream(data, parser)
|
|
end
|
|
|
|
#
|
|
# NetSparker XML
|
|
#
|
|
|
|
# Process a NetSparker XML file
|
|
def import_netsparker_xml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_netsparker_xml(args.merge(:data => data))
|
|
end
|
|
|
|
# Process NetSparker XML
|
|
def import_netsparker_xml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
addr = nil
|
|
parser = Rex::Parser::NetSparkerXMLStreamParser.new
|
|
parser.on_found_vuln = Proc.new do |vuln|
|
|
data = {:workspace => wspace}
|
|
|
|
# Parse the URL
|
|
url = vuln['url']
|
|
return if not url
|
|
|
|
# Crack the URL into a URI
|
|
uri = URI(url) rescue nil
|
|
return if not uri
|
|
|
|
# Resolve the host and cache the IP
|
|
if not addr
|
|
baddr = Rex::Socket.addr_aton(uri.host) rescue nil
|
|
if baddr
|
|
addr = Rex::Socket.addr_ntoa(baddr)
|
|
yield(:address, data[:host]) if block
|
|
end
|
|
end
|
|
|
|
# Bail early if we have no IP address
|
|
if not addr
|
|
raise Interrupt, "Not a valid IP address"
|
|
end
|
|
|
|
if bl.include?(addr)
|
|
raise Interrupt, "IP address is on the blacklist"
|
|
end
|
|
|
|
data[:host] = addr
|
|
data[:vhost] = uri.host
|
|
data[:port] = uri.port
|
|
data[:ssl] = (uri.scheme == "ssl")
|
|
|
|
body = nil
|
|
# First report a web page
|
|
if vuln['response']
|
|
headers = {}
|
|
code = 200
|
|
head,body = vuln['response'].to_s.split(/\r?\n\r?\n/, 2)
|
|
if body
|
|
|
|
if head =~ /^HTTP\d+\.\d+\s+(\d+)\s*/
|
|
code = $1.to_i
|
|
end
|
|
|
|
headers = {}
|
|
head.split(/\r?\n/).each do |line|
|
|
hname,hval = line.strip.split(/\s*:\s*/, 2)
|
|
next if hval.to_s.strip.empty?
|
|
headers[hname.downcase] ||= []
|
|
headers[hname.downcase] << hval
|
|
end
|
|
|
|
info = {
|
|
:path => uri.path,
|
|
:query => uri.query,
|
|
:code => code,
|
|
:body => body,
|
|
:headers => headers
|
|
}
|
|
info.merge!(data)
|
|
|
|
if headers['content-type']
|
|
info[:ctype] = headers['content-type'][0]
|
|
end
|
|
|
|
if headers['set-cookie']
|
|
info[:cookie] = headers['set-cookie'].join("\n")
|
|
end
|
|
|
|
if headers['authorization']
|
|
info[:auth] = headers['authorization'].join("\n")
|
|
end
|
|
|
|
if headers['location']
|
|
info[:location] = headers['location'][0]
|
|
end
|
|
|
|
if headers['last-modified']
|
|
info[:mtime] = headers['last-modified'][0]
|
|
end
|
|
|
|
# Report the web page to the database
|
|
report_web_page(info)
|
|
|
|
yield(:web_page, url) if block
|
|
end
|
|
end # End web_page reporting
|
|
|
|
|
|
details = netsparker_vulnerability_map(vuln)
|
|
|
|
method = netsparker_method_map(vuln)
|
|
pname = netsparker_pname_map(vuln)
|
|
params = netsparker_params_map(vuln)
|
|
|
|
proof = ''
|
|
|
|
if vuln['info'] and vuln['info'].length > 0
|
|
proof << vuln['info'].map{|x| "#{x[0]}: #{x[1]}\n" }.join + "\n"
|
|
end
|
|
|
|
if proof.empty?
|
|
if body
|
|
proof << body + "\n"
|
|
else
|
|
proof << vuln['response'].to_s + "\n"
|
|
end
|
|
end
|
|
|
|
if params.empty? and pname
|
|
params = [[pname, vuln['vparam_name'].to_s]]
|
|
end
|
|
|
|
info = {
|
|
:path => uri.path,
|
|
:query => uri.query,
|
|
:method => method,
|
|
:params => params,
|
|
:pname => pname.to_s,
|
|
:proof => proof,
|
|
:risk => details[:risk],
|
|
:name => details[:name],
|
|
:blame => details[:blame],
|
|
:category => details[:category],
|
|
:description => details[:description],
|
|
:confidence => details[:confidence],
|
|
}
|
|
info.merge!(data)
|
|
|
|
next if vuln['type'].to_s.empty?
|
|
|
|
report_web_vuln(info)
|
|
yield(:web_vuln, url) if block
|
|
end
|
|
|
|
# We throw interrupts in our parser when the job is hopeless
|
|
begin
|
|
REXML::Document.parse_stream(data, parser)
|
|
rescue ::Interrupt => e
|
|
wlog("The netsparker_xml_import() job was interrupted: #{e}")
|
|
end
|
|
end
|
|
|
|
def netsparker_method_map(vuln)
|
|
case vuln['vparam_type']
|
|
when "FullQueryString"
|
|
"GET"
|
|
when "Querystring"
|
|
"GET"
|
|
when "Post"
|
|
"POST"
|
|
when "RawUrlInjection"
|
|
"GET"
|
|
else
|
|
"GET"
|
|
end
|
|
end
|
|
|
|
def netsparker_pname_map(vuln)
|
|
case vuln['vparam_name']
|
|
when "URI-BASED", "Query Based"
|
|
"PATH"
|
|
else
|
|
vuln['vparam_name']
|
|
end
|
|
end
|
|
|
|
def netsparker_params_map(vuln)
|
|
[]
|
|
end
|
|
|
|
def netsparker_vulnerability_map(vuln)
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Information Disclosure',
|
|
:blame => 'System Administrator',
|
|
:category => 'info',
|
|
:description => "This is an information leak",
|
|
:confidence => 100
|
|
}
|
|
|
|
# Risk is a value from 1-5 indicating the severity of the issue
|
|
# Examples: 1, 4, 5
|
|
|
|
# Name is a descriptive name for this vulnerability.
|
|
# Examples: XSS, ReflectiveXSS, PersistentXSS
|
|
|
|
# Blame indicates who is at fault for the vulnerability
|
|
# Examples: App Developer, Server Developer, System Administrator
|
|
|
|
# Category indicates the general class of vulnerability
|
|
# Examples: info, xss, sql, rfi, lfi, cmd
|
|
|
|
# Description is a textual summary of the vulnerability
|
|
# Examples: "A reflective cross-site scripting attack"
|
|
# "The web server leaks the internal IP address"
|
|
# "The cookie is not set to HTTP-only"
|
|
|
|
#
|
|
# Confidence is a value from 1 to 100 indicating how confident the
|
|
# software is that the results are valid.
|
|
# Examples: 100, 90, 75, 15, 10, 0
|
|
|
|
case vuln['type'].to_s
|
|
when "ApacheDirectoryListing"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Directory Listing',
|
|
:blame => 'System Administrator',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "ApacheMultiViewsEnabled"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Apache MultiViews Enabled',
|
|
:blame => 'System Administrator',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "ApacheVersion"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Web Server Version',
|
|
:blame => 'System Administrator',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "PHPVersion"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'PHP Module Version',
|
|
:blame => 'System Administrator',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "AutoCompleteEnabled"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Form AutoComplete Enabled',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "CookieNotMarkedAsHttpOnly"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Cookie Not HttpOnly',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "EmailDisclosure"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Email Address Disclosure',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "ForbiddenResource"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Forbidden Resource',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "FileUploadFound"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'File Upload Form',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "PasswordOverHTTP"
|
|
res = {
|
|
:risk => 2,
|
|
:name => 'Password Over HTTP',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "MySQL5Identified"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'MySQL 5 Identified',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "PossibleInternalWindowsPathLeakage"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Path Leakage - Windows',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "PossibleInternalUnixPathLeakage"
|
|
res = {
|
|
:risk => 1,
|
|
:name => 'Path Leakage - Unix',
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => 100
|
|
}
|
|
when "PossibleXSS", "LowPossibilityPermanentXSS", "XSS", "PermanentXSS"
|
|
conf = 100
|
|
conf = 25 if vuln['type'].to_s == "LowPossibilityPermanentXSS"
|
|
conf = 50 if vuln['type'].to_s == "PossibleXSS"
|
|
res = {
|
|
:risk => 3,
|
|
:name => 'Cross-Site Scripting',
|
|
:blame => 'App Developer',
|
|
:category => 'xss',
|
|
:description => "",
|
|
:confidence => conf
|
|
}
|
|
|
|
when "ConfirmedBlindSQLInjection", "ConfirmedSQLInjection", "HighlyPossibleSqlInjection", "DatabaseErrorMessages"
|
|
conf = 100
|
|
conf = 90 if vuln['type'].to_s == "HighlyPossibleSqlInjection"
|
|
conf = 25 if vuln['type'].to_s == "DatabaseErrorMessages"
|
|
res = {
|
|
:risk => 5,
|
|
:name => 'SQL Injection',
|
|
:blame => 'App Developer',
|
|
:category => 'sql',
|
|
:description => "",
|
|
:confidence => conf
|
|
}
|
|
else
|
|
conf = 100
|
|
res = {
|
|
:risk => 1,
|
|
:name => vuln['type'].to_s,
|
|
:blame => 'App Developer',
|
|
:category => 'info',
|
|
:description => "",
|
|
:confidence => conf
|
|
}
|
|
end
|
|
|
|
res
|
|
end
|
|
|
|
|
|
#
|
|
# Import Nmap's -oX xml output
|
|
#
|
|
def import_nmap_xml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_nmap_xml(args.merge(:data => data))
|
|
end
|
|
|
|
# Too many functions in one def! Refactor this.
|
|
def import_nmap_xml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
fix_services = args[:fix_services]
|
|
|
|
# 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::NmapXMLStreamParser.new
|
|
|
|
# Whenever the parser pulls a host out of the nmap results, store
|
|
# it, along with any associated services, in the database.
|
|
parser.on_found_host = Proc.new { |h|
|
|
data = {:workspace => wspace}
|
|
if (h["addrs"].has_key?("ipv4"))
|
|
addr = h["addrs"]["ipv4"]
|
|
elsif (h["addrs"].has_key?("ipv6"))
|
|
addr = h["addrs"]["ipv6"]
|
|
else
|
|
# Can't report it if it doesn't have an IP
|
|
raise RuntimeError, "At least one IPv4 or IPv6 address is required"
|
|
end
|
|
next if bl.include? addr
|
|
data[:host] = addr
|
|
if (h["addrs"].has_key?("mac"))
|
|
data[:mac] = h["addrs"]["mac"]
|
|
end
|
|
data[:state] = (h["status"] == "up") ? Msf::HostState::Alive : Msf::HostState::Dead
|
|
|
|
if ( h["reverse_dns"] )
|
|
data[:name] = h["reverse_dns"]
|
|
end
|
|
|
|
# Only report alive hosts with ports to speak of.
|
|
if(data[:state] != Msf::HostState::Dead)
|
|
if h["ports"].size > 0
|
|
if fix_services
|
|
port_states = h["ports"].map {|p| p["state"]}.reject {|p| p == "filtered"}
|
|
next if port_states.compact.empty?
|
|
end
|
|
yield(:address,data[:host]) if block
|
|
report_host(data)
|
|
report_import_note(wspace,addr)
|
|
end
|
|
end
|
|
|
|
if( h["os_vendor"] )
|
|
note = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.nmap_fingerprint',
|
|
:data => {
|
|
:os_vendor => h["os_vendor"],
|
|
:os_family => h["os_family"],
|
|
:os_version => h["os_version"],
|
|
:os_accuracy => h["os_accuracy"]
|
|
}
|
|
}
|
|
|
|
if(h["os_match"])
|
|
note[:data][:os_match] = h['os_match']
|
|
end
|
|
|
|
report_note(note)
|
|
end
|
|
|
|
if (h["last_boot"])
|
|
report_note(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.last_boot',
|
|
:data => {
|
|
:time => h["last_boot"]
|
|
}
|
|
)
|
|
end
|
|
|
|
|
|
# Put all the ports, regardless of state, into the db.
|
|
h["ports"].each { |p|
|
|
# Localhost port results are pretty unreliable -- if it's
|
|
# unknown, it's no good (possibly Windows-only)
|
|
if (
|
|
p["state"] == "unknown" &&
|
|
h["status_reason"] == "localhost-response"
|
|
)
|
|
next
|
|
end
|
|
extra = ""
|
|
extra << p["product"] + " " if p["product"]
|
|
extra << p["version"] + " " if p["version"]
|
|
extra << p["extrainfo"] + " " if p["extrainfo"]
|
|
|
|
data = {}
|
|
data[:workspace] = wspace
|
|
if fix_services
|
|
data[:proto] = nmap_msf_service_map(p["protocol"])
|
|
else
|
|
data[:proto] = p["protocol"].downcase
|
|
end
|
|
data[:port] = p["portid"].to_i
|
|
data[:state] = p["state"]
|
|
data[:host] = addr
|
|
data[:info] = extra if not extra.empty?
|
|
if p["name"] != "unknown"
|
|
data[:name] = p["name"]
|
|
end
|
|
report_service(data)
|
|
}
|
|
}
|
|
|
|
REXML::Document.parse_stream(data, parser)
|
|
end
|
|
|
|
def nmap_msf_service_map(proto)
|
|
return proto unless proto.kind_of? String
|
|
case proto.downcase
|
|
when "msrpc", "nfs-or-iis"; "dcerpc"
|
|
when "netbios-ns"; "netbios"
|
|
when "netbios-ssn", "microsoft-ds"; "smb"
|
|
when "ms-sql-s"; "mssql"
|
|
when "ms-sql-m"; "mssql-m"
|
|
when "postgresql"; "postgres"
|
|
when "http-proxy"; "http"
|
|
when "iiimsf"; "db2"
|
|
else
|
|
proto.downcase
|
|
end
|
|
end
|
|
|
|
def report_import_note(wspace,addr)
|
|
if @import_filedata.kind_of?(Hash) && @import_filedata[:filename] && @import_filedata[:filename] !~ /msfe-nmap[0-9]{8}/
|
|
report_note(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.imported',
|
|
:data => @import_filedata.merge(:time=> Time.now.utc)
|
|
)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Import Nessus NBE files
|
|
#
|
|
def import_nessus_nbe_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_nessus_nbe(args.merge(:data => data))
|
|
end
|
|
|
|
def import_nessus_nbe(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
nbe_copy = data.dup
|
|
# First pass, just to build the address map.
|
|
addr_map = {}
|
|
|
|
nbe_copy.each_line do |line|
|
|
r = line.split('|')
|
|
next if r[0] != 'results'
|
|
next if r[4] != "12053"
|
|
data = r[6]
|
|
addr,hname = data.match(/([0-9\x2e]+) resolves as (.+)\x2e\\n/)[1,2]
|
|
addr_map[hname] = addr
|
|
end
|
|
|
|
data.each_line do |line|
|
|
r = line.split('|')
|
|
next if r[0] != 'results'
|
|
hname = r[2]
|
|
if addr_map[hname]
|
|
addr = addr_map[hname]
|
|
else
|
|
addr = hname # Must be unresolved, probably an IP address.
|
|
end
|
|
port = r[3]
|
|
nasl = r[4]
|
|
type = r[5]
|
|
data = r[6]
|
|
|
|
# If there's no resolution, or if it's malformed, skip it.
|
|
next unless ipv4_validator(addr)
|
|
|
|
if bl.include? addr
|
|
next
|
|
else
|
|
yield(:address,addr) if block
|
|
end
|
|
|
|
# Match the NBE types with the XML severity ratings
|
|
case type
|
|
# log messages don't actually have any data, they are just
|
|
# complaints about not being able to perform this or that test
|
|
# because such-and-such was missing
|
|
when "Log Message"; next
|
|
when "Security Hole"; severity = 3
|
|
when "Security Warning"; severity = 2
|
|
when "Security Note"; severity = 1
|
|
# a severity 0 means there's no extra data, it's just an open port
|
|
else; severity = 0
|
|
end
|
|
if nasl == "11936"
|
|
os = data.match(/The remote host is running (.*)\\n/)[1]
|
|
report_note(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.nessus_fingerprint',
|
|
:data => {
|
|
:os => os.to_s.strip
|
|
}
|
|
)
|
|
end
|
|
handle_nessus(wspace, addr, port, nasl, severity, data)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Of course they had to change the nessus format.
|
|
#
|
|
def import_openvas_xml(args={}, &block)
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
raise DBImportError.new("No OpenVAS XML support. Please submit a patch to msfdev[at]metasploit.com")
|
|
end
|
|
|
|
#
|
|
# Import Nessus XML v1 and v2 output
|
|
#
|
|
# Old versions of openvas exported this as well
|
|
#
|
|
def import_nessus_xml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
|
|
if data.index("NessusClientData_v2")
|
|
import_nessus_xml_v2(args.merge(:data => data))
|
|
else
|
|
import_nessus_xml(args.merge(:data => data))
|
|
end
|
|
end
|
|
|
|
def import_nessus_xml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
doc = rexmlify(data)
|
|
doc.elements.each('/NessusClientData/Report/ReportHost') do |host|
|
|
|
|
addr = nil
|
|
hname = nil
|
|
os = nil
|
|
# If the name is resolved, the Nessus plugin for DNS
|
|
# resolution should be there. If not, fall back to the
|
|
# HostName
|
|
host.elements.each('ReportItem') do |item|
|
|
next unless item.elements['pluginID'].text == "12053"
|
|
addr = item.elements['data'].text.match(/([0-9\x2e]+) resolves as/)[1]
|
|
hname = host.elements['HostName'].text
|
|
end
|
|
addr ||= host.elements['HostName'].text
|
|
next unless ipv4_validator(addr) # Skip resolved names and SCAN-ERROR.
|
|
if bl.include? addr
|
|
next
|
|
else
|
|
yield(:address,addr) if block
|
|
end
|
|
|
|
hinfo = {
|
|
:workspace => wspace,
|
|
:host => addr
|
|
}
|
|
|
|
# Record the hostname
|
|
hinfo.merge!(:name => hname.to_s.strip) if hname
|
|
report_host(hinfo)
|
|
|
|
# Record the OS
|
|
os ||= host.elements["os_name"]
|
|
if os
|
|
report_note(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.nessus_fingerprint',
|
|
:data => {
|
|
:os => os.text.to_s.strip
|
|
}
|
|
)
|
|
end
|
|
|
|
host.elements.each('ReportItem') do |item|
|
|
nasl = item.elements['pluginID'].text
|
|
port = item.elements['port'].text
|
|
data = item.elements['data'].text
|
|
severity = item.elements['severity'].text
|
|
|
|
handle_nessus(wspace, addr, port, nasl, severity, data)
|
|
end
|
|
end
|
|
end
|
|
|
|
def import_nessus_xml_v2(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
doc = rexmlify(data)
|
|
doc.elements.each('/NessusClientData_v2/Report/ReportHost') do |host|
|
|
# if Nessus resovled the host, its host-ip tag should be set
|
|
# otherwise, fall back to the name attribute which would
|
|
# logically need to be an IP address
|
|
begin
|
|
addr = host.elements["HostProperties/tag[@name='host-ip']"].text
|
|
rescue
|
|
addr = host.attribute("name").value
|
|
end
|
|
|
|
next unless ipv4_validator(addr) # Catches SCAN-ERROR, among others.
|
|
if bl.include? addr
|
|
next
|
|
else
|
|
yield(:address,addr) if block
|
|
end
|
|
|
|
os = host.elements["HostProperties/tag[@name='operating-system']"]
|
|
if os
|
|
report_note(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:type => 'host.os.nessus_fingerprint',
|
|
:data => {
|
|
:os => os.text.to_s.strip
|
|
}
|
|
)
|
|
end
|
|
|
|
hname = host.elements["HostProperties/tag[@name='host-fqdn']"]
|
|
if hname
|
|
report_host(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:name => hname.text.to_s.strip
|
|
)
|
|
end
|
|
|
|
mac = host.elements["HostProperties/tag[@name='mac-address']"]
|
|
if mac
|
|
report_host(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:mac => mac.text.to_s.strip.upcase
|
|
)
|
|
end
|
|
|
|
host.elements.each('ReportItem') do |item|
|
|
nasl = item.attribute('pluginID').value
|
|
port = item.attribute('port').value
|
|
proto = item.attribute('protocol').value
|
|
name = item.attribute('svc_name').value
|
|
severity = item.attribute('severity').value
|
|
description = item.elements['plugin_output']
|
|
cve = item.elements['cve']
|
|
bid = item.elements['bid']
|
|
xref = item.elements['xref']
|
|
|
|
handle_nessus_v2(wspace, addr, port, proto, name, nasl, severity, description, cve, bid, xref)
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Import Qualys' xml output
|
|
#
|
|
def import_qualys_xml_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_qualys_xml(args.merge(:data => data))
|
|
end
|
|
|
|
def import_qualys_xml(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
|
|
doc = rexmlify(data)
|
|
doc.elements.each('/SCAN/IP') do |host|
|
|
addr = host.attributes['value']
|
|
if bl.include? addr
|
|
next
|
|
else
|
|
yield(:address,addr) if block
|
|
end
|
|
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(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
import_ip_list(args.merge(:data => data))
|
|
end
|
|
|
|
def import_ip_list(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
data.each_line do |ip|
|
|
ip.strip!
|
|
if bl.include? ip
|
|
next
|
|
else
|
|
yield(:address,ip) if block
|
|
end
|
|
host = find_or_create_host(:workspace => wspace, :host=> ip, :state => Msf::HostState::Alive)
|
|
end
|
|
end
|
|
|
|
def import_amap_log_file(args={})
|
|
filename = args[:filename]
|
|
wspace = args[:wspace] || workspace
|
|
f = File.open(filename, 'rb')
|
|
data = f.read(f.stat.size)
|
|
case import_filetype_detect(data)
|
|
when :amap_log
|
|
import_amap_log(args.merge(:data => data))
|
|
when :amap_mlog
|
|
import_amap_mlog(args.merge(:data => data))
|
|
else
|
|
raise DBImportError.new("Could not determine file type")
|
|
end
|
|
end
|
|
|
|
def import_amap_log(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
data.each_line do |line|
|
|
next if line =~ /^#/
|
|
next if line !~ /^Protocol on ([^:]+):([^\x5c\x2f]+)[\x5c\x2f](tcp|udp) matches (.*)$/
|
|
addr = $1
|
|
next if bl.include? addr
|
|
port = $2.to_i
|
|
proto = $3.downcase
|
|
name = $4
|
|
host = find_or_create_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive)
|
|
next if not host
|
|
yield(:address,addr) if block
|
|
info = {
|
|
:workspace => wspace,
|
|
:host => host,
|
|
:proto => proto,
|
|
:port => port
|
|
}
|
|
if name != "unidentified"
|
|
info[:name] = name
|
|
end
|
|
service = find_or_create_service(info)
|
|
end
|
|
end
|
|
|
|
def import_amap_mlog(args={}, &block)
|
|
data = args[:data]
|
|
wspace = args[:wspace] || workspace
|
|
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
|
|
|
data.each_line do |line|
|
|
next if line =~ /^#/
|
|
r = line.split(':')
|
|
next if r.length < 6
|
|
|
|
addr = r[0]
|
|
next if bl.include? addr
|
|
port = r[1].to_i
|
|
proto = r[2].downcase
|
|
status = r[3]
|
|
name = r[5]
|
|
next if status != "open"
|
|
|
|
host = find_or_create_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive)
|
|
next if not host
|
|
yield(:address,addr) if block
|
|
info = {
|
|
:workspace => wspace,
|
|
:host => host,
|
|
:proto => proto,
|
|
:port => port
|
|
}
|
|
if name != "unidentified"
|
|
info[:name] = name
|
|
end
|
|
service = find_or_create_service(info)
|
|
end
|
|
end
|
|
|
|
def unserialize_object(xml_elem, allow_yaml = false)
|
|
string = xml_elem.text.to_s.strip
|
|
return string unless string.is_a?(String)
|
|
return nil if not string
|
|
return nil if string.empty?
|
|
|
|
begin
|
|
# Validate that it is properly formed base64 first
|
|
if string.gsub(/\s+/, '') =~ /^([a-z0-9A-Z\+\/=]+)$/
|
|
Marshal.load($1.unpack("m")[0])
|
|
else
|
|
if allow_yaml
|
|
begin
|
|
YAML.load(string)
|
|
rescue
|
|
dlog("Badly formatted YAML: '#{string}'")
|
|
string
|
|
end
|
|
else
|
|
string
|
|
end
|
|
end
|
|
rescue ::Exception => e
|
|
if allow_yaml
|
|
YAML.load(string) rescue string
|
|
else
|
|
string
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
protected
|
|
|
|
#
|
|
# This holds all of the shared parsing/handling used by the
|
|
# Nessus NBE and NESSUS v1 methods
|
|
#
|
|
def handle_nessus(wspace, addr, port, nasl, severity, data)
|
|
# The port section looks like:
|
|
# http (80/tcp)
|
|
p = port.match(/^([^\(]+)\((\d+)\/([^\)]+)\)/)
|
|
return if not p
|
|
|
|
report_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive)
|
|
name = p[1].strip
|
|
port = p[2].to_i
|
|
proto = p[3].downcase
|
|
|
|
info = { :workspace => wspace, :host => addr, :port => port, :proto => proto }
|
|
if name != "unknown" and name[-1,1] != "?"
|
|
info[:name] = name
|
|
end
|
|
report_service(info)
|
|
|
|
return if not nasl
|
|
|
|
data.gsub!("\\n", "\n")
|
|
|
|
refs = []
|
|
|
|
if (data =~ /^CVE : (.*)$/)
|
|
$1.gsub(/C(VE|AN)\-/, '').split(',').map { |r| r.strip }.each do |r|
|
|
refs.push('CVE-' + r)
|
|
end
|
|
end
|
|
|
|
if (data =~ /^BID : (.*)$/)
|
|
$1.split(',').map { |r| r.strip }.each do |r|
|
|
refs.push('BID-' + r)
|
|
end
|
|
end
|
|
|
|
if (data =~ /^Other references : (.*)$/)
|
|
$1.split(',').map { |r| r.strip }.each do |r|
|
|
ref_id, ref_val = r.split(':')
|
|
ref_val ? refs.push(ref_id + '-' + ref_val) : refs.push(ref_id)
|
|
end
|
|
end
|
|
|
|
nss = 'NSS-' + nasl.to_s
|
|
|
|
vuln_info = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:port => port,
|
|
:proto => proto,
|
|
:name => nss,
|
|
:info => data,
|
|
:refs => refs
|
|
}
|
|
report_vuln(vuln_info)
|
|
end
|
|
|
|
#
|
|
# NESSUS v2 file format has a dramatically different layout
|
|
# for ReportItem data
|
|
#
|
|
def handle_nessus_v2(wspace,addr,port,proto,name,nasl,severity,description,cve,bid,xref)
|
|
|
|
report_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive)
|
|
|
|
info = { :workspace => wspace, :host => addr, :port => port, :proto => proto }
|
|
if name != "unknown" and name[-1,1] != "?"
|
|
info[:name] = name
|
|
end
|
|
|
|
if port.to_i != 0
|
|
report_service(info)
|
|
end
|
|
|
|
return if nasl == "0"
|
|
|
|
refs = []
|
|
|
|
cve.collect do |r|
|
|
r.to_s.gsub!(/C(VE|AN)\-/, '')
|
|
refs.push('CVE-' + r.to_s)
|
|
end if cve
|
|
|
|
bid.collect do |r|
|
|
refs.push('BID-' + r.to_s)
|
|
end if bid
|
|
|
|
xref.collect do |r|
|
|
ref_id, ref_val = r.to_s.split(':')
|
|
ref_val ? refs.push(ref_id + '-' + ref_val) : refs.push(ref_id)
|
|
end if xref
|
|
|
|
nss = 'NSS-' + nasl
|
|
|
|
vuln = {
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:name => nss,
|
|
:info => description ? description.text : "",
|
|
:refs => refs
|
|
}
|
|
|
|
if port.to_i != 0
|
|
vuln[:port] = port
|
|
vuln[:proto] = proto
|
|
end
|
|
|
|
report_vuln(vuln)
|
|
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
|
|
|
|
if info[:host] && info[:port] && info[:proto]
|
|
report_service(info)
|
|
end
|
|
|
|
return if qid == 0
|
|
|
|
if addr
|
|
report_vuln(
|
|
:workspace => wspace,
|
|
:host => addr,
|
|
:port => port,
|
|
:proto => protocol,
|
|
:name => 'QUALYS-' + qid,
|
|
:refs => refs
|
|
)
|
|
end
|
|
end
|
|
|
|
def process_nexpose_data_sxml_refs(vuln)
|
|
refs = []
|
|
vid = vuln.attributes['id'].to_s.downcase
|
|
vry = vuln.attributes['resultCode'].to_s.upcase
|
|
|
|
# Only process vuln-exploitable and vuln-version statuses
|
|
return if vry !~ /^V[VE]$/
|
|
|
|
refs = []
|
|
vuln.elements.each('id') do |ref|
|
|
rtyp = ref.attributes['type'].to_s.upcase
|
|
rval = ref.text.to_s.strip
|
|
case rtyp
|
|
when 'CVE'
|
|
refs << rval.gsub('CAN', 'CVE')
|
|
when 'MS' # obsolete?
|
|
refs << "MSB-MS-#{rval}"
|
|
else
|
|
refs << "#{rtyp}-#{rval}"
|
|
end
|
|
end
|
|
|
|
refs << "NEXPOSE-#{vid}"
|
|
refs
|
|
end
|
|
|
|
#
|
|
# NeXpose vuln lookup
|
|
#
|
|
def nexpose_vuln_lookup(wspace, doc, vid, refs, host, serv=nil)
|
|
doc.elements.each("/NexposeReport/VulnerabilityDefinitions/vulnerability[@id = '#{vid}']]") do |vulndef|
|
|
|
|
title = vulndef.attributes['title']
|
|
pciSeverity = vulndef.attributes['pciSeverity']
|
|
cvss_score = vulndef.attributes['cvssScore']
|
|
cvss_vector = vulndef.attributes['cvssVector']
|
|
|
|
vulndef.elements['references'].elements.each('reference') do |ref|
|
|
if ref.attributes['source'] == 'BID'
|
|
refs[ 'BID-' + ref.text ] = true
|
|
elsif ref.attributes['source'] == 'CVE'
|
|
# ref.text is CVE-$ID
|
|
refs[ ref.text ] = true
|
|
elsif ref.attributes['source'] == 'MS'
|
|
refs[ 'MSB-MS-' + ref.text ] = true
|
|
end
|
|
end
|
|
|
|
refs[ 'NEXPOSE-' + vid.downcase ] = true
|
|
|
|
vuln = find_or_create_vuln(
|
|
:workspace => wspace,
|
|
:host => host,
|
|
:service => serv,
|
|
:name => 'NEXPOSE-' + vid.downcase,
|
|
:info => title)
|
|
|
|
rids = []
|
|
refs.keys.each do |r|
|
|
rids << find_or_create_ref(:name => r)
|
|
end
|
|
|
|
vuln.refs << (rids - vuln.refs)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|