636 lines
16 KiB
Ruby
636 lines
16 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
require 'msf/base/config'
|
|
require 'msf/core'
|
|
require 'msf/core/db'
|
|
require 'msf/core/task_manager'
|
|
require 'fileutils'
|
|
require 'shellwords'
|
|
|
|
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
|
|
|
|
# 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
|
|
|
|
# Provides :framework and other accessors
|
|
include Framework::Offspring
|
|
|
|
# Returns true if we are ready to load/store data
|
|
def active
|
|
return false if not @usable
|
|
# We have established a connection, some connection is active, and we have run migrations
|
|
(ActiveRecord::Base.connected? && ActiveRecord::Base.connection_pool.connected? && migrated)# rescue false
|
|
end
|
|
|
|
# Returns true if the prerequisites have been installed
|
|
attr_accessor :usable
|
|
|
|
# Returns the list of usable database drivers
|
|
attr_accessor :drivers
|
|
|
|
# Returns the active driver
|
|
attr_accessor :driver
|
|
|
|
# Stores the error message for why the db was not loaded
|
|
attr_accessor :error
|
|
|
|
# Stores a TaskManager for serializing database events
|
|
attr_accessor :sink
|
|
|
|
# Flag to indicate database migration has completed
|
|
attr_accessor :migrated
|
|
|
|
# Array of additional migration paths
|
|
attr_accessor :migration_paths
|
|
|
|
# Flag to indicate that modules are cached
|
|
attr_accessor :modules_cached
|
|
|
|
# Flag to indicate that the module cacher is running
|
|
attr_accessor :modules_caching
|
|
|
|
def initialize(framework, opts = {})
|
|
|
|
self.framework = framework
|
|
self.migrated = false
|
|
self.migration_paths = [ ::File.join(Msf::Config.install_root, "data", "sql", "migrate") ]
|
|
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
|
|
|
|
#
|
|
# Add additional migration paths
|
|
#
|
|
def add_migration_path(path)
|
|
self.migration_paths.push(path)
|
|
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\./
|
|
|
|
require "active_record"
|
|
|
|
# Provide access to ActiveRecord models shared w/ commercial versions
|
|
require "metasploit_data_models"
|
|
|
|
# Patches issues with ActiveRecord
|
|
require "msf/core/patches/active_record"
|
|
|
|
@usable = true
|
|
|
|
rescue ::Exception => e
|
|
self.error = e
|
|
elog("DB is not enabled due to load error: #{e}")
|
|
return false
|
|
end
|
|
|
|
# Only include Mdm if we're not using Metasploit commercial versions
|
|
# If Mdm::Host is defined, the dynamically created classes
|
|
# are already in the object space
|
|
begin
|
|
unless defined? Mdm::Host
|
|
MetasploitDataModels.require_models
|
|
end
|
|
rescue NameError => e
|
|
warn_about_rubies
|
|
raise e
|
|
end
|
|
|
|
#
|
|
# Determine what drivers are available
|
|
#
|
|
initialize_drivers
|
|
|
|
#
|
|
# Instantiate the database sink
|
|
#
|
|
initialize_sink
|
|
|
|
true
|
|
end
|
|
|
|
#
|
|
# Scan through available drivers
|
|
#
|
|
def initialize_drivers
|
|
self.drivers = []
|
|
tdrivers = %W{ postgresql }
|
|
tdrivers.each do |driver|
|
|
begin
|
|
ActiveRecord::Base.default_timezone = :utc
|
|
ActiveRecord::Base.establish_connection(:adapter => driver)
|
|
if(self.respond_to?("driver_check_#{driver}"))
|
|
self.send("driver_check_#{driver}")
|
|
end
|
|
ActiveRecord::Base.remove_connection
|
|
self.drivers << driver
|
|
rescue ::Exception
|
|
end
|
|
end
|
|
|
|
if(not self.drivers.empty?)
|
|
self.driver = self.drivers[0]
|
|
end
|
|
|
|
# Database drivers can reset our KCODE, do not let them
|
|
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
|
end
|
|
|
|
#
|
|
# Create a new database sink and initialize it
|
|
#
|
|
def initialize_sink
|
|
self.sink = TaskManager.new(framework)
|
|
self.sink.start
|
|
end
|
|
|
|
#
|
|
# Add a new task to the sink
|
|
#
|
|
def queue(proc)
|
|
self.sink.queue_proc(proc)
|
|
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
|
|
create_db(nopts)
|
|
|
|
# Configure the database adapter
|
|
ActiveRecord::Base.establish_connection(nopts)
|
|
|
|
# Migrate the database, if needed
|
|
migrate
|
|
|
|
# Set the default workspace
|
|
framework.db.workspace = framework.db.default_workspace
|
|
|
|
# Flag that migration has completed
|
|
self.migrated = true
|
|
rescue ::Exception => e
|
|
self.error = e
|
|
elog("DB.connect threw an exception: #{e}")
|
|
dlog("Call stack: #{$@.join"\n"}", LEV_1)
|
|
return false
|
|
ensure
|
|
# Database drivers can reset our KCODE, do not let them
|
|
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
|
end
|
|
|
|
true
|
|
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' => nil))
|
|
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
|
|
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
|
|
|
|
#
|
|
# Migrate database to latest schema version
|
|
#
|
|
def migrate(verbose=false)
|
|
|
|
temp_dir = ::File.expand_path(::File.join( Msf::Config.config_directory, "schema", "#{Time.now.to_i}_#{$$}" ))
|
|
::FileUtils.rm_rf(temp_dir)
|
|
::FileUtils.mkdir_p(temp_dir)
|
|
|
|
self.migration_paths.each do |mpath|
|
|
dir = Dir.new(mpath) rescue nil
|
|
if not dir
|
|
elog("Could not access migration path #{mpath}")
|
|
next
|
|
end
|
|
|
|
dir.entries.each do |ent|
|
|
next unless ent =~ /^\d+.*\.rb$/
|
|
::FileUtils.cp( ::File.join(mpath, ent), ::File.join(temp_dir, ent) )
|
|
end
|
|
end
|
|
|
|
success = true
|
|
begin
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
ActiveRecord::Migration.verbose = verbose
|
|
ActiveRecord::Migrator.migrate(temp_dir, nil)
|
|
}
|
|
rescue ::Exception => e
|
|
self.error = e
|
|
elog("DB.migrate threw an exception: #{e}")
|
|
dlog("Call stack:\n#{e.backtrace.join "\n"}")
|
|
success = false
|
|
end
|
|
|
|
::FileUtils.rm_rf(temp_dir)
|
|
|
|
return true
|
|
end
|
|
|
|
def workspace=(workspace)
|
|
@workspace_name = workspace.name
|
|
end
|
|
|
|
def workspace
|
|
framework.db.find_workspace(@workspace_name)
|
|
end
|
|
|
|
|
|
def purge_all_module_details
|
|
return if not self.migrated
|
|
return if self.modules_caching
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection do
|
|
Mdm::ModuleDetail.destroy_all
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
def update_all_module_details
|
|
return if not self.migrated
|
|
return if self.modules_caching
|
|
|
|
self.framework.cache_thread = Thread.current
|
|
|
|
self.modules_cached = false
|
|
self.modules_caching = true
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
|
|
refresh = []
|
|
skipped = []
|
|
|
|
Mdm::ModuleDetail.find_each do |md|
|
|
|
|
unless md.ready
|
|
refresh << md
|
|
next
|
|
end
|
|
|
|
unless md.file and ::File.exists?(md.file)
|
|
refresh << md
|
|
next
|
|
end
|
|
|
|
if ::File.mtime(md.file).to_i != md.mtime.to_i
|
|
refresh << md
|
|
next
|
|
end
|
|
|
|
skipped << [md.mtype, md.refname]
|
|
end
|
|
|
|
refresh.each {|md| md.destroy }
|
|
refresh = nil
|
|
|
|
[
|
|
[ 'exploit', framework.exploits ],
|
|
[ 'auxiliary', framework.auxiliary ],
|
|
[ 'post', framework.post ],
|
|
[ 'payload', framework.payloads ],
|
|
[ 'encoder', framework.encoders ],
|
|
[ 'nop', framework.nops ]
|
|
].each do |mt|
|
|
mt[1].keys.sort.each do |mn|
|
|
next if skipped.include?( [ mt[0], mn ] )
|
|
obj = mt[1].create(mn)
|
|
next if not obj
|
|
begin
|
|
update_module_details(obj)
|
|
rescue ::Exception
|
|
elog("Error updating module details for #{obj.fullname}: #{$!.class} #{$!}")
|
|
end
|
|
end
|
|
end
|
|
|
|
self.framework.cache_initialized = true
|
|
self.framework.cache_thread = nil
|
|
|
|
self.modules_cached = true
|
|
self.modules_caching = false
|
|
|
|
nil
|
|
|
|
}
|
|
end
|
|
|
|
def update_module_details(obj)
|
|
return if not self.migrated
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
info = module_to_details_hash(obj)
|
|
bits = info.delete(:bits) || []
|
|
|
|
md = Mdm::ModuleDetail.create(info)
|
|
bits.each do |args|
|
|
otype, vals = args
|
|
case otype
|
|
when :author
|
|
md.add_author(vals[:name], vals[:email])
|
|
when :action
|
|
md.add_action(vals[:name])
|
|
when :arch
|
|
md.add_arch(vals[:name])
|
|
when :platform
|
|
md.add_platform(vals[:name])
|
|
when :target
|
|
md.add_target(vals[:index], vals[:name])
|
|
when :ref
|
|
md.add_ref(vals[:name])
|
|
when :mixin
|
|
# md.add_mixin(vals[:name])
|
|
end
|
|
end
|
|
|
|
md.ready = true
|
|
md.save
|
|
md.id
|
|
|
|
}
|
|
end
|
|
|
|
def remove_module_details(mtype, refname)
|
|
return if not self.migrated
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
md = Mdm::ModuleDetail.find(:conditions => [ 'mtype = ? and refname = ?', mtype, refname])
|
|
md.destroy if md
|
|
}
|
|
end
|
|
|
|
def module_to_details_hash(m)
|
|
res = {}
|
|
bits = []
|
|
|
|
res[:mtime] = ::File.mtime(m.file_path) rescue Time.now
|
|
res[:file] = m.file_path
|
|
res[:mtype] = m.type
|
|
res[:name] = m.name.to_s
|
|
res[:refname] = m.refname
|
|
res[:fullname] = m.fullname
|
|
res[:rank] = m.rank.to_i
|
|
res[:license] = m.license.to_s
|
|
|
|
res[:description] = m.description.to_s.strip
|
|
|
|
m.arch.map{ |x|
|
|
bits << [ :arch, { :name => x.to_s } ]
|
|
}
|
|
|
|
m.platform.platforms.map{ |x|
|
|
bits << [ :platform, { :name => x.to_s.split('::').last.downcase } ]
|
|
}
|
|
|
|
m.author.map{|x|
|
|
bits << [ :author, { :name => x.to_s } ]
|
|
}
|
|
|
|
m.references.map do |r|
|
|
bits << [ :ref, { :name => [r.ctx_id.to_s, r.ctx_val.to_s].join("-") } ]
|
|
end
|
|
|
|
res[:privileged] = m.privileged?
|
|
|
|
|
|
if m.disclosure_date
|
|
begin
|
|
res[:disclosure_date] = m.disclosure_date.to_datetime.to_time
|
|
rescue ::Exception
|
|
res.delete(:disclosure_date)
|
|
end
|
|
end
|
|
|
|
if(m.type == "exploit")
|
|
|
|
m.targets.each_index do |i|
|
|
bits << [ :target, { :index => i, :name => m.targets[i].name.to_s } ]
|
|
end
|
|
|
|
if (m.default_target)
|
|
res[:default_target] = m.default_target
|
|
end
|
|
|
|
# Some modules are a combination, which means they are actually aggressive
|
|
res[:stance] = m.stance.to_s.index("aggressive") ? "aggressive" : "passive"
|
|
|
|
|
|
m.class.mixins.each do |x|
|
|
bits << [ :mixin, { :name => x.to_s } ]
|
|
end
|
|
end
|
|
|
|
if(m.type == "auxiliary")
|
|
|
|
m.actions.each_index do |i|
|
|
bits << [ :action, { :name => m.actions[i].name.to_s } ]
|
|
end
|
|
|
|
if (m.default_action)
|
|
res[:default_action] = m.default_action.to_s
|
|
end
|
|
|
|
res[:stance] = m.passive? ? "passive" : "aggressive"
|
|
end
|
|
|
|
res[:bits] = bits
|
|
|
|
res
|
|
end
|
|
|
|
|
|
|
|
#
|
|
# This provides a standard set of search filters for every module.
|
|
# The search terms are in the form of:
|
|
# {
|
|
# "text" => [ [ "include_term1", "include_term2", ...], [ "exclude_term1", "exclude_term2"], ... ],
|
|
# "cve" => [ [ "include_term1", "include_term2", ...], [ "exclude_term1", "exclude_term2"], ... ]
|
|
# }
|
|
#
|
|
# Returns true on no match, false on match
|
|
#
|
|
def search_modules(search_string, inclusive=false)
|
|
return false if not search_string
|
|
|
|
search_string += " "
|
|
|
|
# Split search terms by space, but allow quoted strings
|
|
terms = Shellwords.shellwords(search_string)
|
|
terms.delete('')
|
|
|
|
# All terms are either included or excluded
|
|
res = {}
|
|
|
|
terms.each do |t|
|
|
f,v = t.split(":", 2)
|
|
if not v
|
|
v = f
|
|
f = 'text'
|
|
end
|
|
next if v.length == 0
|
|
f.downcase!
|
|
v.downcase!
|
|
res[f] ||= [ ]
|
|
res[f] << v
|
|
end
|
|
|
|
::ActiveRecord::Base.connection_pool.with_connection {
|
|
|
|
where_q = []
|
|
where_v = []
|
|
|
|
res.keys.each do |kt|
|
|
res[kt].each do |kv|
|
|
kv = kv.downcase
|
|
case kt
|
|
when 'text'
|
|
xv = "%#{kv}%"
|
|
where_q << ' ( ' +
|
|
'module_details.fullname ILIKE ? OR module_details.name ILIKE ? OR module_details.description ILIKE ? OR ' +
|
|
'module_authors.name ILIKE ? OR module_actions.name ILIKE ? OR module_archs.name ILIKE ? OR ' +
|
|
'module_targets.name ILIKE ? OR module_platforms.name ILIKE ? OR module_refs.name ILIKE ?' +
|
|
') '
|
|
where_v << [ xv, xv, xv, xv, xv, xv, xv, xv, xv ]
|
|
when 'name'
|
|
xv = "%#{kv}%"
|
|
where_q << ' ( module_details.fullname ILIKE ? OR module_details.name ILIKE ? ) '
|
|
where_v << [ xv, xv ]
|
|
when 'author'
|
|
xv = "%#{kv}%"
|
|
where_q << ' ( module_authors.name ILIKE ? OR module_authors.email ILIKE ? ) '
|
|
where_v << [ xv, xv ]
|
|
when 'os','platform'
|
|
xv = "%#{kv}%"
|
|
where_q << ' ( module_platforms.name ILIKE ? OR module_targets.name ILIKE ? ) '
|
|
where_v << [ xv, xv ]
|
|
when 'port'
|
|
# TODO
|
|
when 'type'
|
|
where_q << ' ( module_details.mtype = ? ) '
|
|
where_v << [ kv ]
|
|
when 'app'
|
|
where_q << ' ( module_details.stance = ? )'
|
|
where_v << [ ( kv == "client") ? "passive" : "active" ]
|
|
when 'ref'
|
|
where_q << ' ( module_refs.name ILIKE ? )'
|
|
where_v << [ '%' + kv + '%' ]
|
|
when 'cve','bid','osvdb','edb'
|
|
where_q << ' ( module_refs.name = ? )'
|
|
where_v << [ kt.upcase + '-' + kv ]
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
qry = Mdm::ModuleDetail.select("DISTINCT(module_details.*)").
|
|
joins(
|
|
"LEFT OUTER JOIN module_authors ON module_details.id = module_authors.module_detail_id " +
|
|
"LEFT OUTER JOIN module_actions ON module_details.id = module_actions.module_detail_id " +
|
|
"LEFT OUTER JOIN module_archs ON module_details.id = module_archs.module_detail_id " +
|
|
"LEFT OUTER JOIN module_refs ON module_details.id = module_refs.module_detail_id " +
|
|
"LEFT OUTER JOIN module_targets ON module_details.id = module_targets.module_detail_id " +
|
|
"LEFT OUTER JOIN module_platforms ON module_details.id = module_platforms.module_detail_id "
|
|
).
|
|
where(where_q.join(inclusive ? " OR " : " AND "), *(where_v.flatten)).
|
|
# Compatibility for Postgres installations prior to 9.1 - doesn't have support for wildcard group by clauses
|
|
group("module_details.id, module_details.mtime, module_details.file, module_details.mtype, module_details.refname, module_details.fullname, module_details.name, module_details.rank, module_details.description, module_details.license, module_details.privileged, module_details.disclosure_date, module_details.default_target, module_details.default_action, module_details.stance, module_details.ready")
|
|
|
|
res = qry.all
|
|
|
|
}
|
|
end
|
|
|
|
end
|
|
end
|