f4476cb1b7
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]
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
|