metasploit-framework/lib/msf/core/db.rb

5708 lines
157 KiB
Ruby
Raw Normal View History

# Check Rex::Parser.nokogiri_loaded for status of the Nokogiri parsers
require 'rex/parser/nmap_nokogiri'
require 'rex/parser/nexpose_simple_nokogiri'
require 'rex/parser/nexpose_raw_nokogiri'
require 'rex/parser/foundstone_nokogiri'
require 'rex/parser/mbsa_nokogiri'
require 'rex/parser/acunetix_nokogiri'
require 'rex/parser/appscan_nokogiri'
require 'rex/parser/burp_session_nokogiri'
require 'rex/parser/ci_nokogiri'
# Legacy XML parsers -- these will be converted some day
require 'rex/parser/nmap_xml'
require 'rex/parser/nexpose_xml'
require 'rex/parser/retina_xml'
require 'rex/parser/netsparker_xml'
require 'rex/parser/nessus_xml'
require 'rex/parser/ip360_xml'
require 'rex/parser/ip360_aspl_xml'
require 'rex/socket'
require 'zip'
require 'packetfu'
require 'uri'
require 'tmpdir'
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 rfc3330_reserved(ip)
case ip.class.to_s
when "PacketFu::Octets"
ip_x = ip.to_x
ip_i = ip.to_i
when "String"
if ipv46_validator(ip)
ip_x = ip
ip_i = Rex::Socket.addr_atoi(ip)
else
raise ArgumentError, "Invalid IP address: #{ip.inspect}"
end
when "Fixnum"
if (0..2**32-1).include? ip
ip_x = Rex::Socket.addr_itoa(ip)
ip_i = ip
else
raise ArgumentError, "Invalid IP address: #{ip.inspect}"
end
else
raise ArgumentError, "Invalid IP address: #{ip.inspect}"
end
return true if Rex::Socket::RangeWalker.new("0.0.0.0-0.255.255.255").include? ip_x
return true if Rex::Socket::RangeWalker.new("127.0.0.0-127.255.255.255").include? ip_x
return true if Rex::Socket::RangeWalker.new("169.254.0.0-169.254.255.255").include? ip_x
return true if Rex::Socket::RangeWalker.new("224.0.0.0-239.255.255.255").include? ip_x
return true if Rex::Socket::RangeWalker.new("255.255.255.255-255.255.255.255").include? ip_x
return false
end
def ipv46_validator(addr)
ipv4_validator(addr) or ipv6_validator(addr)
end
def ipv4_validator(addr)
return false unless addr.kind_of? String
Rex::Socket.is_ipv4?(addr)
end
def ipv6_validator(addr)
Rex::Socket.is_ipv6?(addr)
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(/\s+/).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
# There is no more queue.
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
if wspace.kind_of? String
wspace = find_workspace(wspace)
end
2011-12-10 19:24:20 +00:00
address, scope = address.split('%', 2)
return wspace.hosts.find_by_address(address)
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)
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
# +:scope+:: -- interface identifier for link-local IPv6
# +:virtual_host+:: -- the name of the VM host software, eg "VMWare", "QEMU", "Xen", etc.
#
def report_host(opts)
return if not active
addr = opts.delete(:host) || return
# Sometimes a host setup through a pivot will see the address as "Remote Pipe"
if addr.eql? "Remote Pipe"
return
end
wspace = opts.delete(:workspace) || workspace
Merge session-host-rework branch back to master Squashed commit of the following: commit 2f4e8df33c5b4baa8d6fd67b400778a3f93482aa Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:31:03 2012 -0700 Clean up some rdoc comments This adds categories for the various interfaces that meterpreter and shell sessions implement so they are grouped logically in the docs. commit 9d31bc1b35845f7279148412f49bda56a39c9d9d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 13:00:25 2012 -0700 Combine the docs into one output dir There's really no need to separate the API sections into their own directory. Combining them makes it much easier to read. commit eadd7fc136a9e7e4d9652d55dfb86e6f318332e0 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:27:22 2012 -0700 Keep the order of iface attributes the same accross rubies 1.8 doesn't maintain insertion order for Hash keys like 1.9 does so we end up with ~random order for the display with the previous technique. Switch to an Array instead of a Hash so it's always the same. commit 6f66dd40f39959711f9bacbda99717253a375d21 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:23:35 2012 -0700 Fix a few more compiler warnings commit f39cb536a80c5000a5b9ca1fec5902300ae4b440 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:17:39 2012 -0700 Fix a type-safety warning commit 1e52785f38146515409da3724f858b9603d19454 Author: James Lee <egypt@metasploit.com> Date: Mon Feb 27 15:21:36 2012 -0700 LHOST should be OptAddress, not OptAddressRange commit acef978aa4233c7bd0b00ef63646eb4da5457f67 Author: James Lee <egypt@metasploit.com> Date: Sun Feb 26 17:45:59 2012 -0700 Fix a couple of warnings and a typo commit 29d87f88790aa1b3e5db6df650ecfb3fb93c675b Author: HD Moore <hdm@digitaloffense.net> Date: Mon Feb 27 11:54:29 2012 -0600 Fix ctype vs content_type typo commit 83b5400356c47dd1973e6be3aa343084dfd09c73 Author: Gregory Man <man.gregory@gmail.com> Date: Sun Feb 26 15:38:33 2012 +0200 Fixed scripts/meterpreter/enum_firefox to work with firefox > 3.6.x commit 49c2c80b347820d02348d694cc71f1b3028b4365 Author: Steve Tornio <swtornio@gmail.com> Date: Sun Feb 26 07:13:13 2012 -0600 add osvdb ref commit e18e1fe97b89c3a2b8c22bc6c18726853d2c2bee Author: Matt Andreko <mandreko@gmail.com> Date: Sat Feb 25 18:02:56 2012 -0500 Added aspx target to msfvenom. This in turn added it to msfencode as well. Ref: https://github.com/rapid7/metasploit-framework/pull/188 Tested on winxp with IIS in .net 1.1 and 2.0 modes commit e6aa5072112d79bbf8a4d2289cf8d301db3932f5 Author: Joshua J. Drake <github.jdrake@qoop.org> Date: Sat Feb 25 13:00:48 2012 -0600 Fixes #6308: Fall back to 127.0.0.1 when SocketError is raised from the resolver commit b3371e8bfeea4d84f9d0cba100352b57d7e9e78b Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 17:07:42 2012 -0700 Simplify logic for whether an inner iface has the same address commit 5417419f35a40d1c08ca11ca40744722692d3b0d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:58:16 2012 -0700 Whitespace commit 9036875c2918439ae23e11ee7b958e30ccc29545 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:53:45 2012 -0700 Set session info before worrying about address get_interfaces can take a while on Linux, grab uid and hostname earlier so we can give the user an idea of what they popped as soon as possible. commit f34b51c6291031ab25b5bfb1ac6307a516ab0ee9 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:48:42 2012 -0700 Clean up rdoc commit e61a0663454400ec66f59a80d18b0baff4cb8cd9 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:54:45 2012 -0600 Ensure the architecture is only the first word (not the full WOW64 message in some cases) commit 4c701610976a92298c1182eecc9291a1b301e43b Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:49:17 2012 -0600 More paranoia code, just in case RHOST is set to whitespace commit c5ff89fe3dc9061e0fa9f761e6530f6571989d28 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:47:01 2012 -0600 A few more small bug fixes to handle cases with an empty string target host resulting in a bad address commit 462d0188a1298f29ac83b10349aec6737efc5b19 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 03:55:10 2012 -0600 Fix up the logic (reversed by accident) commit 2b2b0adaec2448423dbd3ec54d90a5721965e2df Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 23:29:52 2012 -0600 Automatically parse system information and populate the db, identify and report NAT when detected, show the real session_host in the sessions -l listing commit 547a4ab4c62dc3248f847dd5d305ad3b74157348 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:16:03 2012 -0600 Fix typo introduced commit 27a7b7961e61894bdecd55310a8f45d0917c5a5c Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:11:38 2012 -0600 More session.session_host tweaks commit e447302a1a9915795e89b5e29c89ff2ab9b6209b Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:08:20 2012 -0600 Additional tunnel_peer changes commit 93369fcffaf8c6b00d992526b4083acfce036bb3 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:06:21 2012 -0600 Additional changes to session.session_host commit c3552f66d158685909e2c8b51dfead7c240c4f40 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:00:19 2012 -0600 Merge changes into the new branch
2012-02-29 01:28:47 +00:00
if wspace.kind_of? String
wspace = find_workspace(wspace)
end
ret = { }
if not addr.kind_of? Host
addr = normalize_host(addr)
2011-12-10 19:24:20 +00:00
addr, scope = addr.split('%', 2)
opts[:scope] = scope if scope
unless ipv46_validator(addr)
raise ::ArgumentError, "Invalid IP address in report_host(): #{addr}"
end
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
else
host = addr
end
# Truncate the info field at the maximum field length
if opts[:info]
opts[:info] = opts[:info][0,65535]
end
# Truncate the name field at the maximum field length
if opts[:name]
opts[:name] = opts[:name][0,255]
end
opts.each { |k,v|
if (host.attribute_names.include?(k.to_s))
unless host.attribute_locked?(k.to_s)
host[k] = v.to_s.gsub(/[\x00-\x1f]/, '')
end
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
2011-12-10 19:24:20 +00:00
if host.changed?
msf_import_timestamps(opts,host)
host.save!
end
host
end
Merge session-host-rework branch back to master Squashed commit of the following: commit 2f4e8df33c5b4baa8d6fd67b400778a3f93482aa Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:31:03 2012 -0700 Clean up some rdoc comments This adds categories for the various interfaces that meterpreter and shell sessions implement so they are grouped logically in the docs. commit 9d31bc1b35845f7279148412f49bda56a39c9d9d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 13:00:25 2012 -0700 Combine the docs into one output dir There's really no need to separate the API sections into their own directory. Combining them makes it much easier to read. commit eadd7fc136a9e7e4d9652d55dfb86e6f318332e0 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:27:22 2012 -0700 Keep the order of iface attributes the same accross rubies 1.8 doesn't maintain insertion order for Hash keys like 1.9 does so we end up with ~random order for the display with the previous technique. Switch to an Array instead of a Hash so it's always the same. commit 6f66dd40f39959711f9bacbda99717253a375d21 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:23:35 2012 -0700 Fix a few more compiler warnings commit f39cb536a80c5000a5b9ca1fec5902300ae4b440 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:17:39 2012 -0700 Fix a type-safety warning commit 1e52785f38146515409da3724f858b9603d19454 Author: James Lee <egypt@metasploit.com> Date: Mon Feb 27 15:21:36 2012 -0700 LHOST should be OptAddress, not OptAddressRange commit acef978aa4233c7bd0b00ef63646eb4da5457f67 Author: James Lee <egypt@metasploit.com> Date: Sun Feb 26 17:45:59 2012 -0700 Fix a couple of warnings and a typo commit 29d87f88790aa1b3e5db6df650ecfb3fb93c675b Author: HD Moore <hdm@digitaloffense.net> Date: Mon Feb 27 11:54:29 2012 -0600 Fix ctype vs content_type typo commit 83b5400356c47dd1973e6be3aa343084dfd09c73 Author: Gregory Man <man.gregory@gmail.com> Date: Sun Feb 26 15:38:33 2012 +0200 Fixed scripts/meterpreter/enum_firefox to work with firefox > 3.6.x commit 49c2c80b347820d02348d694cc71f1b3028b4365 Author: Steve Tornio <swtornio@gmail.com> Date: Sun Feb 26 07:13:13 2012 -0600 add osvdb ref commit e18e1fe97b89c3a2b8c22bc6c18726853d2c2bee Author: Matt Andreko <mandreko@gmail.com> Date: Sat Feb 25 18:02:56 2012 -0500 Added aspx target to msfvenom. This in turn added it to msfencode as well. Ref: https://github.com/rapid7/metasploit-framework/pull/188 Tested on winxp with IIS in .net 1.1 and 2.0 modes commit e6aa5072112d79bbf8a4d2289cf8d301db3932f5 Author: Joshua J. Drake <github.jdrake@qoop.org> Date: Sat Feb 25 13:00:48 2012 -0600 Fixes #6308: Fall back to 127.0.0.1 when SocketError is raised from the resolver commit b3371e8bfeea4d84f9d0cba100352b57d7e9e78b Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 17:07:42 2012 -0700 Simplify logic for whether an inner iface has the same address commit 5417419f35a40d1c08ca11ca40744722692d3b0d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:58:16 2012 -0700 Whitespace commit 9036875c2918439ae23e11ee7b958e30ccc29545 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:53:45 2012 -0700 Set session info before worrying about address get_interfaces can take a while on Linux, grab uid and hostname earlier so we can give the user an idea of what they popped as soon as possible. commit f34b51c6291031ab25b5bfb1ac6307a516ab0ee9 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:48:42 2012 -0700 Clean up rdoc commit e61a0663454400ec66f59a80d18b0baff4cb8cd9 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:54:45 2012 -0600 Ensure the architecture is only the first word (not the full WOW64 message in some cases) commit 4c701610976a92298c1182eecc9291a1b301e43b Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:49:17 2012 -0600 More paranoia code, just in case RHOST is set to whitespace commit c5ff89fe3dc9061e0fa9f761e6530f6571989d28 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:47:01 2012 -0600 A few more small bug fixes to handle cases with an empty string target host resulting in a bad address commit 462d0188a1298f29ac83b10349aec6737efc5b19 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 03:55:10 2012 -0600 Fix up the logic (reversed by accident) commit 2b2b0adaec2448423dbd3ec54d90a5721965e2df Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 23:29:52 2012 -0600 Automatically parse system information and populate the db, identify and report NAT when detected, show the real session_host in the sessions -l listing commit 547a4ab4c62dc3248f847dd5d305ad3b74157348 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:16:03 2012 -0600 Fix typo introduced commit 27a7b7961e61894bdecd55310a8f45d0917c5a5c Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:11:38 2012 -0600 More session.session_host tweaks commit e447302a1a9915795e89b5e29c89ff2ab9b6209b Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:08:20 2012 -0600 Additional tunnel_peer changes commit 93369fcffaf8c6b00d992526b4083acfce036bb3 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:06:21 2012 -0600 Additional changes to session.session_host commit c3552f66d158685909e2c8b51dfead7c240c4f40 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:00:19 2012 -0600 Merge changes into the new branch
2012-02-29 01:28:47 +00:00
#
# Update a host's attributes via semi-standardized sysinfo hash (Meterpreter)
#
# The opts parameter MUST contain the following entries
# +:host+:: -- the host's ip address
# +:info+:: -- the information hash
# * 'Computer' -- the host name
# * 'OS' -- the operating system string
# * 'Architecture' -- the hardware architecture
# * 'System Language' -- the system language
#
# The opts parameter can contain:
# +:workspace+:: -- the workspace for this host
#
def update_host_via_sysinfo(opts)
return if not active
addr = opts.delete(:host) || return
info = opts.delete(:info) || return
# Sometimes a host setup through a pivot will see the address as "Remote Pipe"
if addr.eql? "Remote Pipe"
return
end
wspace = opts.delete(:workspace) || workspace
if wspace.kind_of? String
wspace = find_workspace(wspace)
end
if not addr.kind_of? Host
addr = normalize_host(addr)
addr, scope = addr.split('%', 2)
opts[:scope] = scope if scope
unless ipv46_validator(addr)
raise ::ArgumentError, "Invalid IP address in report_host(): #{addr}"
end
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
else
host = addr
end
res = {}
if info['Computer']
res[:name] = info['Computer']
end
if info['Architecture']
res[:arch] = info['Architecture'].split(/\s+/).first
end
if info['OS'] =~ /^Windows\s*([^\(]+)\(([^\)]+)\)/i
res[:os_name] = "Microsoft Windows"
res[:os_flavor] = $1.strip
build = $2.strip
if build =~ /Service Pack (\d+)/
res[:os_sp] = "SP" + $1
else
res[:os_sp] = "SP0"
end
end
if info["System Language"]
case info["System Language"]
when /^en_/
res[:os_lang] = "English"
end
end
# Truncate the info field at the maximum field length
if res[:info]
res[:info] = res[:info][0,65535]
end
# Truncate the name field at the maximum field length
if res[:name]
res[:name] = res[:name][0,255]
end
res.each { |k,v|
if (host.attribute_names.include?(k.to_s))
unless host.attribute_locked?(k.to_s)
host[k] = v.to_s.gsub(/[\x00-\x1f]/, '')
end
else
dlog("Unknown attribute for Host: #{k}")
end
}
# 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
if host.changed?
host.save!
end
host
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)
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)
# +:sname+:: an alias for the above
#
def report_service(opts)
return if not active
addr = opts.delete(:host) || return
hname = opts.delete(:host_name)
hmac = opts.delete(:mac)
host = nil
wspace = opts.delete(:workspace) || workspace
hopts = {:workspace => wspace, :host => addr}
hopts[:name] = hname if hname
hopts[:mac] = hmac if hmac
# Other report_* methods take :sname to mean the service name, so we
# map it here to ensure it ends up in the right place despite not being
# a real column.
if opts[:sname]
opts[:name] = opts.delete(:sname)
end
if addr.kind_of? Host
host = addr
addr = host.address
else
host = report_host(hopts)
end
if opts[:port].to_i.zero?
dlog("Skipping port zero for service '%s' on host '%s'" % [opts[:name],host.address])
return nil
end
ret = {}
=begin
host = get_host(:workspace => wspace, :address => addr)
if host
host.updated_at = host.created_at
host.state = HostState::Alive
host.save!
end
=end
proto = opts[:proto] || 'tcp'
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 and k == :name) ? v.to_s.downcase : 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
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
# Returns a session based on opened_time, host address, and workspace
# (or returns nil)
def get_session(opts)
return if not active
wspace = opts[:workspace] || opts[:wspace] || workspace
addr = opts[:addr] || opts[:address] || opts[:host] || return
host = get_host(:workspace => wspace, :host => addr)
time = opts[:opened_at] || opts[:created_at] || opts[:time] || return
Msf::DBManager::Session.find_by_host_id_and_opened_at(host.id, time)
end
# Record a new session in the database
#
# opts MUST contain either
# +:session+:: the Msf::Session object we are reporting
# +:host+:: the Host object we are reporting a session on.
#
def report_session(opts)
return if not active
if opts[:session]
raise ArgumentError.new("Invalid :session, expected Msf::Session") unless opts[:session].kind_of? Msf::Session
session = opts[:session]
wspace = opts[:workspace] || find_workspace(session.workspace)
h_opts = { }
h_opts[:host] = normalize_host(session)
h_opts[:arch] = session.arch if session.respond_to?(:arch) and session.arch
h_opts[:workspace] = wspace
host = find_or_create_host(h_opts)
sess_data = {
:host_id => host.id,
:stype => session.type,
:desc => session.info,
:platform => session.platform,
:via_payload => session.via_payload,
:via_exploit => session.via_exploit,
:routes => [],
:datastore => session.exploit_datastore.to_h,
:opened_at => Time.now.utc,
:last_seen => Time.now.utc,
:local_id => session.sid
}
elsif opts[:host]
raise ArgumentError.new("Invalid :host, expected Host object") unless opts[:host].kind_of? Host
host = opts[:host]
sess_data = {
:host_id => host.id,
:stype => opts[:stype],
:desc => opts[:desc],
:platform => opts[:platform],
:via_payload => opts[:via_payload],
:via_exploit => opts[:via_exploit],
:routes => opts[:routes] || [],
:datastore => opts[:datastore],
:opened_at => opts[:opened_at],
:closed_at => opts[:closed_at],
:last_seen => opts[:last_seen] || opts[:closed_at],
:close_reason => opts[:close_reason],
}
else
raise ArgumentError.new("Missing option :session or :host")
end
ret = {}
# Truncate the session data if necessary
if sess_data[:desc]
sess_data[:desc] = sess_data[:desc][0,255]
end
s = Msf::DBManager::Session.new(sess_data)
s.save!
if opts[:session]
session.db_record = s
end
# If this is a live session, we know the host is vulnerable to something.
# If the exploit used was multi/handler, though, we don't know what
# it's vulnerable to, so it isn't really useful to save it.
if opts[:session] and session.via_exploit and session.via_exploit != "exploit/multi/handler"
return unless host
port = session.exploit_datastore["RPORT"]
service = (port ? host.services.find_by_port(port) : nil)
mod = framework.modules.create(session.via_exploit)
vuln_info = {
:host => host.address,
:name => session.via_exploit,
:refs => mod.references,
:workspace => wspace,
:exploited_at => Time.now.utc
}
framework.db.report_vuln(vuln_info)
end
s
end
#
# Record a session event in the database
#
# opts MUST contain one of:
# +:session+:: the Msf::Session OR the Msf::DBManager::Session we are reporting
# +:etype+:: event type, enum: command, output, upload, download, filedelete
#
# opts may contain
# +:output+:: the data for an output event
# +:command+:: the data for an command event
# +:remote_path+:: path to the associated file for upload, download, and filedelete events
# +:local_path+:: path to the associated file for upload, and download
#
def report_session_event(opts)
return if not active
raise ArgumentError.new("Missing required option :session") if opts[:session].nil?
raise ArgumentError.new("Expected an :etype") unless opts[:etype]
session = nil
if opts[:session].respond_to? :db_record
session = opts[:session].db_record
if session.nil?
# The session doesn't have a db_record which means
# a) the database wasn't connected at session registration time
# or
# b) something awful happened and the report_session call failed
#
# Either way, we can't do anything with this session as is, so
# log a warning and punt.
wlog("Warning: trying to report a session_event for a session with no db_record (#{opts[:session].sid})")
return
end
event_data = { :created_at => Time.now }
else
session = opts[:session]
event_data = { :created_at => opts[:created_at] }
end
event_data[:session_id] = session.id
[:remote_path, :local_path, :output, :command, :etype].each do |attr|
event_data[attr] = opts[attr] if opts[attr]
end
s = Msf::DBManager::SessionEvent.create(event_data)
end
def report_session_route(session, route)
return if not active
if session.respond_to? :db_record
s = session.db_record
else
s = session
end
unless s.respond_to?(:routes)
raise ArgumentError.new("Invalid :session, expected Session object got #{session.class}")
end
subnet, netmask = route.split("/")
s.routes.create(:subnet => subnet, :netmask => netmask)
end
def report_session_route_remove(session, route)
return if not active
if session.respond_to? :db_record
s = session.db_record
else
s = session
end
unless s.respond_to?(:routes)
raise ArgumentError.new("Invalid :session, expected Session object got #{session.class}")
end
subnet, netmask = route.split("/")
r = s.routes.find_by_subnet_and_netmask(subnet, netmask)
r.destroy if r
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)
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)
ret = {}
host = get_host(:workspace => wspace, :host => addr)
client = host.clients.find_or_initialize_by_ua_string(opts[:ua_string])
opts[:ua_string] = opts[:ua_string].to_s
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
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)
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
wspace = opts.delete(:workspace) || workspace
if wspace.kind_of? String
wspace = find_workspace(wspace)
end
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
addr = normalize_host(opts[:host])
host = report_host({:workspace => wspace, :host => addr})
end
# Do the same for a service if that's also included.
if (opts[:port])
proto = nil
sname = nil
case opts[:proto].to_s.downcase # Catch incorrect usages
when 'tcp','udp'
proto = opts[:proto]
sname = opts[:sname] if opts[:sname]
when 'dns','snmp','dhcp'
proto = 'udp'
sname = opts[:proto]
else
proto = 'tcp'
sname = opts[:proto]
end
sopts = {
:workspace => wspace,
:host => 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 = {}
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
=begin
if host
host.updated_at = host.created_at
host.state = HostState::Alive
host.save!
end
=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
case mode
when :unique
notes = wspace.notes.find(:all, :conditions => conditions)
# 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
notes = wspace.notes.find(:all, :conditions => conditions)
# 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
end
#
# This methods returns a list of all notes in the database
#
def notes(wspace=workspace)
wspace.notes
end
# This is only exercised by MSF3 XML importing for now. Needs the wait
# conditions and return hash as well.
def report_host_tag(opts)
name = opts.delete(:name)
raise DBImportError.new("Missing required option :name") unless name
addr = opts.delete(:addr)
raise DBImportError.new("Missing required option :addr") unless addr
wspace = opts.delete(:wspace)
raise DBImportError.new("Missing required option :wspace") unless wspace
Merge session-host-rework branch back to master Squashed commit of the following: commit 2f4e8df33c5b4baa8d6fd67b400778a3f93482aa Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:31:03 2012 -0700 Clean up some rdoc comments This adds categories for the various interfaces that meterpreter and shell sessions implement so they are grouped logically in the docs. commit 9d31bc1b35845f7279148412f49bda56a39c9d9d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 13:00:25 2012 -0700 Combine the docs into one output dir There's really no need to separate the API sections into their own directory. Combining them makes it much easier to read. commit eadd7fc136a9e7e4d9652d55dfb86e6f318332e0 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:27:22 2012 -0700 Keep the order of iface attributes the same accross rubies 1.8 doesn't maintain insertion order for Hash keys like 1.9 does so we end up with ~random order for the display with the previous technique. Switch to an Array instead of a Hash so it's always the same. commit 6f66dd40f39959711f9bacbda99717253a375d21 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:23:35 2012 -0700 Fix a few more compiler warnings commit f39cb536a80c5000a5b9ca1fec5902300ae4b440 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:17:39 2012 -0700 Fix a type-safety warning commit 1e52785f38146515409da3724f858b9603d19454 Author: James Lee <egypt@metasploit.com> Date: Mon Feb 27 15:21:36 2012 -0700 LHOST should be OptAddress, not OptAddressRange commit acef978aa4233c7bd0b00ef63646eb4da5457f67 Author: James Lee <egypt@metasploit.com> Date: Sun Feb 26 17:45:59 2012 -0700 Fix a couple of warnings and a typo commit 29d87f88790aa1b3e5db6df650ecfb3fb93c675b Author: HD Moore <hdm@digitaloffense.net> Date: Mon Feb 27 11:54:29 2012 -0600 Fix ctype vs content_type typo commit 83b5400356c47dd1973e6be3aa343084dfd09c73 Author: Gregory Man <man.gregory@gmail.com> Date: Sun Feb 26 15:38:33 2012 +0200 Fixed scripts/meterpreter/enum_firefox to work with firefox > 3.6.x commit 49c2c80b347820d02348d694cc71f1b3028b4365 Author: Steve Tornio <swtornio@gmail.com> Date: Sun Feb 26 07:13:13 2012 -0600 add osvdb ref commit e18e1fe97b89c3a2b8c22bc6c18726853d2c2bee Author: Matt Andreko <mandreko@gmail.com> Date: Sat Feb 25 18:02:56 2012 -0500 Added aspx target to msfvenom. This in turn added it to msfencode as well. Ref: https://github.com/rapid7/metasploit-framework/pull/188 Tested on winxp with IIS in .net 1.1 and 2.0 modes commit e6aa5072112d79bbf8a4d2289cf8d301db3932f5 Author: Joshua J. Drake <github.jdrake@qoop.org> Date: Sat Feb 25 13:00:48 2012 -0600 Fixes #6308: Fall back to 127.0.0.1 when SocketError is raised from the resolver commit b3371e8bfeea4d84f9d0cba100352b57d7e9e78b Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 17:07:42 2012 -0700 Simplify logic for whether an inner iface has the same address commit 5417419f35a40d1c08ca11ca40744722692d3b0d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:58:16 2012 -0700 Whitespace commit 9036875c2918439ae23e11ee7b958e30ccc29545 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:53:45 2012 -0700 Set session info before worrying about address get_interfaces can take a while on Linux, grab uid and hostname earlier so we can give the user an idea of what they popped as soon as possible. commit f34b51c6291031ab25b5bfb1ac6307a516ab0ee9 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:48:42 2012 -0700 Clean up rdoc commit e61a0663454400ec66f59a80d18b0baff4cb8cd9 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:54:45 2012 -0600 Ensure the architecture is only the first word (not the full WOW64 message in some cases) commit 4c701610976a92298c1182eecc9291a1b301e43b Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:49:17 2012 -0600 More paranoia code, just in case RHOST is set to whitespace commit c5ff89fe3dc9061e0fa9f761e6530f6571989d28 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:47:01 2012 -0600 A few more small bug fixes to handle cases with an empty string target host resulting in a bad address commit 462d0188a1298f29ac83b10349aec6737efc5b19 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 03:55:10 2012 -0600 Fix up the logic (reversed by accident) commit 2b2b0adaec2448423dbd3ec54d90a5721965e2df Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 23:29:52 2012 -0600 Automatically parse system information and populate the db, identify and report NAT when detected, show the real session_host in the sessions -l listing commit 547a4ab4c62dc3248f847dd5d305ad3b74157348 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:16:03 2012 -0600 Fix typo introduced commit 27a7b7961e61894bdecd55310a8f45d0917c5a5c Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:11:38 2012 -0600 More session.session_host tweaks commit e447302a1a9915795e89b5e29c89ff2ab9b6209b Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:08:20 2012 -0600 Additional tunnel_peer changes commit 93369fcffaf8c6b00d992526b4083acfce036bb3 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:06:21 2012 -0600 Additional changes to session.session_host commit c3552f66d158685909e2c8b51dfead7c240c4f40 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:00:19 2012 -0600 Merge changes into the new branch
2012-02-29 01:28:47 +00:00
if wspace.kind_of? String
wspace = find_workspace(wspace)
end
host = nil
report_host(:workspace => wspace, :address => addr)
host = get_host(:workspace => wspace, :address => addr)
desc = opts.delete(:desc)
summary = opts.delete(:summary)
detail = opts.delete(:detail)
crit = opts.delete(:crit)
possible_tag = Tag.find(:all,
:include => :hosts,
:conditions => ["hosts.workspace_id = ? and tags.name = ?",
wspace.id,
name
]
).first
tag = possible_tag || Tag.new
tag.name = name
tag.desc = desc
tag.report_summary = !!summary
tag.report_detail = !!detail
tag.critical = !!crit
tag.hosts = tag.hosts | [host]
tag.save! if tag.changed?
end
#
# Store a set of credentials in the database.
#
# 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 or Host object reference
# +:port+:: a port number
#
# opts can contain
# +:user+:: the username
# +:pass+:: the password, or path to ssh_key
# +:ptype+:: the type of password (password(ish), 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("Missing required option :port") if (opts[:port].nil? and opts[:service].nil?)
if (not opts[:host].kind_of?(Host)) and (not validate_ips(opts[:host]))
raise ArgumentError.new("Invalid address or object for :host (#{opts[:host].inspect})")
end
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)
duplicate_ok = opts.delete(:duplicate_ok)
# Nil is true for active.
active = (opts[:active] || opts[:active].nil?) ? true : false
wspace = opts.delete(:workspace) || workspace
# Service management; assume the user knows what
# he's talking about.
service = opts.delete(:service) || report_service(:host => host, :port => port, :proto => proto, :name => sname, :workspace => wspace)
# Non-US-ASCII usernames are tripping up the database at the moment, this is a temporary fix until we update the tables
( token[0] = token[0].gsub(/[\x00-\x1f\x7f-\xff]/){|m| "\\x%.2x" % m.unpack("C")[0] } ) if token[0]
( token[1] = token[1].gsub(/[\x00-\x1f\x7f-\xff]/){|m| "\\x%.2x" % m.unpack("C")[0] } ) if token[1]
ret = {}
#Check to see if the creds already exist. We look also for a downcased username with the
#same password because we can fairly safely assume they are not in fact two seperate creds.
#this allows us to hedge against duplication of creds in the DB.
if duplicate_ok
# If duplicate usernames are okay, find by both user and password (allows
# for actual duplicates to get modified updated_at, sources, etc)
if token[0].nil? or token[0].empty?
cred = service.creds.find_or_initialize_by_user_and_ptype_and_pass(token[0] || "", ptype, token[1] || "")
else
cred = service.creds.find_by_user_and_ptype_and_pass(token[0] || "", ptype, token[1] || "")
unless cred
dcu = token[0].downcase
cred = service.creds.find_by_user_and_ptype_and_pass( dcu || "", ptype, token[1] || "")
unless cred
cred = service.creds.find_or_initialize_by_user_and_ptype_and_pass(token[0] || "", ptype, token[1] || "")
end
end
end
else
# Create the cred by username only (so we can change passwords)
if token[0].nil? or token[0].empty?
cred = service.creds.find_or_initialize_by_user_and_ptype(token[0] || "", ptype)
else
cred = service.creds.find_by_user_and_ptype(token[0] || "", ptype)
unless cred
dcu = token[0].downcase
cred = service.creds.find_by_user_and_ptype_and_pass( dcu || "", ptype, token[1] || "")
unless cred
cred = service.creds.find_or_initialize_by_user_and_ptype(token[0] || "", ptype)
end
end
end
end
# 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
end
alias :report_cred :report_auth_info
alias :report_auth :report_auth_info
#
# Find or create a credential matching this type/data
#
def find_or_create_cred(opts)
report_auth_info(opts)
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)
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]
wspace = opts.delete(:workspace) || workspace
exploited_at = opts[:exploited_at] || opts["exploited_at"]
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
host = report_host({:workspace => wspace, :host => opts[:host]})
addr = normalize_host(opts[:host])
end
ret = {}
=begin
if host
host.updated_at = host.created_at
host.state = HostState::Alive
host.save!
else
host = get_host(:workspace => wspace, :address => addr)
end
=end
# Truncate the info field at the maximum field length
if info
info = info[0,65535]
end
# Truncate the name field at the maximum field length
name = name[0,255]
if info and name !~ /^NEXPOSE-/
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
vuln.info = info.to_s if info
vuln.exploited_at = exploited_at if exploited_at
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
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]
ref = Ref.find_or_initialize_by_name(opts[:name])
if ref and ref.changed?
ref.save!
end
ret[:ref] = ref
end
def get_ref(name)
Ref.find_by_name(name)
end
# report_exploit() used to be used to track sessions and which modules
# opened them. That information is now available with the session table
# directly. TODO: kill this completely some day -- for now just warn if
# some other UI is actually using it.
def report_exploit(opts={})
wlog("Deprecated method call: report_exploit()\n" +
"report_exploit() options: #{opts.inspect}\n" +
"report_exploit() call stack:\n\t#{caller.join("\n\t")}"
)
end
#
# Deletes a host and associated data matching this address/comm
#
def del_host(wspace, address, comm='')
2011-12-10 19:24:20 +00:00
address, scope = address.split('%', 2)
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)
2011-12-10 19:24:20 +00:00
address, scope = addr.split('%', 2)
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
return if not wspace # Temp fix?
uname = opts.delete(:username)
if ! opts[:host].kind_of? Host and opts[:host]
opts[:host] = report_host(:workspace => wspace, :host => opts[:host])
end
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)
end
def report_loot(opts)
return if not active
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
host = report_host({:workspace => wspace, :host => opts[:host]})
addr = normalize_host(opts[:host])
end
end
ret = {}
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]
=begin
if host
host.updated_at = host.created_at
host.state = HostState::Alive
host.save!
end
=end
end
ret[:loot] = loot
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)
end
def report_task(opts)
return if not active
wspace = opts.delete(:workspace) || workspace
path = opts.delete(:path) || (raise RuntimeError, "A task :path is required")
ret = {}
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
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)
end
def report_report(opts)
return if not active
wspace = opts.delete(:workspace) || workspace
path = opts.delete(:path) || (raise RuntimeError, "A report :path is required")
ret = {}
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
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
# +:info+:: if present, report the service with this info
#
# Duplicate records for a given host, port, vhost combination will be overwritten
#
def report_web_site(opts)
return if not active
wspace = opts.delete(:workspace) || workspace
vhost = opts.delete(:vhost)
addr = nil
port = nil
name = nil
serv = nil
info = 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'
info = opts[:info]
if not (addr and port)
raise ArgumentError, "report_web_site requires service OR host/port/ssl"
end
# Force addr to be the address and not hostname
addr = Rex::Socket.getaddress(addr, true)
end
ret = {}
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
end
# Add the info if it's there.
unless info.to_s.empty?
serv.info = info
end
serv.save! if serv.changed?
=begin
host.updated_at = host.created_at
host.state = HostState::Alive
host.save!
=end
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
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
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]
)
if not site
raise ArgumentError, "report_web_page was unable to create the associated web site"
end
end
ret = {}
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
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
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]
)
if not site
raise ArgumentError, "report_web_form was unable to create the associated web site"
end
end
ret = {}
# 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
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
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]
)
if not site
raise ArgumentError, "report_web_form was unable to create the associated web site"
end
end
ret = {}
meth = meth.to_s.upcase
vuln = WebVuln.find_or_initialize_by_web_site_id_and_path_and_method_and_pname_and_name_and_category_and_query(site[:id], path, meth, pname, name, 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
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)
if data.kind_of?(REXML::Document)
return data
else
# Make an attempt to recover from a REXML import fail, since
# it's better than dying outright.
begin
return REXML::Document.new(data)
rescue REXML::ParseException => e
dlog("REXML error: Badly formatted XML, attempting to recover. Error was: #{e.inspect}")
return REXML::Document.new(data.gsub(/([\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xff])/){ |x| "\\x%.2x" % x.unpack("C*")[0] })
end
end
end
# Handles timestamps from Metasploit Express/Pro 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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
case data[0,4]
when "PK\x03\x04"
data = Zip::ZipFile.open(filename)
when "\xd4\xc3\xb2\xa1", "\xa1\xb2\xc3\xd4"
data = PacketFu::PcapFile.new.readfile(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
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_scan_xml, :qualys_asset_xml, :msf_xml :nessus_nbe :amap_mlog
# :amap_log :ip_list, :msf_zip, :libpcap, :foundstone_xml, :acunetix_xml, :appscan_xml
# :burp_session, :ip360_xml_v3, :ip360_aspl_xml, :nikto_xml
# If there is no match, an error is raised instead.
def import_filetype_detect(data)
if data and data.kind_of? Zip::ZipFile
raise DBImportError.new("The zip file provided is empty.") if data.entries.empty?
@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}
begin
@import_filedata[:zip_xml] = @import_filedata[:zip_entry_names].grep(/^(.*)_[0-9]+\.xml$/).first || raise
@import_filedata[:zip_wspace] = @import_filedata[:zip_xml].to_s.match(/^(.*)_[0-9]+\.xml$/)[1]
@import_filedata[:type] = "Metasploit ZIP Report"
return :msf_zip
rescue ::Interrupt
raise $!
rescue ::Exception
raise DBImportError.new("The zip file provided is not a Metasploit ZIP report")
end
end
if data and data.kind_of? PacketFu::PcapFile
raise DBImportError.new("The pcap file provided is empty.") if data.body.empty?
@import_filedata ||= {}
@import_filedata[:type] = "Libpcap Packet Capture"
return :libpcap
end
# This is a text string, lets make sure its treated as binary
data = data.unpack("C*").pack("C*")
if data and data.to_s.strip.length == 0
raise DBImportError.new("The data provided to the import function was empty")
end
# Parse the first line or 4k of data from the file
di = data.index("\n") || 4096
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
2012-03-13 15:45:41 +00:00
elsif (firstline.index("Name,Manufacturer,Device Type,Model,IP Address,Serial Number,Location,Operating System"))
@import_filedata[:type] = "Spiceworks CSV Export"
return :spiceworks_csv
elsif (firstline.index("<scanJob>"))
@import_filedata[:type] = "Retina XML"
return :retina_xml
elsif (firstline.index("<NessusClientData>"))
@import_filedata[:type] = "Nessus XML (v1)"
return :nessus_xml
elsif (firstline.index("<SecScan ID="))
@import_filedata[:type] = "Microsoft Baseline Security Analyzer"
return :mbsa_xml
elsif (data[0,1024] =~ /<!ATTLIST\s+items\s+burpVersion/)
@import_filedata[:type] = "Burp Session XML"
return :burp_session_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 "niktoscan"
@import_filedata[:type] = "Nikto XML"
return :nikto_xml
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 Scan XML"
return :qualys_scan_xml
when "ASSET_DATA_REPORT"
@import_filedata[:type] = "Qualys Asset XML"
return :qualys_asset_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
when /audits?/ # <audit> and <audits> are both valid for nCircle. wtfmate.
@import_filedata[:type] = "IP360 XML v3"
return :ip360_xml_v3
when /ontology/
@import_filedata[:type] = "IP360 ASPL"
return :ip360_aspl_xml
when /ReportInfo/
@import_filedata[:type] = "Foundstone"
return :foundstone_xml
when /ScanGroup/
@import_filedata[:type] = "Acunetix"
return :acunetix_xml
when /AppScanInfo/ # Actually the second line
@import_filedata[:type] = "Appscan"
return :appscan_xml
when "entities"
if line =~ /creator.*\x43\x4f\x52\x45\x20\x49\x4d\x50\x41\x43\x54/i
@import_filedata[:type] = "CI"
return :ci_xml
end
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 ipv46_validator(firstline)
# 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
elsif (firstline.index("# Metasploit PWDump Export"))
# then it's a Metasploit PWDump export
@import_filedata[:type] = "msf_pwdump"
return :msf_pwdump
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
#
# Imports Nikto scan data from -Format xml as notes.
#
def import_nikto_xml(args={}, &block)
data = args[:data]
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
doc = rexmlify(data)
doc.elements.each do |f|
f.elements.each('scandetails') do |host|
# Get host information
addr = host.attributes['targetip']
next if not addr
if bl.include? addr
next
else
yield(:address,addr) if block
end
# Get service information
port = host.attributes['targetport']
next if port.to_i == 0
uri = URI.parse(host.attributes['sitename']) rescue nil
next unless uri and uri.scheme
2011-11-20 01:10:08 +00:00
# Collect and report scan descriptions.
host.elements.each do |item|
if item.elements['description']
desc_text = item.elements['description'].text
next if desc_text.nil? or desc_text.empty?
desc_data = {
:workspace => wspace,
:host => addr,
:type => "service.nikto.scan.description",
:data => desc_text,
:proto => "tcp",
:port => port.to_i,
:sname => uri.scheme,
:update => :unique_data
}
# Always report it as a note.
report_note(desc_data)
# Sometimes report it as a vuln, too.
# XXX: There's a Vuln.info field but nothing reads from it? See Bug #5837
if item.attributes['osvdbid'].to_i != 0
desc_data[:refs] = ["OSVDB-#{item.attributes['osvdbid']}"]
desc_data[:name] = "NIKTO-#{item.attributes['id']}"
2011-11-20 01:10:08 +00:00
desc_data.delete(:data)
desc_data.delete(:type)
desc_data.delete(:update)
report_vuln(desc_data)
end
end
2011-11-20 01:10:08 +00:00
end
end
end
2011-11-20 01:10:08 +00:00
end
def import_libpcap_file(args={})
filename = args[:filename]
wspace = args[:wspace] || workspace
data = PacketFu::PcapFile.new.readfile(filename)
import_libpcap(args.merge(:data => data))
end
# The libpcap file format is handled by PacketFu for data
# extraction. TODO: Make this its own mixin, and possibly
# extend PacketFu to do better stream analysis on the fly.
def import_libpcap(args={}, &block)
data = args[:data]
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
# seen_hosts is only used for determining when to yield an address. Once we get
# some packet analysis going, the values will have all sorts of info. The plan
# is to ru through all the packets as a first pass and report host and service,
# then, once we have everything parsed, we can reconstruct sessions and ngrep
# out things like authentication sequences, examine ttl's and window sizes, all
# kinds of crazy awesome stuff like that.
seen_hosts = {}
decoded_packets = 0
last_count = 0
data.body.map {|p| p.data}.each do |p|
if (decoded_packets >= last_count + 1000) and block
yield(:pcap_count, decoded_packets)
last_count = decoded_packets
end
decoded_packets += 1
pkt = PacketFu::Packet.parse(p) rescue next # Just silently skip bad packets
next unless pkt.is_ip? # Skip anything that's not IP. Technically, not Ethernet::Ip
next if pkt.is_tcp? && (pkt.tcp_src == 0 || pkt.tcp_dst == 0) # Skip port 0
next if pkt.is_udp? && (pkt.udp_src == 0 || pkt.udp_dst == 0) # Skip port 0
saddr = pkt.ip_saddr
daddr = pkt.ip_daddr
# Handle blacklists and obviously useless IP addresses, and report the host.
next if (bl | [saddr,daddr]).size == bl.size # Both hosts are blacklisted, skip everything.
unless( bl.include?(saddr) || rfc3330_reserved(saddr))
yield(:address,saddr) if block and !seen_hosts.keys.include?(saddr)
report_host(:workspace => wspace, :host => saddr, :state => Msf::HostState::Alive) unless seen_hosts[saddr]
seen_hosts[saddr] ||= []
end
unless( bl.include?(daddr) || rfc3330_reserved(daddr))
yield(:address,daddr) if block and !seen_hosts.keys.include?(daddr)
report_host(:workspace => wspace, :host => daddr, :state => Msf::HostState::Alive) unless seen_hosts[daddr]
seen_hosts[daddr] ||= []
end
if pkt.is_tcp? # First pass on TCP packets
if (pkt.tcp_flags.syn == 1 and pkt.tcp_flags.ack == 1) or # Oh, this kills me
pkt.tcp_src < 1024 # If it's a low port, assume it's a proper service.
if seen_hosts[saddr]
unless seen_hosts[saddr].include? [pkt.tcp_src,"tcp"]
report_service(
:workspace => wspace, :host => saddr,
:proto => "tcp", :port => pkt.tcp_src,
:state => Msf::ServiceState::Open
)
seen_hosts[saddr] << [pkt.tcp_src,"tcp"]
yield(:service,"%s:%d/%s" % [saddr,pkt.tcp_src,"tcp"])
end
end
end
elsif pkt.is_udp? # First pass on UDP packets
if pkt.udp_src == pkt.udp_dst # Very basic p2p detection.
[saddr,daddr].each do |xaddr|
if seen_hosts[xaddr]
unless seen_hosts[xaddr].include? [pkt.udp_src,"udp"]
report_service(
:workspace => wspace, :host => xaddr,
:proto => "udp", :port => pkt.udp_src,
:state => Msf::ServiceState::Open
)
seen_hosts[xaddr] << [pkt.udp_src,"udp"]
yield(:service,"%s:%d/%s" % [xaddr,pkt.udp_src,"udp"])
end
end
end
elsif pkt.udp_src < 1024 # Probably a service
if seen_hosts[saddr]
unless seen_hosts[saddr].include? [pkt.udp_src,"udp"]
report_service(
:workspace => wspace, :host => saddr,
:proto => "udp", :port => pkt.udp_src,
:state => Msf::ServiceState::Open
)
seen_hosts[saddr] << [pkt.udp_src,"udp"]
yield(:service,"%s:%d/%s" % [saddr,pkt.udp_src,"udp"])
end
end
end
end # tcp or udp
inspect_single_packet(pkt,wspace)
end # data.body.map
# Right about here, we should have built up some streams for some stream analysis.
# Not sure what form that will take, but people like shoving many hundreds of
# thousands of packets through this thing, so it'll need to be memory efficient.
end
# Do all the single packet analysis we can while churning through the pcap
# the first time. Multiple packet inspection will come later, where we can
# do stream analysis, compare requests and responses, etc.
def inspect_single_packet(pkt,wspace)
if pkt.is_tcp? or pkt.is_udp?
inspect_single_packet_http(pkt,wspace)
end
end
# Checks for packets that are headed towards port 80, are tcp, contain an HTTP/1.0
# line, contains an Authorization line, contains a b64-encoded credential, and
# extracts it. Reports this credential and solidifies the service as HTTP.
def inspect_single_packet_http(pkt,wspace)
# First, check the server side (data from port 80).
if pkt.is_tcp? and pkt.tcp_src == 80 and !pkt.payload.nil? and !pkt.payload.empty?
if pkt.payload =~ /^HTTP\x2f1\x2e[01]/
http_server_match = pkt.payload.match(/\nServer:\s+([^\r\n]+)[\r\n]/)
if http_server_match.kind_of?(MatchData) and http_server_match[1]
report_service(
:workspace => wspace,
:host => pkt.ip_saddr,
:port => pkt.tcp_src,
:proto => "tcp",
:name => "http",
:info => http_server_match[1],
:state => Msf::ServiceState::Open
)
# That's all we want to know from this service.
return :something_significant
end
end
end
# Next, check the client side (data to port 80)
if pkt.is_tcp? and pkt.tcp_dst == 80 and !pkt.payload.nil? and !pkt.payload.empty?
if pkt.payload.match(/[\x00-\x20]HTTP\x2f1\x2e[10]/)
auth_match = pkt.payload.match(/\nAuthorization:\s+Basic\s+([A-Za-z0-9=\x2b]+)/)
if auth_match.kind_of?(MatchData) and auth_match[1]
b64_cred = auth_match[1]
else
return false
end
# If we're this far, we can surmise that at least the client is a web browser,
# he thinks the server is HTTP and he just made an authentication attempt. At
# this point, we'll just believe everything the packet says -- validation ought
# to come later.
user,pass = b64_cred.unpack("m*").first.split(/:/,2)
report_service(
:workspace => wspace,
:host => pkt.ip_daddr,
:port => pkt.tcp_dst,
:proto => "tcp",
:name => "http"
)
report_auth_info(
:workspace => wspace,
:host => pkt.ip_daddr,
:port => pkt.tcp_dst,
:proto => "tcp",
:type => "password",
:active => true, # Once we can build a stream, determine if the auth was successful. For now, assume it is.
:user => user,
:pass => pass
)
# That's all we want to know from this service.
return :something_significant
end
end
end
2012-03-13 15:45:41 +00:00
def import_spiceworks_csv(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 =~ /^Name,Manufacturer,Device/ #header
split = line.split(',')
name = split[0]
manufacturer = split[1]
device = split[2]
model = split[3]
ip = split[4]
serialno = split[5]
location = split[6]
os = split[7]
conf = {
:workspace => wspace,
:host => ip,
:os_name => os,
:name => name
}
host = report_host(conf)
report_import_note(wspace, host)
end
end
#
# Metasploit PWDump Export
#
# This file format is generated by the db_export -f pwdump and
# the Metasploit Express and Pro report types of "PWDump."
#
# This particular block scheme is temporary, since someone is
# bound to want to import gigantic lists, so we'll want a
# stream parser eventually (just like the other non-nmap formats).
#
# The file format is:
# # 1.2.3.4:23/tcp (telnet)
# username password
# user2 p\x01a\x02ss2
# <BLANK> pass3
# user3 <BLANK>
# smbuser:sid:lmhash:nthash:::
#
# Note the leading hash for the host:port line. Note also all usernames
# and passwords must be in 7-bit ASCII (character sequences of "\x01"
# will be interpolated -- this includes spaces, which must be notated
# as "\x20". Blank usernames or passwords should be <BLANK>.
#
def import_msf_pwdump(args={}, &block)
data = args[:data]
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
last_host = nil
addr = nil
port = nil
proto = nil
sname = nil
ptype = nil
active = false # Are there cases where imported creds are good? I just hate trusting the import right away.
data.each_line do |line|
case line
when /^[\s]*#/ # Comment lines
if line[/^#[\s]*([0-9.]+):([0-9]+)(\x2f(tcp|udp))?[\s]*(\x28([^\x29]*)\x29)?/]
addr = $1
port = $2
proto = $4
sname = $6
end
when /^[\s]*Warning:/
next # Discard warning messages.
when /^[\s]*([^\s:]+):[0-9]+:([A-Fa-f0-9]+:[A-Fa-f0-9]+):[^\s]*$/ # SMB Hash
user = ([nil, "<BLANK>"].include?($1)) ? "" : $1
pass = ([nil, "<BLANK>"].include?($2)) ? "" : $2
ptype = "smb_hash"
when /^[\s]*([^\s:]+):([0-9]+):NO PASSWORD\*+:NO PASSWORD\*+[^\s]*$/ # SMB Hash
user = ([nil, "<BLANK>"].include?($1)) ? "" : $1
pass = ""
ptype = "smb_hash"
when /^[\s]*([\x21-\x7f]+)[\s]+([\x21-\x7f]+)?/ # Must be a user pass
user = ([nil, "<BLANK>"].include?($1)) ? "" : dehex($1)
pass = ([nil, "<BLANK>"].include?($2)) ? "" : dehex($2)
ptype = "password"
else # Some unknown line not broken by a space.
next
end
next unless [addr,port,user,pass].compact.size == 4
next unless ipv46_validator(addr) # Skip Malformed addrs
next unless port[/^[0-9]+$/] # Skip malformed ports
if bl.include? addr
next
else
yield(:address,addr) if block and addr != last_host
last_host = addr
end
cred_info = {
:host => addr,
:port => port,
:user => user,
:pass => pass,
:type => ptype,
:workspace => wspace
}
cred_info[:proto] = proto if proto
cred_info[:sname] = sname if sname
cred_info[:active] = active
report_auth_info(cred_info)
user = pass = ptype = nil
end
end
# If hex notation is present, turn them into a character.
def dehex(str)
hexen = str.scan(/\x5cx[0-9a-fA-F]{2}/)
hexen.each { |h|
str.gsub!(h,h[2,2].to_i(16).chr)
}
return str
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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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","imp_#{Rex::Text::rand_text_alphanumeric(4)}",@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
# Grab the list of unique basedirs over all entries.
@import_filedata[:zip_tmp_subdirs] = @import_filedata[:zip_entry_names].map {|x| ::File.split(x)}.map {|x| x[0]}.uniq.reject {|x| x == "."}
# mkdir all of the base directores we just pulled out, if they don't
# already exist
@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))
# if it exists but we can't write to it, give up
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$/
target_data = ::File.open(target) {|f| f.read 1024}
if import_filetype_detect(target_data) == :msf_xml
@import_filedata[:zip_extracted_xml] = target
#break
end
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[: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
hobj = report_host(host_data)
host.elements.each('services/service') do |service|
service_data = {}
service_data[:workspace] = wspace
service_data[:host] = hobj
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] = hobj
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('tags/tag') do |tag|
tag_data = {}
tag_data[:addr] = host_address
tag_data[:wspace] = wspace
tag_data[:name] = tag.elements["name"].text.to_s.strip
tag_data[:desc] = tag.elements["desc"].text.to_s.strip
if tag.elements["report-summary"].text
tag_data[:summary] = tag.elements["report-summary"].text.to_s.strip
end
if tag.elements["report-detail"].text
tag_data[:detail] = tag.elements["report-detail"].text.to_s.strip
end
if tag.elements["critical"].text
tag_data[:crit] = true unless tag.elements["critical"].text.to_s.strip == "NULL"
end
report_host_tag(tag_data)
end
host.elements.each('vulns/vuln') do |vuln|
vuln_data = {}
vuln_data[:workspace] = wspace
vuln_data[:host] = hobj
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 exploited-at}.each { |datum|
if vuln.elements[datum] and vuln.elements[datum].text
vuln_data[datum.gsub("-","_")] = nils_for_nulls(vuln.elements[datum].text.to_s.strip)
end
}
if vuln.elements["refs"]
vuln_data[:refs] = []
vuln.elements.each("refs/ref") do |ref|
vuln_data[:refs] << nils_for_nulls(ref.text.to_s.strip)
end
end
report_vuln(vuln_data)
end
host.elements.each('creds/cred') do |cred|
cred_data = {}
cred_data[:workspace] = wspace
cred_data[:host] = hobj
%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
}
2011-11-11 02:59:30 +00:00
%W{source-type source-id}.each { |datum|
if cred.elements[datum].respond_to? :text
cred_data[datum.gsub("-","_").intern] = 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)
end
host.elements.each('sessions/session') do |sess|
sess_id = nils_for_nulls(sess.elements["id"].text.to_s.strip.to_i)
sess_data = {}
sess_data[:host] = hobj
%W{desc platform port stype}.each {|datum|
if sess.elements[datum].respond_to? :text
sess_data[datum.intern] = nils_for_nulls(sess.elements[datum].text.to_s.strip)
end
}
%W{opened-at close-reason closed-at via-exploit via-payload}.each {|datum|
if sess.elements[datum].respond_to? :text
sess_data[datum.gsub("-","_").intern] = nils_for_nulls(sess.elements[datum].text.to_s.strip)
end
}
sess_data[:datastore] = nils_for_nulls(unserialize_object(sess.elements["datastore"], allow_yaml))
if sess.elements["routes"]
sess_data[:routes] = nils_for_nulls(unserialize_object(sess.elements["routes"], allow_yaml)) || []
end
if not sess_data[:closed_at] # Fake a close if we don't already have one
sess_data[:closed_at] = Time.now.utc
sess_data[:close_reason] = "Imported at #{Time.now.utc}"
end
existing_session = get_session(
:workspace => sess_data[:host].workspace,
:addr => sess_data[:host].address,
:time => sess_data[:opened_at]
)
this_session = existing_session || report_session(sess_data)
next if existing_session
sess.elements.each('events/event') do |sess_event|
sess_event_data = {}
sess_event_data[:session] = this_session
%W{created-at etype local-path remote-path}.each {|datum|
if sess_event.elements[datum].respond_to? :text
sess_event_data[datum.gsub("-","_").intern] = nils_for_nulls(sess_event.elements[datum].text.to_s.strip)
end
}
%W{command output}.each {|datum|
if sess_event.elements[datum].respond_to? :text
sess_event_data[datum.gsub("-","_").intern] = nils_for_nulls(unserialize_object(sess_event.elements[datum], allow_yaml))
end
}
report_session_event(sess_event_data)
end
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)
yield(:web_site, "#{info[:host]}:#{info[:port]} (#{info[:vhost]})") if block
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)
yield("web_#{wtype}".intern, info[:path]) if block
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)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_nexpose_noko_stream(noko_args) {|type, data| yield type,data}
else
import_nexpose_noko_stream(noko_args)
end
return true
end
data = args[:data]
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
}
host = report_host(conf)
report_import_note(wspace, host)
report_note(
:workspace => wspace,
:host => host,
: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 => host,
: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 => host, :proto => sprot, :port => sport, :name => name, :info => info)
else
report_service(:workspace => wspace, :host => host, :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 => host,
: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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
import_nexpose_rawxml(args.merge(:data => data))
end
def import_nexpose_rawxml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_nexpose_raw_noko_stream(noko_args) {|type, data| yield type,data}
else
import_nexpose_raw_noko_stream(noko_args)
end
return true
end
data = args[:data]
# 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
# XXX: Blacklist should be checked here instead of saving a
# host we're just going to throw away later
hosts.push(value)
when :vuln
value["id"] = value["id"].downcase if value["id"]
vulns.push(value)
end
}
REXML::Document.parse_stream(data, parser)
vuln_refs = nexpose_refs_to_struct(vulns)
hosts.each do |host|
if bl.include? host["addr"]
next
else
yield(:address,host["addr"]) if block
end
nexpose_host_from_rawxml(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 struct, containing :id, :refs, :title, and :severity
#
# Other attributes can be added later, as needed.
def nexpose_refs_to_struct(vulns)
ret = []
vulns.each do |vuln|
next if ret.map {|v| v.id}.include? vuln["id"]
vstruct = Struct.new(:id, :refs, :title, :severity).new
vstruct.id = vuln["id"]
vstruct.title = vuln["title"]
vstruct.severity = vuln["severity"]
vstruct.refs = []
vuln["refs"].each do |ref|
if ref['source'] == 'BID'
vstruct.refs.push('BID-' + ref["value"])
elsif ref['source'] == 'CVE'
# value is CVE-$ID
vstruct.refs.push(ref["value"])
elsif ref['source'] == 'MS'
vstruct.refs.push('MSB-' + ref["value"])
elsif ref['source'] == 'URL'
vstruct.refs.push('URL-' + ref["value"])
#else
# $stdout.puts("Unknown source: #{ref["source"]}")
end
end
ret.push vstruct
end
return ret
end
# Takes a Host object, an array of vuln structs (generated by nexpose_refs_to_struct()),
# and a workspace, and reports the vulns on that host.
def nexpose_host_from_rawxml(h, vstructs, wspace)
hobj = nil
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)
hobj = report_host(data)
report_import_note(wspace, hobj)
end
if h["notes"]
note = {
:workspace => wspace,
:host => (hobj || addr),
:type => "host.vuln.nexpose_keys",
:data => {},
:mode => :unique_data
}
h["notes"].each do |v,k|
note[:data][v] ||= []
next if note[:data][v].include? k
note[:data][v] << k
end
report_note(note)
end
if h["os_family"]
note = {
:workspace => wspace,
:host => hobj || 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][:version] = h["os_version"] if h["os_version"]
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] = hobj || 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/
vstruct = vstructs.select {|vs| vs.id.to_s.downcase == v["id"].to_s.downcase}.first
next unless vstruct
data = {}
data[:workspace] = wspace
data[:host] = hobj || addr
data[:proto] = v["protocol"].downcase if v["protocol"]
data[:port] = v["port"].to_i if v["port"]
data[:name] = "NEXPOSE-" + v["id"]
data[:info] = vstruct.title
data[:refs] = vstruct.refs
report_vuln(data)
}
end
#
# Retina XML
#
# Process a Retina XML file
def import_retina_xml_file(args={})
filename = args[:filename]
wspace = args[:wspace] || workspace
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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 : []
msg = "Warning: The Retina XML format does not associate vulnerabilities with the\n"
msg << "specific service on which they were found.\n"
msg << "This makes it impossible to correlate exploits to discovered vulnerabilities\n"
msg << "in a reliable fashion."
yield(:warning,msg) if block
parser = Rex::Parser::RetinaXMLStreamParser.new
parser.on_found_host = Proc.new do |host|
hobj = nil
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
hobj = report_host(data)
report_import_note(wspace, hobj)
# 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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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, addr) 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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
import_nmap_xml(args.merge(:data => data))
end
def import_nexpose_raw_noko_stream(args, &block)
if block
doc = Rex::Parser::NexposeRawDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::NexposeRawDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def import_nexpose_noko_stream(args, &block)
if block
doc = Rex::Parser::NexposeSimpleDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::NexposeSimpleDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def import_nmap_noko_stream(args, &block)
if block
doc = Rex::Parser::NmapDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::NmapDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
# If you have Nokogiri installed, you'll be shunted over to
# that. Otherwise, you'll hit the old NmapXMLStreamParser.
def import_nmap_xml(args={}, &block)
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
if Rex::Parser.nokogiri_loaded
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, "Nokogiri v#{::Nokogiri::VERSION}")
import_nmap_noko_stream(noko_args) {|type, data| yield type,data }
else
import_nmap_noko_stream(noko_args)
end
return true
end
# XXX: Legacy nmap xml parser starts here.
fix_services = args[:fix_services]
data = args[:data]
# 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
yield(:parser, parser.class.name) if block
# 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|
hobj = nil
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
hobj = report_host(data)
report_import_note(wspace,hobj)
end
end
if( h["os_vendor"] )
note = {
:workspace => wspace,
:host => hobj || 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 => hobj || addr,
:type => 'host.last_boot',
:data => {
:time => h["last_boot"]
}
)
end
if (h["trace"])
hops = []
h["trace"]["hops"].each do |hop|
hops << {
"ttl" => hop["ttl"].to_i,
"address" => hop["ipaddr"].to_s,
"rtt" => hop["rtt"].to_f,
"name" => hop["host"].to_s
}
end
report_note(
:workspace => wspace,
:host => hobj || addr,
:type => 'host.nmap.traceroute',
:data => {
'port' => h["trace"]["port"].to_i,
'proto' => h["trace"]["proto"].to_s,
'hops' => hops
}
)
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] = hobj || addr
data[:info] = extra if not extra.empty?
if p["name"] != "unknown"
data[:name] = p["name"]
end
report_service(data)
}
#Parse the scripts output
if h["scripts"]
h["scripts"].each do |key,val|
if key == "smb-check-vulns"
if val =~ /MS08-067: VULNERABLE/
vuln_info = {
:workspace => wspace,
:host => hobj || addr,
:port => 445,
:proto => 'tcp',
:name => 'MS08-067',
:info => 'Microsoft Windows Server Service Crafted RPC Request Handling Unspecified Remote Code Execution',
:refs =>['CVE-2008-4250',
'BID-31874',
'OSVDB-49243',
'CWE-94',
'MSFT-MS08-067',
'MSF-Microsoft Server Service Relative Path Stack Corruption',
'NSS-34476']
}
report_vuln(vuln_info)
end
if val =~ /MS06-025: VULNERABLE/
vuln_info = {
:workspace => wspace,
:host => hobj || addr,
:port => 445,
:proto => 'tcp',
:name => 'MS06-025',
:info => 'Vulnerability in Routing and Remote Access Could Allow Remote Code Execution',
:refs =>['CVE-2006-2370',
'CVE-2006-2371',
'BID-18325',
'BID-18358',
'BID-18424',
'OSVDB-26436',
'OSVDB-26437',
'MSFT-MS06-025',
'MSF-Microsoft RRAS Service RASMAN Registry Overflow',
'NSS-21689']
}
report_vuln(vuln_info)
end
# This one has NOT been Tested , remove this comment if confirmed working
if val =~ /MS07-029: VULNERABLE/
vuln_info = {
:workspace => wspace,
:host => hobj || addr,
:port => 445,
:proto => 'tcp',
:name => 'MS07-029',
:info => 'Vulnerability in Windows DNS RPC Interface Could Allow Remote Code Execution',
# Add more refs based on nessus/nexpose .. results
:refs =>['CVE-2007-1748',
'OSVDB-34100',
'MSF-Microsoft DNS RPC Service extractQuotedChar()',
'NSS-25168']
}
report_vuln(vuln_info)
end
end
end
end
}
# XXX: Legacy nmap xml parser ends here.
REXML::Document.parse_stream(data, parser)
end
# This is starting to be more than just nmap -> msf, other
# things are creeping in here. Consider renaming the method
# and intentionally making it more general.
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"
when "oracle-tns"; "oracle"
2011-11-20 01:10:08 +00:00
when "quickbooksrds"; "metasploit"
when /^dns-(udp|tcp)$/; "dns"
when /^dce[\s+]rpc$/; "dcerpc"
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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
import_nessus_nbe(args.merge(:data => data))
end
# There is no place the NBE actually stores the plugin name used to
# scan. You get "Security Note" or "Security Warning," and that's it.
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 = {}
# Cache host objects before passing into handle_nessus()
hobj_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 ipv46_validator(addr)
if bl.include? addr
next
else
yield(:address,addr) if block
end
hobj_map[ addr ] ||= report_host(:host => addr, :workspace => wspace)
# 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 => hobj_map[ addr ],
:type => 'host.os.nessus_fingerprint',
:data => {
:os => os.to_s.strip
}
)
end
next if nasl.to_s.strip.empty?
plugin_name = nil # NBE doesn't ever populate this
handle_nessus(wspace, hobj_map[ addr ], port, nasl, plugin_name, 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 IP360 XML v3 output
#
def import_ip360_xml_file(args={})
filename = args[:filename]
wspace = args[:wspace] || workspace
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
import_ip360_xml_v3(args.merge(:data => data))
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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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|
hobj = nil
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 ipv46_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
hobj = report_host(hinfo)
report_import_note(wspace,hobj)
# Record the OS
os ||= host.elements["os_name"]
if os
report_note(
:workspace => wspace,
:host => hobj,
:type => 'host.os.nessus_fingerprint',
:data => {
:os => os.text.to_s.strip
}
)
end
host.elements.each('ReportItem') do |item|
nasl = item.elements['pluginID'].text
plugin_name = item.elements['pluginName'].text
port = item.elements['port'].text
data = item.elements['data'].text
severity = item.elements['severity'].text
handle_nessus(wspace, hobj, port, nasl, plugin_name, 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 : []
#@host = {
#'hname' => nil,
#'addr' => nil,
#'mac' => nil,
#'os' => nil,
#'ports' => [ 'port' => { 'port' => nil,
# 'svc_name' => nil,
# 'proto' => nil,
# 'severity' => nil,
# 'nasl' => nil,
# 'description' => nil,
# 'cve' => [],
# 'bid' => [],
# 'xref' => []
# }
# ]
#}
parser = Rex::Parser::NessusXMLStreamParser.new
parser.on_found_host = Proc.new { |host|
hobj = nil
addr = host['addr'] || host['hname']
next unless ipv46_validator(addr) # Catches SCAN-ERROR, among others.
if bl.include? addr
next
else
yield(:address,addr) if block
end
os = host['os']
hname = host['hname']
mac = host['mac']
host_info = {
:workspace => wspace,
:host => addr,
}
host_info[:name] = hname.to_s.strip if hname
# Short mac, protect against Nessus's habit of saving multiple macs
# We can't use them anyway, so take just the first.
host_info[:mac] = mac.to_s.strip.upcase.split(/\s+/).first if mac
hobj = report_host(host_info)
report_import_note(wspace,hobj)
os = host['os']
yield(:os,os) if block
if os
report_note(
:workspace => wspace,
:host => hobj,
:type => 'host.os.nessus_fingerprint',
:data => {
:os => os.to_s.strip
}
)
end
host['ports'].each do |item|
next if item['port'] == 0
msf = nil
nasl = item['nasl'].to_s
nasl_name = item['nasl_name'].to_s
port = item['port'].to_s
proto = item['proto'] || "tcp"
sname = item['svc_name']
severity = item['severity']
description = item['description']
cve = item['cve']
bid = item['bid']
xref = item['xref']
msf = item['msf']
yield(:port,port) if block
handle_nessus_v2(wspace, hobj, port, proto, sname, nasl, nasl_name, severity, description, cve, bid, xref, msf)
end
yield(:end,hname) if block
}
REXML::Document.parse_stream(data, parser)
end
def import_mbsa_xml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_mbsa_noko_stream(noko_args) {|type, data| yield type,data}
else
import_mbsa_noko_stream(noko_args)
end
return true
else # Sorry
raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.")
end
end
def import_mbsa_noko_stream(args={},&block)
if block
doc = Rex::Parser::MbsaDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::MbsaDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def import_foundstone_xml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_foundstone_noko_stream(noko_args) {|type, data| yield type,data}
else
import_foundstone_noko_stream(noko_args)
end
return true
else # Sorry
raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.")
end
end
def import_foundstone_noko_stream(args={},&block)
if block
doc = Rex::Parser::FoundstoneDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::FoundstoneDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def import_acunetix_xml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_acunetix_noko_stream(noko_args) {|type, data| yield type,data}
else
import_acunetix_noko_stream(noko_args)
end
return true
else # Sorry
raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.")
end
end
def import_ci_xml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_ci_noko_stream(noko_args) {|type, data| yield type,data}
else
import_ci_noko_stream(noko_args)
end
return true
else # Sorry
raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.")
end
end
def import_acunetix_noko_stream(args={},&block)
if block
doc = Rex::Parser::AcunetixDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::AcunetixFoundstoneDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def import_appscan_xml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_appscan_noko_stream(noko_args) {|type, data| yield type,data}
else
import_appscan_noko_stream(noko_args)
end
return true
else # Sorry
raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.")
end
end
def import_appscan_noko_stream(args={},&block)
if block
doc = Rex::Parser::AppscanDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::AppscanDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def import_burp_session_xml(args={}, &block)
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
wspace = args[:wspace] || workspace
if Rex::Parser.nokogiri_loaded
# Rex::Parser.reload("burp_session_nokogiri.rb")
parser = "Nokogiri v#{::Nokogiri::VERSION}"
noko_args = args.dup
noko_args[:blacklist] = bl
noko_args[:wspace] = wspace
if block
yield(:parser, parser)
import_burp_session_noko_stream(noko_args) {|type, data| yield type,data}
else
import_burp_session_noko_stream(noko_args)
end
return true
else # Sorry
raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.")
end
end
def import_burp_session_noko_stream(args={},&block)
if block
doc = Rex::Parser::BurpSessionDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::BurpSessionDocument.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
#
# Import IP360's ASPL database
#
def import_ip360_aspl_xml(args={}, &block)
data = args[:data]
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
if not data.index("<ontology")
raise DBImportError.new("The ASPL file does not appear to be valid or may still be compressed")
end
base = ::File.join(Msf::Config.config_directory, "data", "ncircle")
::FileUtils.mkdir_p(base)
::File.open(::File.join(base, "ip360.aspl"), "w") do |fd|
fd.write(data)
end
yield(:notice, "Saved the IP360 ASPL database to #{base}...")
end
#
# Import IP360's xml output
#
def import_ip360_xml_v3(args={}, &block)
data = args[:data]
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
# @aspl = {'vulns' => {'name' => { }, 'cve' => { }, 'bid' => { } }
# 'oses' => {'name' } }
aspl_path = nil
aspl_paths = [
::File.join(Msf::Config.config_directory, "data", "ncircle", "ip360.aspl"),
::File.join(Msf::Config.data_directory, "ncircle", "ip360.aspl")
]
aspl_paths.each do |tpath|
next if not (::File.exist?(tpath) and ::File.readable?(tpath))
aspl_path = tpath
break
end
if not aspl_path
raise DBImportError.new("The nCircle IP360 ASPL file is not present.\n Download ASPL from nCircle VNE | Administer | Support | Resources, unzip it, and import it first")
end
# parse nCircle ASPL file
aspl = ""
::File.open(aspl_path, "rb") do |f|
aspl = f.read(f.stat.size)
end
@asplhash = nil
parser = Rex::Parser::IP360ASPLXMLStreamParser.new
parser.on_found_aspl = Proc.new { |asplh|
@asplhash = asplh
}
REXML::Document.parse_stream(aspl, parser)
#@host = {'hname' => nil, 'addr' => nil, 'mac' => nil, 'os' => nil, 'hid' => nil,
# 'vulns' => ['vuln' => {'vulnid' => nil, 'port' => nil, 'proto' => nil } ],
# 'apps' => ['app' => {'appid' => nil, 'svcid' => nil, 'port' => nil, 'proto' => nil } ],
# 'shares' => []
# }
# nCircle has some quotes escaped which causes the parser to break
# we don't need these lines so just replace \" with "
data.gsub!(/\\"/,'"')
# parse nCircle Scan Output
parser = Rex::Parser::IP360XMLStreamParser.new
parser.on_found_host = Proc.new { |host|
hobj = nil
addr = host['addr'] || host['hname']
next unless ipv46_validator(addr) # Catches SCAN-ERROR, among others.
if bl.include? addr
next
else
yield(:address,addr) if block
end
os = host['os']
hname = host['hname']
mac = host['mac']
host_hash = {
:workspace => wspace,
:host => addr,
}
host_hash[:name] = hname.to_s.strip if hname
host_hash[:mac] = mac.to_s.strip.upcase if mac
hobj = report_host(host_hash)
yield(:os, os) if block
if os
report_note(
:workspace => wspace,
:host => hobj,
:type => 'host.os.ip360_fingerprint',
:data => {
:os => @asplhash['oses'][os].to_s.strip
}
)
end
host['apps'].each do |item|
port = item['port'].to_s
proto = item['proto'].to_s
handle_ip360_v3_svc(wspace, hobj, port, proto, hname)
end
host['vulns'].each do |item|
vulnid = item['vulnid'].to_s
port = item['port'].to_s
proto = item['proto'] || "tcp"
vulnname = @asplhash['vulns']['name'][vulnid]
cves = @asplhash['vulns']['cve'][vulnid]
bids = @asplhash['vulns']['bid'][vulnid]
yield(:port, port) if block
handle_ip360_v3_vuln(wspace, hobj, port, proto, hname, vulnid, vulnname, cves, bids)
end
yield(:end, hname) if block
}
REXML::Document.parse_stream(data, parser)
end
def find_qualys_asset_vuln_refs(doc)
vuln_refs = {}
doc.elements.each("/ASSET_DATA_REPORT/GLOSSARY/VULN_DETAILS_LIST/VULN_DETAILS") do |vuln|
next unless vuln.elements['QID'] && vuln.elements['QID'].first
qid = vuln.elements['QID'].first.to_s
vuln_refs[qid] ||= []
vuln.elements.each('CVE_ID_LIST/CVE_ID') do |ref|
vuln_refs[qid].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|
vuln_refs[qid].push('BID-' + ref.elements['ID'].text.to_s)
end
end
return vuln_refs
end
# Pull out vulnerabilities that have at least one matching
# ref -- many "vulns" are not vulns, just audit information.
def find_qualys_asset_vulns(host,wspace,hobj,vuln_refs,&block)
host.elements.each("VULN_INFO_LIST/VULN_INFO") do |vi|
next unless vi.elements["QID"]
vi.elements.each("QID") do |qid|
next if vuln_refs[qid.text].nil? || vuln_refs[qid.text].empty?
handle_qualys(wspace, hobj, nil, nil, qid.text, nil, vuln_refs[qid.text], nil)
end
end
end
# Takes QID numbers and finds the discovered services in
# a qualys_asset_xml.
def find_qualys_asset_ports(i,host,wspace,hobj)
return unless (i == 82023 || i == 82004)
proto = i == 82023 ? 'tcp' : 'udp'
qid = host.elements["VULN_INFO_LIST/VULN_INFO/QID[@id='qid_#{i}']"]
qid_result = qid.parent.elements["RESULT[@format='table']"] if qid
hports = qid_result.first.to_s if qid_result
if hports
hports.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, hobj, match[0].to_s, proto, 0, nil, nil, name)
end
end
end
#
# Import Qualys's Asset Data Report format
#
def import_qualys_asset_xml(args={}, &block)
data = args[:data]
wspace = args[:wspace] || workspace
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
doc = rexmlify(data)
vuln_refs = find_qualys_asset_vuln_refs(doc)
# 2nd pass, actually grab the hosts.
doc.elements.each("/ASSET_DATA_REPORT/HOST_LIST/HOST") do |host|
hobj = nil
addr = host.elements["IP"].text if host.elements["IP"]
next unless validate_ips(addr)
if bl.include? addr
next
else
yield(:address,addr) if block
end
hname = ( # Prefer NetBIOS over DNS
(host.elements["NETBIOS"].text if host.elements["NETBIOS"]) ||
(host.elements["DNS"].text if host.elements["DNS"]) ||
"" )
hobj = report_host(:workspace => wspace, :host => addr, :name => hname, :state => Msf::HostState::Alive)
report_import_note(wspace,hobj)
if host.elements["OPERATING_SYSTEM"]
hos = host.elements["OPERATING_SYSTEM"].text
report_note(
:workspace => wspace,
:host => hobj,
:type => 'host.os.qualys_fingerprint',
:data => { :os => hos }
)
end
# Report open ports.
find_qualys_asset_ports(82023,host,wspace,hobj) # TCP
find_qualys_asset_ports(82004,host,wspace,hobj) # UDP
# Report vulns
find_qualys_asset_vulns(host,wspace,hobj,vuln_refs,&block)
end # host
end
#
# Import Qualys' Scan xml output
#
def import_qualys_scan_xml_file(args={})
filename = args[:filename]
wspace = args[:wspace] || workspace
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
import_qualys_scan_xml(args.merge(:data => data))
end
def import_qualys_scan_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|
hobj = nil
addr = host.attributes['value']
if bl.include? addr
next
else
yield(:address,addr) if block
end
hname = host.attributes['name'] || ''
hobj = report_host(:workspace => wspace, :host => addr, :name => hname, :state => Msf::HostState::Alive)
report_import_note(wspace,hobj)
if host.elements["OS"]
hos = host.elements["OS"].text
report_note(
:workspace => wspace,
:host => hobj,
: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, hobj, 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, hobj, 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, hobj, port, protocol, qid, severity, refs)
end
end
end
end
def import_ip_list_file(args={})
filename = args[:filename]
wspace = args[:wspace] || workspace
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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
data = ""
::File.open(filename, 'rb') do |f|
data = f.read(f.stat.size)
end
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 import_ci_noko_stream(args, &block)
if block
doc = Rex::Parser::CIDocument.new(args,framework.db) {|type, data| yield type,data }
else
doc = Rex::Parser::CI.new(args,self)
end
parser = ::Nokogiri::XML::SAX::Parser.new(doc)
parser.parse(args[:data])
end
def unserialize_object(xml_elem, allow_yaml = false)
return nil unless xml_elem
string = xml_elem.text.to_s.strip
return string unless string.is_a?(String)
return nil if (string.empty? || string.nil?)
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
#
# Returns something suitable for the +:host+ parameter to the various report_* methods
#
# Takes a Host object, a Session object, an Msf::Session object or a String
# address
#
def normalize_host(host)
return host if host.kind_of? Host
norm_host = nil
if (host.kind_of? String)
2011-12-10 19:24:20 +00:00
# If it's an IPv4 addr with a port on the end, strip the port
if Rex::Socket.is_ipv4?(host) and host =~ /((\d{1,3}\.){3}\d{1,3}):\d+/
norm_host = $1
else
norm_host = host
end
elsif host.kind_of? Session
norm_host = host.host
Merge session-host-rework branch back to master Squashed commit of the following: commit 2f4e8df33c5b4baa8d6fd67b400778a3f93482aa Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:31:03 2012 -0700 Clean up some rdoc comments This adds categories for the various interfaces that meterpreter and shell sessions implement so they are grouped logically in the docs. commit 9d31bc1b35845f7279148412f49bda56a39c9d9d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 13:00:25 2012 -0700 Combine the docs into one output dir There's really no need to separate the API sections into their own directory. Combining them makes it much easier to read. commit eadd7fc136a9e7e4d9652d55dfb86e6f318332e0 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:27:22 2012 -0700 Keep the order of iface attributes the same accross rubies 1.8 doesn't maintain insertion order for Hash keys like 1.9 does so we end up with ~random order for the display with the previous technique. Switch to an Array instead of a Hash so it's always the same. commit 6f66dd40f39959711f9bacbda99717253a375d21 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:23:35 2012 -0700 Fix a few more compiler warnings commit f39cb536a80c5000a5b9ca1fec5902300ae4b440 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 08:17:39 2012 -0700 Fix a type-safety warning commit 1e52785f38146515409da3724f858b9603d19454 Author: James Lee <egypt@metasploit.com> Date: Mon Feb 27 15:21:36 2012 -0700 LHOST should be OptAddress, not OptAddressRange commit acef978aa4233c7bd0b00ef63646eb4da5457f67 Author: James Lee <egypt@metasploit.com> Date: Sun Feb 26 17:45:59 2012 -0700 Fix a couple of warnings and a typo commit 29d87f88790aa1b3e5db6df650ecfb3fb93c675b Author: HD Moore <hdm@digitaloffense.net> Date: Mon Feb 27 11:54:29 2012 -0600 Fix ctype vs content_type typo commit 83b5400356c47dd1973e6be3aa343084dfd09c73 Author: Gregory Man <man.gregory@gmail.com> Date: Sun Feb 26 15:38:33 2012 +0200 Fixed scripts/meterpreter/enum_firefox to work with firefox > 3.6.x commit 49c2c80b347820d02348d694cc71f1b3028b4365 Author: Steve Tornio <swtornio@gmail.com> Date: Sun Feb 26 07:13:13 2012 -0600 add osvdb ref commit e18e1fe97b89c3a2b8c22bc6c18726853d2c2bee Author: Matt Andreko <mandreko@gmail.com> Date: Sat Feb 25 18:02:56 2012 -0500 Added aspx target to msfvenom. This in turn added it to msfencode as well. Ref: https://github.com/rapid7/metasploit-framework/pull/188 Tested on winxp with IIS in .net 1.1 and 2.0 modes commit e6aa5072112d79bbf8a4d2289cf8d301db3932f5 Author: Joshua J. Drake <github.jdrake@qoop.org> Date: Sat Feb 25 13:00:48 2012 -0600 Fixes #6308: Fall back to 127.0.0.1 when SocketError is raised from the resolver commit b3371e8bfeea4d84f9d0cba100352b57d7e9e78b Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 17:07:42 2012 -0700 Simplify logic for whether an inner iface has the same address commit 5417419f35a40d1c08ca11ca40744722692d3b0d Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:58:16 2012 -0700 Whitespace commit 9036875c2918439ae23e11ee7b958e30ccc29545 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:53:45 2012 -0700 Set session info before worrying about address get_interfaces can take a while on Linux, grab uid and hostname earlier so we can give the user an idea of what they popped as soon as possible. commit f34b51c6291031ab25b5bfb1ac6307a516ab0ee9 Author: James Lee <egypt@metasploit.com> Date: Tue Feb 28 16:48:42 2012 -0700 Clean up rdoc commit e61a0663454400ec66f59a80d18b0baff4cb8cd9 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:54:45 2012 -0600 Ensure the architecture is only the first word (not the full WOW64 message in some cases) commit 4c701610976a92298c1182eecc9291a1b301e43b Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:49:17 2012 -0600 More paranoia code, just in case RHOST is set to whitespace commit c5ff89fe3dc9061e0fa9f761e6530f6571989d28 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 04:47:01 2012 -0600 A few more small bug fixes to handle cases with an empty string target host resulting in a bad address commit 462d0188a1298f29ac83b10349aec6737efc5b19 Author: HD Moore <hd_moore@rapid7.com> Date: Tue Feb 28 03:55:10 2012 -0600 Fix up the logic (reversed by accident) commit 2b2b0adaec2448423dbd3ec54d90a5721965e2df Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 23:29:52 2012 -0600 Automatically parse system information and populate the db, identify and report NAT when detected, show the real session_host in the sessions -l listing commit 547a4ab4c62dc3248f847dd5d305ad3b74157348 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:16:03 2012 -0600 Fix typo introduced commit 27a7b7961e61894bdecd55310a8f45d0917c5a5c Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:11:38 2012 -0600 More session.session_host tweaks commit e447302a1a9915795e89b5e29c89ff2ab9b6209b Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:08:20 2012 -0600 Additional tunnel_peer changes commit 93369fcffaf8c6b00d992526b4083acfce036bb3 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:06:21 2012 -0600 Additional changes to session.session_host commit c3552f66d158685909e2c8b51dfead7c240c4f40 Author: HD Moore <hd_moore@rapid7.com> Date: Mon Feb 27 22:00:19 2012 -0600 Merge changes into the new branch
2012-02-29 01:28:47 +00:00
elsif host.respond_to?(:session_host)
# Then it's an Msf::Session object
thost = host.session_host
norm_host = thost
end
# If we got here and don't have a norm_host yet, it could be a
# Msf::Session object with an empty or nil tunnel_host and tunnel_peer;
# see if it has a socket and use its peerhost if so.
if (
norm_host.nil? and
host.respond_to?(:sock) and
host.sock.respond_to?(:peerhost) and
host.sock.peerhost.to_s.length > 0
)
norm_host = session.sock.peerhost
end
# If We got here and still don't have a real host, there's nothing left
# to try, just log it and return what we were given
if not norm_host
dlog("Host could not be normalized: #{host.inspect}")
norm_host = host
end
norm_host
end
# A way to sneak the yield back into the db importer.
# Used by the SAX parsers.
def emit(sym,data,&block)
yield(sym,data)
end
# Debug logger
def xxx(desc,thing)
$stderr.puts "**** #{desc} ****"
$stderr.puts thing.inspect
end
protected
#
# This holds all of the shared parsing/handling used by the
# Nessus NBE and NESSUS v1 methods
#
def handle_nessus(wspace, hobj, port, nasl, plugin_name, severity, data)
addr = hobj.address
# The port section looks like:
# http (80/tcp)
p = port.match(/^([^\(]+)\((\d+)\/([^\)]+)\)/)
return if not p
# Unnecessary as the caller should already have reported this host
#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 => hobj, :port => port, :proto => proto }
if name != "unknown" and name[-1,1] != "?"
info[:name] = name
end
report_service(info)
if nasl.nil? || nasl.empty? || nasl == 0 || nasl == "0"
return
end
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.strip
refs << nss
unless plugin_name.to_s.strip.empty?
vuln_name = plugin_name
else
vuln_name = nss
end
vuln_info = {
:workspace => wspace,
:host => hobj,
:port => port,
:proto => proto,
:name => vuln_name,
: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,hobj,port,proto,name,nasl,nasl_name,severity,description,cve,bid,xref,msf)
addr = hobj.address
info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto }
unless name =~ /^unknown$|\?$/
info[:name] = name
end
if port.to_i != 0
report_service(info)
end
if nasl.nil? || nasl.empty? || nasl == 0 || nasl == "0"
return
end
refs = []
cve.each do |r|
r.to_s.gsub!(/C(VE|AN)\-/, '')
refs.push('CVE-' + r.to_s)
end if cve
bid.each do |r|
refs.push('BID-' + r.to_s)
end if bid
xref.each 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
msfref = "MSF-" << msf if msf
refs.push msfref if msfref
nss = 'NSS-' + nasl
if nasl_name.nil? || nasl_name.empty?
vuln_name = nss
else
vuln_name = nasl_name
end
refs << nss.strip
vuln = {
:workspace => wspace,
:host => hobj,
:name => vuln_name,
:info => description ? description : "",
:refs => refs
}
if port.to_i != 0
vuln[:port] = port
vuln[:proto] = proto
end
report_vuln(vuln)
end
#
# IP360 v3 vuln
#
def handle_ip360_v3_svc(wspace,hobj,port,proto,hname)
addr = hobj.address
report_host(:workspace => wspace, :host => hobj, :state => Msf::HostState::Alive)
info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto }
if hname != "unknown" and hname[-1,1] != "?"
info[:name] = hname
end
if port.to_i != 0
report_service(info)
end
end #handle_ip360_v3_svc
#
# IP360 v3 vuln
#
def handle_ip360_v3_vuln(wspace,hobj,port,proto,hname,vulnid,vulnname,cves,bids)
info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto }
if hname != "unknown" and hname[-1,1] != "?"
info[:name] = hname
end
if port.to_i != 0
report_service(info)
end
refs = []
cves.split(/,/).each do |cve|
refs.push(cve.to_s)
end if cves
bids.split(/,/).each do |bid|
refs.push('BID-' + bid.to_s)
end if bids
description = nil # not working yet
vuln = {
:workspace => wspace,
:host => hobj,
:name => vulnname,
:info => description ? description : "",
:refs => refs
}
if port.to_i != 0
vuln[:port] = port
vuln[:proto] = proto
end
report_vuln(vuln)
end #handle_ip360_v3_vuln
#
# Qualys report parsing/handling
#
def handle_qualys(wspace, hobj, port, protocol, qid, severity, refs, name=nil)
addr = hobj.address
port = port.to_i if port
info = { :workspace => wspace, :host => hobj, :port => port, :proto => protocol }
if name and name != 'unknown' and name != 'No registered hostname'
info[:name] = name
end
if info[:host] && info[:port] && info[:proto]
report_service(info)
end
fixed_refs = []
if refs
refs.each do |ref|
case ref
when /^MS[0-9]{2}-[0-9]{3}/
fixed_refs << "MSB-#{ref}"
else
fixed_refs << ref
end
end
end
return if qid == 0
if addr
report_vuln(
:workspace => wspace,
:host => hobj,
:port => port,
:proto => protocol,
:name => 'QUALYS-' + qid,
:refs => fixed_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
end
end