1626 lines
49 KiB
Ruby
1626 lines
49 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
#
|
|
# Standard Library
|
|
#
|
|
|
|
require 'csv'
|
|
require 'fileutils'
|
|
require 'shellwords'
|
|
require 'tmpdir'
|
|
require 'uri'
|
|
|
|
#
|
|
#
|
|
# Gems
|
|
#
|
|
#
|
|
|
|
#
|
|
# PacketFu
|
|
#
|
|
|
|
require 'packetfu'
|
|
|
|
#
|
|
# Rex
|
|
#
|
|
|
|
require 'rex/parser/acunetix_nokogiri'
|
|
require 'rex/parser/appscan_nokogiri'
|
|
require 'rex/parser/burp_session_nokogiri'
|
|
require 'rex/parser/ci_nokogiri'
|
|
require 'rex/parser/foundstone_nokogiri'
|
|
require 'rex/parser/fusionvm_nokogiri'
|
|
require 'rex/parser/ip360_aspl_xml'
|
|
require 'rex/parser/ip360_xml'
|
|
require 'rex/parser/mbsa_nokogiri'
|
|
require 'rex/parser/nessus_xml'
|
|
require 'rex/parser/netsparker_xml'
|
|
require 'rex/parser/nexpose_raw_nokogiri'
|
|
require 'rex/parser/nexpose_simple_nokogiri'
|
|
require 'rex/parser/nexpose_xml'
|
|
require 'rex/parser/nmap_nokogiri'
|
|
require 'rex/parser/nmap_xml'
|
|
require 'rex/parser/openvas_nokogiri'
|
|
require 'rex/parser/outpost24_nokogiri'
|
|
require 'rex/parser/retina_xml'
|
|
require 'rex/parser/wapiti_nokogiri'
|
|
require 'rex/socket'
|
|
|
|
#
|
|
# Project
|
|
#
|
|
|
|
require 'metasploit/framework/require'
|
|
require 'msf/base/config'
|
|
require 'msf/core'
|
|
require 'msf/core/database_event'
|
|
require 'msf/core/db_import_error'
|
|
require 'msf/core/db_manager/import_msf_xml'
|
|
require 'msf/core/db_manager/migration'
|
|
require 'msf/core/host_state'
|
|
require 'msf/core/service_state'
|
|
require 'msf/core/task_manager'
|
|
|
|
module Msf
|
|
|
|
###
|
|
#
|
|
# The db module provides persistent storage and events. This class should be instantiated LAST
|
|
# as the active_suppport library overrides Kernel.require, slowing down all future code loads.
|
|
#
|
|
###
|
|
|
|
class DBManager
|
|
extend Metasploit::Framework::Require
|
|
|
|
autoload :Cred, 'msf/core/db_manager/cred'
|
|
autoload :ExploitedHost, 'msf/core/db_manager/exploited_host'
|
|
autoload :Host, 'msf/core/db_manager/host'
|
|
autoload :Import, 'msf/core/db_manager/import'
|
|
autoload :IPAddress, 'msf/core/db_manager/ip_address'
|
|
autoload :Loot, 'msf/core/db_manager/loot'
|
|
autoload :ModuleCache, 'msf/core/db_manager/module_cache'
|
|
autoload :Note, 'msf/core/db_manager/note'
|
|
autoload :Service, 'msf/core/db_manager/service'
|
|
autoload :Sink, 'msf/core/db_manager/sink'
|
|
autoload :Vuln, 'msf/core/db_manager/vuln'
|
|
autoload :WMAP, 'msf/core/db_manager/wmap'
|
|
autoload :Workspace, 'msf/core/db_manager/workspace'
|
|
|
|
optionally_include_metasploit_credential_creation
|
|
|
|
include Msf::DBManager::Cred
|
|
include Msf::DBManager::ExploitedHost
|
|
include Msf::DBManager::Host
|
|
include Msf::DBManager::Import
|
|
include Msf::DBManager::ImportMsfXml
|
|
include Msf::DBManager::IPAddress
|
|
include Msf::DBManager::Loot
|
|
include Msf::DBManager::Migration
|
|
include Msf::DBManager::ModuleCache
|
|
include Msf::DBManager::Note
|
|
include Msf::DBManager::Service
|
|
include Msf::DBManager::Sink
|
|
include Msf::DBManager::Vuln
|
|
include Msf::DBManager::WMAP
|
|
include Msf::DBManager::Workspace
|
|
|
|
# Provides :framework and other accessors
|
|
include Msf::Framework::Offspring
|
|
|
|
#
|
|
# CONSTANTS
|
|
#
|
|
|
|
# The adapter to use to establish database connection.
|
|
ADAPTER = 'postgresql'
|
|
|
|
# Mainly, it's Ruby 1.9.1 that cause a lot of problems now, along with Ruby 1.8.6.
|
|
# Ruby 1.8.7 actually seems okay, but why tempt fate? Let's say 1.9.3 and beyond.
|
|
def warn_about_rubies
|
|
if ::RUBY_VERSION =~ /^1\.9\.[012]($|[^\d])/
|
|
$stderr.puts "**************************************************************************************"
|
|
$stderr.puts "Metasploit requires at least Ruby 1.9.3. For an easy upgrade path, see https://rvm.io/"
|
|
$stderr.puts "**************************************************************************************"
|
|
end
|
|
end
|
|
|
|
# Returns true if we are ready to load/store data
|
|
def active
|
|
# usable and migrated a just Boolean attributes, so check those first because they don't actually contact the
|
|
# database.
|
|
usable && migrated && connection_established?
|
|
end
|
|
|
|
# Returns true if the prerequisites have been installed
|
|
attr_accessor :usable
|
|
|
|
# Returns the list of usable database drivers
|
|
def drivers
|
|
@drivers ||= []
|
|
end
|
|
attr_writer :drivers
|
|
|
|
# Returns the active driver
|
|
attr_accessor :driver
|
|
|
|
# Stores the error message for why the db was not loaded
|
|
attr_accessor :error
|
|
|
|
def initialize(framework, opts = {})
|
|
|
|
self.framework = framework
|
|
self.migrated = false
|
|
self.modules_cached = false
|
|
self.modules_caching = false
|
|
|
|
@usable = false
|
|
|
|
# Don't load the database if the user said they didn't need it.
|
|
if (opts['DisableDatabase'])
|
|
self.error = "disabled"
|
|
return
|
|
end
|
|
|
|
initialize_database_support
|
|
end
|
|
|
|
#
|
|
# Do what is necessary to load our database support
|
|
#
|
|
def initialize_database_support
|
|
begin
|
|
# Database drivers can reset our KCODE, do not let them
|
|
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
|
|
|
add_rails_engine_migration_paths
|
|
|
|
@usable = true
|
|
|
|
rescue ::Exception => e
|
|
self.error = e
|
|
elog("DB is not enabled due to load error: #{e}")
|
|
return false
|
|
end
|
|
|
|
#
|
|
# Determine what drivers are available
|
|
#
|
|
initialize_adapter
|
|
|
|
#
|
|
# Instantiate the database sink
|
|
#
|
|
initialize_sink
|
|
|
|
true
|
|
end
|
|
|
|
# Checks if the spec passed to `ActiveRecord::Base.establish_connection` can connect to the database.
|
|
#
|
|
# @return [true] if an active connection can be made to the database using the current config.
|
|
# @return [false] if an active connection cannot be made to the database.
|
|
def connection_established?
|
|
begin
|
|
# use with_connection so the connection doesn't stay pinned to the thread.
|
|
ActiveRecord::Base.connection_pool.with_connection {
|
|
ActiveRecord::Base.connection.active?
|
|
}
|
|
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => error
|
|
elog("Connection not established: #{error.class} #{error}:\n#{error.backtrace.join("\n")}")
|
|
|
|
false
|
|
end
|
|
end
|
|
|
|
#
|
|
# Scan through available drivers
|
|
#
|
|
def initialize_adapter
|
|
ActiveRecord::Base.default_timezone = :utc
|
|
|
|
if connection_established? && ActiveRecord::Base.connection_config[:adapter] == ADAPTER
|
|
dlog("Already established connection to #{ADAPTER}, so reusing active connection.")
|
|
self.drivers << ADAPTER
|
|
self.driver = ADAPTER
|
|
else
|
|
begin
|
|
ActiveRecord::Base.establish_connection(adapter: ADAPTER)
|
|
ActiveRecord::Base.remove_connection
|
|
rescue Exception => error
|
|
@adapter_error = error
|
|
else
|
|
self.drivers << ADAPTER
|
|
self.driver = ADAPTER
|
|
end
|
|
end
|
|
end
|
|
|
|
# Loads Metasploit Data Models and adds its migrations to migrations paths.
|
|
#
|
|
# @return [void]
|
|
def add_rails_engine_migration_paths
|
|
unless defined? ActiveRecord
|
|
fail "Bundle installed '--without #{Bundler.settings.without.join(' ')}'. To clear the without option do " \
|
|
"`bundle install --without ''` (the --without flag with an empty string) or `rm -rf .bundle` to remove " \
|
|
"the .bundle/config manually and then `bundle install`"
|
|
end
|
|
|
|
Rails.application.railties.engines.each do |engine|
|
|
migrations_paths = engine.paths['db/migrate'].existent_directories
|
|
|
|
migrations_paths.each do |migrations_path|
|
|
# Since ActiveRecord::Migrator.migrations_paths can persist between
|
|
# instances of Msf::DBManager, such as in specs,
|
|
# migrations_path may already be part of
|
|
# migrations_paths, in which case it should not be added or multiple
|
|
# migrations with the same version number errors will occur.
|
|
unless ActiveRecord::Migrator.migrations_paths.include? migrations_path
|
|
ActiveRecord::Migrator.migrations_paths << migrations_path
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Connects this instance to a database
|
|
#
|
|
def connect(opts={})
|
|
|
|
return false if not @usable
|
|
|
|
nopts = opts.dup
|
|
if (nopts['port'])
|
|
nopts['port'] = nopts['port'].to_i
|
|
end
|
|
|
|
# Prefer the config file's pool setting
|
|
nopts['pool'] ||= 75
|
|
|
|
# Prefer the config file's wait_timeout setting too
|
|
nopts['wait_timeout'] ||= 300
|
|
|
|
begin
|
|
self.migrated = false
|
|
|
|
# Check ActiveRecord::Base was already connected by Rails::Application.initialize! or some other API.
|
|
unless connection_established?
|
|
create_db(nopts)
|
|
|
|
# Configure the database adapter
|
|
ActiveRecord::Base.establish_connection(nopts)
|
|
end
|
|
rescue ::Exception => e
|
|
self.error = e
|
|
elog("DB.connect threw an exception: #{e}")
|
|
dlog("Call stack: #{$@.join"\n"}", LEV_1)
|
|
return false
|
|
ensure
|
|
after_establish_connection
|
|
|
|
# Database drivers can reset our KCODE, do not let them
|
|
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
# Finishes {#connect} after `ActiveRecord::Base.establish_connection` has succeeded by {#migrate migrating database}
|
|
# and setting {#workspace}.
|
|
#
|
|
# @return [void]
|
|
def after_establish_connection
|
|
self.migrated = false
|
|
|
|
begin
|
|
# Migrate the database, if needed
|
|
migrate
|
|
|
|
# Set the default workspace
|
|
framework.db.workspace = framework.db.default_workspace
|
|
rescue ::Exception => exception
|
|
self.error = exception
|
|
elog("DB.connect threw an exception: #{exception}")
|
|
dlog("Call stack: #{exception.backtrace.join("\n")}", LEV_1)
|
|
else
|
|
# Flag that migration has completed
|
|
self.migrated = true
|
|
end
|
|
end
|
|
|
|
#
|
|
# Attempt to create the database
|
|
#
|
|
# If the database already exists this will fail and we will continue on our
|
|
# merry way, connecting anyway. If it doesn't, we try to create it. If
|
|
# that fails, then it wasn't meant to be and the connect will raise a
|
|
# useful exception so the user won't be in the dark; no need to raise
|
|
# anything at all here.
|
|
#
|
|
def create_db(opts)
|
|
begin
|
|
case opts["adapter"]
|
|
when 'postgresql'
|
|
# Try to force a connection to be made to the database, if it succeeds
|
|
# then we know we don't need to create it :)
|
|
ActiveRecord::Base.establish_connection(opts)
|
|
# Do the checkout, checkin dance here to make sure this thread doesn't
|
|
# hold on to a connection we don't need
|
|
conn = ActiveRecord::Base.connection_pool.checkout
|
|
ActiveRecord::Base.connection_pool.checkin(conn)
|
|
end
|
|
rescue ::Exception => e
|
|
errstr = e.to_s
|
|
if errstr =~ /does not exist/i or errstr =~ /Unknown database/
|
|
ilog("Database doesn't exist \"#{opts['database']}\", attempting to create it.")
|
|
ActiveRecord::Base.establish_connection(
|
|
opts.merge(
|
|
'database' => 'postgres',
|
|
'schema_search_path' => 'public'
|
|
)
|
|
)
|
|
|
|
ActiveRecord::Base.connection.create_database(opts['database'])
|
|
else
|
|
ilog("Trying to continue despite failed database creation: #{e}")
|
|
end
|
|
end
|
|
ActiveRecord::Base.remove_connection
|
|
end
|
|
|
|
#
|
|
# Disconnects a database session
|
|
#
|
|
def disconnect
|
|
begin
|
|
ActiveRecord::Base.remove_connection
|
|
self.migrated = false
|
|
self.modules_cached = false
|
|
rescue ::Exception => e
|
|
self.error = e
|
|
elog("DB.disconnect threw an exception: #{e}")
|
|
ensure
|
|
# Database drivers can reset our KCODE, do not let them
|
|
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
|
end
|
|
end
|
|
|
|
#
|
|
# Determines if the database is functional
|
|
#
|
|
def check
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
res = ::Mdm::Host.find(:first)
|
|
}
|
|
end
|
|
|
|
# Returns a session based on opened_time, host address, and workspace
|
|
# (or returns nil)
|
|
def get_session(opts)
|
|
return if not active
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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
|
|
::Mdm::Session.find_by_host_id_and_opened_at(host.id, time)
|
|
}
|
|
end
|
|
|
|
# @note The Mdm::Session#desc will be truncated to 255 characters.
|
|
# @todo https://www.pivotaltracker.com/story/show/48249739
|
|
#
|
|
# @overload report_session(opts)
|
|
# Creates an Mdm::Session from Msf::Session. If +via_exploit+ is set on the
|
|
# +session+, then an Mdm::Vuln and Mdm::ExploitAttempt is created for the
|
|
# session's host. The Mdm::Host for the +session_host+ is created using
|
|
# The session.session_host, +session.arch+ (if +session+ responds to arch),
|
|
# and the workspace derived from opts or the +session+. The Mdm::Session is
|
|
# assumed to be +last_seen+ and +opened_at+ at the time report_session is
|
|
# called. +session.exploit_datastore['ParentModule']+ is used for the
|
|
# Mdm::Session#via_exploit if +session.via_exploit+ is
|
|
# 'exploit/multi/handler'.
|
|
#
|
|
# @param opts [Hash{Symbol => Object}] options
|
|
# @option opt [Msf::Session, #datastore, #platform, #type, #via_exploit, #via_payload] :session
|
|
# The in-memory session to persist to the database.
|
|
# @option opts [Mdm::Workspace] :workspace The workspace for in which the
|
|
# :session host is contained. Also used as the workspace for the
|
|
# Mdm::ExploitAttempt and Mdm::Vuln. Defaults to Mdm::Worksapce with
|
|
# Mdm::Workspace#name equal to +session.workspace+.
|
|
# @return [nil] if {Msf::DBManager#active} is +false+.
|
|
# @return [Mdm::Session] if session is saved
|
|
# @raise [ArgumentError] if :session is not an {Msf::Session}.
|
|
# @raise [ActiveRecord::RecordInvalid] if session is invalid and cannot be
|
|
# saved, in which case, the Mdm::ExploitAttempt and Mdm::Vuln will not be
|
|
# created, but the Mdm::Host will have been. (There is no transaction
|
|
# to rollback the Mdm::Host creation.)
|
|
# @see #find_or_create_host
|
|
# @see #normalize_host
|
|
# @see #report_exploit_success
|
|
# @see #report_vuln
|
|
#
|
|
# @overload report_session(opts)
|
|
# Creates an Mdm::Session from Mdm::Host.
|
|
#
|
|
# @param opts [Hash{Symbol => Object}] options
|
|
# @option opts [DateTime, Time] :closed_at The date and time the sesion was
|
|
# closed.
|
|
# @option opts [String] :close_reason Reason the session was closed.
|
|
# @option opts [Hash] :datastore {Msf::DataStore#to_h}.
|
|
# @option opts [String] :desc Session description. Will be truncated to 255
|
|
# characters.
|
|
# @option opts [Mdm::Host] :host The host on which the session was opened.
|
|
# @option opts [DateTime, Time] :last_seen The last date and time the
|
|
# session was seen to be open. Defaults to :closed_at's value.
|
|
# @option opts [DateTime, Time] :opened_at The date and time that the
|
|
# session was opened.
|
|
# @option opts [String] :platform The platform of the host.
|
|
# @option opts [Array] :routes ([]) The routes through the session for
|
|
# pivoting.
|
|
# @option opts [String] :stype Session type.
|
|
# @option opts [String] :via_exploit The {Msf::Module#fullname} of the
|
|
# exploit that was used to open the session.
|
|
# @option option [String] :via_payload the {MSf::Module#fullname} of the
|
|
# payload sent to the host when the exploit was successful.
|
|
# @return [nil] if {Msf::DBManager#active} is +false+.
|
|
# @return [Mdm::Session] if session is saved.
|
|
# @raise [ArgumentError] if :host is not an Mdm::Host.
|
|
# @raise [ActiveRecord::RecordInvalid] if session is invalid and cannot be
|
|
# saved.
|
|
#
|
|
# @raise ArgumentError if :host and :session is +nil+
|
|
def report_session(opts)
|
|
return if not active
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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,
|
|
:port => session.session_port,
|
|
: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? ::Mdm::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
|
|
|
|
# In the case of multi handler we cannot yet determine the true
|
|
# exploit responsible. But we can at least show the parent versus
|
|
# just the generic handler:
|
|
if session and session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule']
|
|
sess_data[:via_exploit] = sess_data[:datastore]['ParentModule']
|
|
end
|
|
|
|
s = ::Mdm::Session.new(sess_data)
|
|
s.save!
|
|
|
|
if session and session.exploit_task and session.exploit_task.record
|
|
session_task = session.exploit_task.record
|
|
if session_task.class == Mdm::Task
|
|
Mdm::TaskSession.create(:task => session_task, :session => s )
|
|
end
|
|
end
|
|
|
|
|
|
if opts[:session]
|
|
session.db_record = s
|
|
end
|
|
|
|
# If this is a live session, we know the host is vulnerable to something.
|
|
if opts[:session] and session.via_exploit
|
|
mod = framework.modules.create(session.via_exploit)
|
|
|
|
if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule']
|
|
mod_fullname = sess_data[:datastore]['ParentModule']
|
|
mod_name = ::Mdm::Module::Detail.find_by_fullname(mod_fullname).name
|
|
else
|
|
mod_name = mod.name
|
|
mod_fullname = mod.fullname
|
|
end
|
|
|
|
vuln_info = {
|
|
:host => host.address,
|
|
:name => mod_name,
|
|
:refs => mod.references,
|
|
:workspace => wspace,
|
|
:exploited_at => Time.now.utc,
|
|
:info => "Exploited by #{mod_fullname} to create Session #{s.id}"
|
|
}
|
|
|
|
port = session.exploit_datastore["RPORT"]
|
|
service = (port ? host.services.find_by_port(port.to_i) : nil)
|
|
|
|
vuln_info[:service] = service if service
|
|
|
|
vuln = framework.db.report_vuln(vuln_info)
|
|
|
|
if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule']
|
|
via_exploit = sess_data[:datastore]['ParentModule']
|
|
else
|
|
via_exploit = session.via_exploit
|
|
end
|
|
attempt_info = {
|
|
:timestamp => Time.now.utc,
|
|
:workspace => wspace,
|
|
:module => via_exploit,
|
|
:username => session.username,
|
|
:refs => mod.references,
|
|
:session_id => s.id,
|
|
:host => host,
|
|
:service => service,
|
|
:vuln => vuln
|
|
}
|
|
|
|
framework.db.report_exploit_success(attempt_info)
|
|
|
|
end
|
|
|
|
s
|
|
}
|
|
end
|
|
|
|
#
|
|
# Record a session event in the database
|
|
#
|
|
# opts MUST contain one of:
|
|
# +:session+:: the Msf::Session OR the ::Mdm::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
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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 = ::Mdm::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
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
|
|
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
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
subnet, netmask = route.split("/")
|
|
r = s.routes.find_by_subnet_and_netmask(subnet, netmask)
|
|
r.destroy if r
|
|
}
|
|
end
|
|
|
|
|
|
def report_exploit_success(opts)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
|
|
wspace = opts.delete(:workspace) || workspace
|
|
mrefs = opts.delete(:refs) || return
|
|
host = opts.delete(:host)
|
|
port = opts.delete(:port)
|
|
prot = opts.delete(:proto)
|
|
svc = opts.delete(:service)
|
|
vuln = opts.delete(:vuln)
|
|
|
|
timestamp = opts.delete(:timestamp)
|
|
username = opts.delete(:username)
|
|
mname = opts.delete(:module)
|
|
|
|
# Look up or generate the host as appropriate
|
|
if not (host and host.kind_of? ::Mdm::Host)
|
|
if svc.kind_of? ::Mdm::Service
|
|
host = svc.host
|
|
else
|
|
host = report_host(:workspace => wspace, :address => host )
|
|
end
|
|
end
|
|
|
|
# Bail if we dont have a host object
|
|
return if not host
|
|
|
|
# Look up or generate the service as appropriate
|
|
if port and svc.nil?
|
|
svc = report_service(:workspace => wspace, :host => host, :port => port, :proto => prot ) if port
|
|
end
|
|
|
|
if not vuln
|
|
# Create a references map from the module list
|
|
ref_objs = ::Mdm::Ref.where(:name => mrefs.map { |ref|
|
|
if ref.respond_to?(:ctx_id) and ref.respond_to?(:ctx_val)
|
|
"#{ref.ctx_id}-#{ref.ctx_val}"
|
|
else
|
|
ref.to_s
|
|
end
|
|
})
|
|
|
|
# Try find a matching vulnerability
|
|
vuln = find_vuln_by_refs(ref_objs, host, svc)
|
|
end
|
|
|
|
# We have match, lets create a vuln_attempt record
|
|
if vuln
|
|
attempt_info = {
|
|
:vuln_id => vuln.id,
|
|
:attempted_at => timestamp || Time.now.utc,
|
|
:exploited => true,
|
|
:username => username || "unknown",
|
|
:module => mname
|
|
}
|
|
|
|
attempt_info[:session_id] = opts[:session_id] if opts[:session_id]
|
|
attempt_info[:loot_id] = opts[:loot_id] if opts[:loot_id]
|
|
|
|
vuln.vuln_attempts.create(attempt_info)
|
|
|
|
# Correct the vuln's associated service if necessary
|
|
if svc and vuln.service_id.nil?
|
|
vuln.service = svc
|
|
vuln.save
|
|
end
|
|
end
|
|
|
|
# Report an exploit attempt all the same
|
|
attempt_info = {
|
|
:attempted_at => timestamp || Time.now.utc,
|
|
:exploited => true,
|
|
:username => username || "unknown",
|
|
:module => mname
|
|
}
|
|
|
|
attempt_info[:vuln_id] = vuln.id if vuln
|
|
attempt_info[:session_id] = opts[:session_id] if opts[:session_id]
|
|
attempt_info[:loot_id] = opts[:loot_id] if opts[:loot_id]
|
|
|
|
if svc
|
|
attempt_info[:port] = svc.port
|
|
attempt_info[:proto] = svc.proto
|
|
end
|
|
|
|
if port and svc.nil?
|
|
attempt_info[:port] = port
|
|
attempt_info[:proto] = prot || "tcp"
|
|
end
|
|
|
|
host.exploit_attempts.create(attempt_info)
|
|
}
|
|
end
|
|
|
|
def report_exploit_failure(opts)
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
wspace = opts.delete(:workspace) || workspace
|
|
mrefs = opts.delete(:refs) || return
|
|
host = opts.delete(:host)
|
|
port = opts.delete(:port)
|
|
prot = opts.delete(:proto)
|
|
svc = opts.delete(:service)
|
|
vuln = opts.delete(:vuln)
|
|
|
|
timestamp = opts.delete(:timestamp)
|
|
freason = opts.delete(:fail_reason)
|
|
fdetail = opts.delete(:fail_detail)
|
|
username = opts.delete(:username)
|
|
mname = opts.delete(:module)
|
|
|
|
# Look up the host as appropriate
|
|
if not (host and host.kind_of? ::Mdm::Host)
|
|
if svc.kind_of? ::Mdm::Service
|
|
host = svc.host
|
|
else
|
|
host = get_host( :workspace => wspace, :address => host )
|
|
end
|
|
end
|
|
|
|
# Bail if we dont have a host object
|
|
return if not host
|
|
|
|
# Look up the service as appropriate
|
|
if port and svc.nil?
|
|
prot ||= "tcp"
|
|
svc = get_service(wspace, host, prot, port) if port
|
|
end
|
|
|
|
if not vuln
|
|
# Create a references map from the module list
|
|
ref_objs = ::Mdm::Ref.where(:name => mrefs.map { |ref|
|
|
if ref.respond_to?(:ctx_id) and ref.respond_to?(:ctx_val)
|
|
"#{ref.ctx_id}-#{ref.ctx_val}"
|
|
else
|
|
ref.to_s
|
|
end
|
|
})
|
|
|
|
# Try find a matching vulnerability
|
|
vuln = find_vuln_by_refs(ref_objs, host, svc)
|
|
end
|
|
|
|
# Report a vuln_attempt if we found a match
|
|
if vuln
|
|
attempt_info = {
|
|
:attempted_at => timestamp || Time.now.utc,
|
|
:exploited => false,
|
|
:fail_reason => freason,
|
|
:fail_detail => fdetail,
|
|
:username => username || "unknown",
|
|
:module => mname
|
|
}
|
|
|
|
vuln.vuln_attempts.create(attempt_info)
|
|
end
|
|
|
|
# Report an exploit attempt all the same
|
|
attempt_info = {
|
|
:attempted_at => timestamp || Time.now.utc,
|
|
:exploited => false,
|
|
:username => username || "unknown",
|
|
:module => mname,
|
|
:fail_reason => freason,
|
|
:fail_detail => fdetail
|
|
}
|
|
|
|
attempt_info[:vuln_id] = vuln.id if vuln
|
|
|
|
if svc
|
|
attempt_info[:port] = svc.port
|
|
attempt_info[:proto] = svc.proto
|
|
end
|
|
|
|
if port and svc.nil?
|
|
attempt_info[:port] = port
|
|
attempt_info[:proto] = prot || "tcp"
|
|
end
|
|
|
|
host.exploit_attempts.create(attempt_info)
|
|
}
|
|
end
|
|
|
|
|
|
def report_vuln_attempt(vuln, opts)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
return if not vuln
|
|
info = {}
|
|
|
|
# Opts can be keyed by strings or symbols
|
|
::Mdm::VulnAttempt.column_names.each do |kn|
|
|
k = kn.to_sym
|
|
next if ['id', 'vuln_id'].include?(kn)
|
|
info[k] = opts[kn] if opts[kn]
|
|
info[k] = opts[k] if opts[k]
|
|
end
|
|
|
|
return unless info[:attempted_at]
|
|
|
|
vuln.vuln_attempts.create(info)
|
|
}
|
|
end
|
|
|
|
def report_exploit_attempt(host, opts)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
return if not host
|
|
info = {}
|
|
|
|
# Opts can be keyed by strings or symbols
|
|
::Mdm::VulnAttempt.column_names.each do |kn|
|
|
k = kn.to_sym
|
|
next if ['id', 'host_id'].include?(kn)
|
|
info[k] = opts[kn] if opts[kn]
|
|
info[k] = opts[k] if opts[k]
|
|
end
|
|
|
|
host.exploit_attempts.create(info)
|
|
}
|
|
end
|
|
|
|
def get_client(opts)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
wspace = opts.delete(:workspace) || workspace
|
|
host = get_host(:workspace => wspace, :host => opts[:host]) || return
|
|
client = host.clients.where({:ua_string => opts[:ua_string]}).first()
|
|
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
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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 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
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, name).order("tags.id DESC").limit(1)
|
|
tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first)
|
|
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
|
|
|
|
#
|
|
# 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]
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
ref = ::Mdm::Ref.find_or_initialize_by_name(opts[:name])
|
|
if ref and ref.changed?
|
|
ref.save!
|
|
end
|
|
ret[:ref] = ref
|
|
}
|
|
end
|
|
|
|
def get_ref(name)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
::Mdm::Ref.find_by_name(name)
|
|
}
|
|
end
|
|
|
|
#
|
|
# Populate the vuln_details table with additional
|
|
# information, matched by a specific criteria
|
|
#
|
|
def report_vuln_details(vuln, details)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
detail = ::Mdm::VulnDetail.where(( details.delete(:key) || {} ).merge(:vuln_id => vuln.id)).first
|
|
if detail
|
|
details.each_pair do |k,v|
|
|
detail[k] = v
|
|
end
|
|
detail.save! if detail.changed?
|
|
detail
|
|
else
|
|
detail = ::Mdm::VulnDetail.create(details.merge(:vuln_id => vuln.id))
|
|
end
|
|
}
|
|
end
|
|
|
|
#
|
|
# Update vuln_details records en-masse based on specific criteria
|
|
# Note that this *can* update data across workspaces
|
|
#
|
|
def update_vuln_details(details)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
criteria = details.delete(:key) || {}
|
|
::Mdm::VulnDetail.update(key, details)
|
|
}
|
|
end
|
|
|
|
#
|
|
# Populate the host_details table with additional
|
|
# information, matched by a specific criteria
|
|
#
|
|
def report_host_details(host, details)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
|
|
detail = ::Mdm::HostDetail.where(( details.delete(:key) || {} ).merge(:host_id => host.id)).first
|
|
if detail
|
|
details.each_pair do |k,v|
|
|
detail[k] = v
|
|
end
|
|
detail.save! if detail.changed?
|
|
detail
|
|
else
|
|
detail = ::Mdm::HostDetail.create(details.merge(:host_id => host.id))
|
|
end
|
|
}
|
|
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
|
|
|
|
#
|
|
# Find a reference matching this name
|
|
#
|
|
def has_ref?(name)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
Mdm::Ref.find_by_name(name)
|
|
}
|
|
end
|
|
|
|
def events(wspace=workspace)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
wspace.events.find :all, :order => 'created_at ASC'
|
|
}
|
|
end
|
|
|
|
def report_event(opts = {})
|
|
return if not active
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
wspace = opts.delete(:workspace) || workspace
|
|
return if not wspace # Temp fix?
|
|
uname = opts.delete(:username)
|
|
|
|
if ! opts[:host].kind_of? ::Mdm::Host and opts[:host]
|
|
opts[:host] = report_host(:workspace => wspace, :host => opts[:host])
|
|
end
|
|
|
|
::Mdm::Event.create(opts.merge(:workspace_id => wspace[:id], :username => uname))
|
|
}
|
|
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
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
wspace.tasks
|
|
}
|
|
end
|
|
|
|
|
|
# TODO This method does not attempt to find. It just creates
|
|
# a report based on the passed params.
|
|
def find_or_create_report(opts)
|
|
report_report(opts)
|
|
end
|
|
|
|
# Creates a Report based on passed parameters. Does not handle
|
|
# child artifacts.
|
|
# @param opts [Hash]
|
|
# @return [Integer] ID of created report
|
|
def report_report(opts)
|
|
return if not active
|
|
created = opts.delete(:created_at)
|
|
updated = opts.delete(:updated_at)
|
|
state = opts.delete(:state)
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
report = Report.new(opts)
|
|
report.created_at = created
|
|
report.updated_at = updated
|
|
|
|
unless report.valid?
|
|
errors = report.errors.full_messages.join('; ')
|
|
raise RuntimeError "Report to be imported is not valid: #{errors}"
|
|
end
|
|
report.state = :complete # Presume complete since it was exported
|
|
report.save
|
|
|
|
report.id
|
|
}
|
|
end
|
|
|
|
# Creates a ReportArtifact based on passed parameters.
|
|
# @param opts [Hash] of ReportArtifact attributes
|
|
def report_artifact(opts)
|
|
return if not active
|
|
|
|
artifacts_dir = Report::ARTIFACT_DIR
|
|
tmp_path = opts[:file_path]
|
|
artifact_name = File.basename tmp_path
|
|
new_path = File.join(artifacts_dir, artifact_name)
|
|
created = opts.delete(:created_at)
|
|
updated = opts.delete(:updated_at)
|
|
|
|
unless File.exists? tmp_path
|
|
raise DBImportError 'Report artifact file to be imported does not exist.'
|
|
end
|
|
|
|
unless (File.directory?(artifacts_dir) && File.writable?(artifacts_dir))
|
|
raise DBImportError "Could not move report artifact file to #{artifacts_dir}."
|
|
end
|
|
|
|
if File.exists? new_path
|
|
unique_basename = "#{(Time.now.to_f*1000).to_i}_#{artifact_name}"
|
|
new_path = File.join(artifacts_dir, unique_basename)
|
|
end
|
|
|
|
FileUtils.copy(tmp_path, new_path)
|
|
opts[:file_path] = new_path
|
|
artifact = ReportArtifact.new(opts)
|
|
artifact.created_at = created
|
|
artifact.updated_at = updated
|
|
|
|
unless artifact.valid?
|
|
errors = artifact.errors.full_messages.join('; ')
|
|
raise RuntimeError "Artifact to be imported is not valid: #{errors}"
|
|
end
|
|
artifact.save
|
|
end
|
|
|
|
#
|
|
# This methods returns a list of all reports in the database
|
|
#
|
|
def reports(wspace=workspace)
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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
|
|
::ActiveRecord::Base.connection_pool.with_connection { |conn|
|
|
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?(::Mdm::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 = ::Mdm::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
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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?(::Mdm::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 = ::Mdm::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
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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?(::Mdm::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
|
|
::Mdm::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 = ::Mdm::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
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
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
|
|
payload = opts[:payload].to_s
|
|
owner = opts[:owner] ? opts[:owner].shortname : nil
|
|
|
|
|
|
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?(::Mdm::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 = ::Mdm::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
|
|
vuln.payload = payload
|
|
vuln.owner = owner
|
|
|
|
msf_import_timestamps(opts, vuln)
|
|
vuln.save!
|
|
|
|
ret[:web_vuln] = vuln
|
|
}
|
|
end
|
|
end
|
|
end
|