Files
metasploit-gs/lib/msf/core/db_manager.rb
T
James Lee f4476cb1b7 Really fix payload recalculation
Instead of deleting all non-symbolics before the re-adding phase of
PayloadSet#recalculate, store a list of old module names, populate a
list of new ones during the re-adding phase, and finally remove any
non-symbolic module that was in the old list but wasn't in the new list.

Also includes a minor refactoring to make ModuleManager its own thing
instead of being an awkard subclass of ModuleSet. Now PayloadSet doesn't
need to know about the existence of framework.modules, which makes the
separation a little more natural.

[FixRM #7037]
2012-12-03 22:23:40 -06:00

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