diff --git a/.gitignore b/.gitignore index 36b7be3192..da36986a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ # Rubymine project directory .idea +.yardoc # Mac OS X files .DS_Store data/meterpreter/ext_server_pivot.dll data/meterpreter/ext_server_pivot.x64.dll +doc external/source/meterpreter/java/bin external/source/meterpreter/java/build external/source/meterpreter/java/extensions diff --git a/.rvmrc b/.rvmrc new file mode 100644 index 0000000000..1c4dccf49c --- /dev/null +++ b/.rvmrc @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# This is an RVM Project .rvmrc file, used to automatically load the ruby +# development environment upon cd'ing into the directory + +# First we specify our desired [@], the @gemset name is optional, +# Only full ruby name is supported here, for short names use: +# echo "rvm use 1.9.3" > .rvmrc +environment_id="ruby-1.9.3-p194@metasploit-framework" + +# Uncomment the following lines if you want to verify rvm version per project +# rvmrc_rvm_version="1.15.7 (stable)" # 1.10.1 seams as a safe start +# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || { +# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading." +# return 1 +# } + +# First we attempt to load the desired environment directly from the environment +# file. This is very fast and efficient compared to running through the entire +# CLI and selector. If you want feedback on which environment was used then +# insert the word 'use' after --create as this triggers verbose mode. +if [[ -d "${rvm_path:-$HOME/.rvm}/environments" + && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]] +then + \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id" + [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] && + \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true +else + # If the environment file has not yet been created, use the RVM CLI to select. + rvm --create "$environment_id" || { + echo "Failed to create RVM environment '${environment_id}'." + return 1 + } +fi + +# If you use bundler, this might be useful to you: +# if [[ -s Gemfile ]] && { +# ! builtin command -v bundle >/dev/null || +# builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null +# } +# then +# printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n" +# gem install bundler +# fi +# if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null +# then +# bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete' +# fi diff --git a/Gemfile b/Gemfile index 8a97cbf4d2..9a7b19a40f 100755 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,10 @@ source 'http://rubygems.org' -gem 'rails', '3.2.2' -gem 'metasploit_data_models', '0.0.2', :git => "git://github.com/rapid7/metasploit_data_models.git" -gem 'pg', '>=0.13' -gem 'msgpack' -gem 'nokogiri' + +group :development do + # running documention generation tasks + gem 'rake' + # Markdown formatting for yara + gem 'redcarpet' + # generating documention + gem 'yard' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..6a0bbe0f53 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,14 @@ +GEM + remote: http://rubygems.org/ + specs: + rake (0.9.2.2) + redcarpet (2.1.1) + yard (0.8.2.1) + +PLATFORMS + ruby + +DEPENDENCIES + rake + redcarpet + yard diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..b01da54239 --- /dev/null +++ b/Rakefile @@ -0,0 +1,43 @@ +require 'bundler/setup' + +require 'yard' + +namespace :yard do + yard_files = [ + # Ruby source files first + 'lib/**/*.rb', + # Anything after '-' is a normal documentation, not source + '-', + 'COPYING', + 'HACKING', + 'THIRD-PARTY.md' + ] + yard_options = [ + # don't generate documentation from the source of the gems in the gemcache. + '--exclude', 'lib/gemcache', + # include documentation for protected methods for developers extending the code. + '--protected' + ] + + YARD::Rake::YardocTask.new(:doc) do |t| + t.files = yard_files + # --no-stats here as 'stats' task called after will print fuller stats + t.options = yard_options + ['--no-stats'] + + t.after = Proc.new { + Rake::Task['yard:stats'].execute + } + end + + desc "Shows stats for YARD Documentation including listing undocumented modules, classes, constants, and methods" + task :stats => :environment do + stats = YARD::CLI::Stats.new + yard_arguments = yard_options + ['--compact', '--list-undoc'] + yard_files + stats.run *yard_arguments + end +end + +# @todo Figure out how to just clone description from yard:doc +desc "Generate YARD documentation" +# allow calling namespace to as a task that goes to default task for namespace +task :yard => ['yard:doc'] diff --git a/lib/msf/core.rb b/lib/msf/core.rb index 64cc36454c..5cbaf19efa 100644 --- a/lib/msf/core.rb +++ b/lib/msf/core.rb @@ -35,6 +35,7 @@ require 'msf/core/framework' require 'msf/core/db_manager' require 'msf/core/event_dispatcher' require 'msf/core/module_manager' +require 'msf/core/module_set' require 'msf/core/plugin_manager' require 'msf/core/session' require 'msf/core/session_manager' diff --git a/lib/msf/core/module.rb b/lib/msf/core/module.rb index 5ed09db4fc..a4449e04ab 100644 --- a/lib/msf/core/module.rb +++ b/lib/msf/core/module.rb @@ -78,14 +78,6 @@ class Module # The path from which the module was loaded. # attr_accessor :file_path - - # - # Override the default Class#inspect which is useless for the way - # modules get loaded - # - def inspect - "#" - end end # diff --git a/lib/msf/core/module_manager.rb b/lib/msf/core/module_manager.rb index 3ce78cee7c..7d071e9bb5 100644 --- a/lib/msf/core/module_manager.rb +++ b/lib/msf/core/module_manager.rb @@ -1,1188 +1,174 @@ # -*- coding: binary -*- -require 'msf/core' -require 'fastlib' +# +# Core +# require 'pathname' +# +# Project +# +require 'fastlib' +require 'msf/core' +require 'msf/core/module_set' + module Msf - -# -# Define used for a place-holder module that is used to indicate that the -# module has not yet been demand-loaded. Soon to go away. -# -SymbolicModule = "__SYMBOLIC__" - - -### -# -# A module set contains zero or more named module classes of an arbitrary -# type. -# -### -class ModuleSet < Hash - - include Framework::Offspring - - # - # Initializes a module set that will contain modules of a specific type and - # expose the mechanism necessary to create instances of them. - # - def initialize(type = nil) - self.module_type = type - - # Hashes that convey the supported architectures and platforms for a - # given module - self.mod_arch_hash = {} - self.mod_platform_hash = {} - self.mod_sorted = nil - self.mod_ranked = nil - self.mod_extensions = [] - self.mod_ambiguous = {} - end - - # - # Wrapper that detects if a symbolic module is in use. If it is, it - # creates an instance to demand load the module and then returns the - # now-loaded class afterwords. - # - def [](name) - if (get_hash_val(name) == SymbolicModule) - create(name) - end - - get_hash_val(name) - end - - # - # Returns the hash value associated with the supplied module name without - # throwing an exception. - # - def get_hash_val(name) - fetch(name) if has_key?(name) - end - - # - # Create an instance of the supplied module by its name - # - def create(name) - - klass = get_hash_val(name) - instance = nil - - # If there is no module associated with this class, then try to demand - # load it. - if (klass.nil? or klass == SymbolicModule) - # If we are the root module set, then we need to try each module - # type's demand loading until we find one that works for us. - if (module_type.nil?) - MODULE_TYPES.each { |type| - framework.modules.demand_load_module(type, name) - } - else - framework.modules.demand_load_module(module_type, name) - end - - recalculate - - klass = get_hash_val(name) - end - - - - # If the klass is valid for this name, try to create it - if (klass and klass != SymbolicModule) - instance = klass.new - end - - # Notify any general subscribers of the creation event - if (instance) - self.framework.events.on_module_created(instance) - end - - return instance - end - - # - # Checks to see if the supplied module name is valid. - # - def valid?(name) - create(name) - (self[name]) ? true : false - end - - # - # Overrides the builtin 'each' operator to avoid the following exception on Ruby 1.9.2+ - # "can't add a new key into hash during iteration" - # - def each(&block) - list = [] - self.keys.sort.each do |sidx| - list << [sidx, self[sidx]] - end - list.each(&block) - end - - # - # Enumerates each module class in the set. - # - def each_module(opts = {}, &block) - demand_load_modules - - self.mod_sorted = self.sort - - each_module_list(mod_sorted, opts, &block) - end - - # - # Enumerates each module class in the set based on their relative ranking - # to one another. Modules that are ranked higher are shown first. - # - def each_module_ranked(opts = {}, &block) - demand_load_modules - - self.mod_ranked = rank_modules - - each_module_list(mod_ranked, opts, &block) - end - - # - # Custom each_module filtering if an advanced set supports doing extended - # filtering. Returns true if the entry should be filtered. - # - def each_module_filter(opts, name, entry) - return false - end - - # - # Dummy placeholder to relcalculate aliases and other fun things. - # - def recalculate - end - - # - # Gives the module set an opportunity to handle a module reload event - # - def on_module_reload(mod) - end - - # - # Forces all modules in this set to be loaded. - # - def force_load_set - each_module { |name, mod| } - end - - attr_reader :module_type - - # - # Whether or not recalculations should be postponed. This is used from the - # context of the each_module_list handler in order to prevent the demand - # loader from calling recalc for each module if it's possible that more - # than one module may be loaded. This field is not initialized until used. - # - attr_accessor :postpone_recalc - -protected - - # - # Load all modules that are marked as being symbolic. - # - def demand_load_modules - # Pre-scan the module list for any symbolic modules - self.each_pair { |name, mod| - if (mod == SymbolicModule) - self.postpone_recalc = true - - mod = create(name) - - next if (mod.nil?) - end - } - - # If we found any symbolic modules, then recalculate. - if (self.postpone_recalc) - self.postpone_recalc = false - - recalculate - end - end - - # - # Enumerates the modules in the supplied array with possible limiting - # factors. - # - def each_module_list(ary, opts, &block) - ary.each { |entry| - name, mod = entry - - # Skip any lingering symbolic modules. - next if (mod == SymbolicModule) - - # Filter out incompatible architectures - if (opts['Arch']) - if (!mod_arch_hash[mod]) - mod_arch_hash[mod] = mod.new.arch - end - - next if ((mod_arch_hash[mod] & opts['Arch']).empty? == true) - end - - # Filter out incompatible platforms - if (opts['Platform']) - if (!mod_platform_hash[mod]) - mod_platform_hash[mod] = mod.new.platform - end - - next if ((mod_platform_hash[mod] & opts['Platform']).empty? == true) - end - - # Custom filtering - next if (each_module_filter(opts, name, entry) == true) - - block.call(name, mod) - } - end - - - # - # Ranks modules based on their constant rank value, if they have one. - # - def rank_modules - self.mod_ranked = self.sort { |a, b| - a_name, a_mod = a - b_name, b_mod = b - - # Dynamically loads the module if needed - a_mod = create(a_name) if a_mod == SymbolicModule - b_mod = create(b_name) if b_mod == SymbolicModule - - # Extract the ranking between the two modules - a_rank = a_mod.const_defined?('Rank') ? a_mod.const_get('Rank') : NormalRanking - b_rank = b_mod.const_defined?('Rank') ? b_mod.const_get('Rank') : NormalRanking - - # Compare their relevant rankings. Since we want highest to lowest, - # we compare b_rank to a_rank in terms of higher/lower precedence - b_rank <=> a_rank - } - end - - # - # Adds a module with a the supplied name. - # - def add_module(mod, name, modinfo = nil) - - - # Set the module's name so that it can be referenced when - # instances are created. - mod.framework = framework - mod.refname = name - mod.file_path = ((modinfo and modinfo['files']) ? modinfo['files'][0] : nil) - mod.orig_cls = mod - - if (get_hash_val(name) and get_hash_val(name) != SymbolicModule) - mod_ambiguous[name] = true - - wlog("The module #{mod.refname} is ambiguous with #{self[name].refname}.") - else - self[name] = mod - end - - mod - end - - attr_writer :module_type - attr_accessor :mod_arch_hash, :mod_platform_hash - attr_accessor :mod_sorted, :mod_ranked - attr_accessor :mod_extensions, :mod_ambiguous - attr_accessor :module_history - -end - -### -# -# Upper management decided to throw in some middle management -# because the modules were getting out of hand. This bad boy -# takes care of the work of managing the interaction with -# modules in terms of loading and instantiation. -# -# TODO: -# -# - add unload support -# -### -class ModuleManager < ModuleSet - - require 'msf/core/payload_set' - - include Framework::Offspring - - # - # Initializes an instance of the overall module manager using the supplied - # framework instance. The types parameter can be used to only load specific - # module types on initialization - # - def initialize(framework,types=MODULE_TYPES) - self.module_paths = [] - self.module_sets = {} - self.module_failed = {} - self.enabled_types = {} - self.framework = framework - self.cache = {} - - types.each { |type| - init_module_set(type) - } - - super(nil) - end - - def init_module_set(type) - self.enabled_types[type] = true - case type - when MODULE_PAYLOAD - instance = PayloadSet.new(self) - else - instance = ModuleSet.new(type) - end - - self.module_sets[type] = instance - - # Set the module set's framework reference - instance.framework = self.framework - end - - # - # Creates a module using the supplied name. - # - def create(name) - # Check to see if it has a module type prefix. If it does, - # try to load it from the specific module set for that type. - if (md = name.match(/^(#{MODULE_TYPES.join('|')})\/(.*)$/)) - module_sets[md[1]].create(md[2]) - # Otherwise, just try to load it by name. - else - super - end - end - - # - # Accessors by module type - # - - # - # Returns all of the modules of the specified type - # - def module_set(type) - module_sets[type] - end - - # - # Returns the set of loaded encoder module classes. - # - def encoders - module_set(MODULE_ENCODER) - end - - - # - # Returns the set of loaded exploit module classes. - # - def exploits - module_set(MODULE_EXPLOIT) - end - - # - # Returns the set of loaded nop module classes. - # - def nops - module_set(MODULE_NOP) - end - - # - # Returns the set of loaded payload module classes. - # - def payloads - module_set(MODULE_PAYLOAD) - end - - # - # Returns the set of loaded auxiliary module classes. - # - def auxiliary - module_set(MODULE_AUX) - end - - # - # Returns the set of loaded auxiliary module classes. - # - def post - module_set(MODULE_POST) - end - - # - # Returns the set of modules that failed to load. - # - def failed - return module_failed - end - - ## - # - # Module path management - # - ## - - # - # Adds a path to be searched for new modules. - # - def add_module_path(path) - npaths = [] - - if path =~ /\.fastlib$/ - unless ::File.exist?(path) - raise RuntimeError, "The path supplied does not exist", caller - end - npaths << ::File.expand_path(path) - else - path.sub!(/#{File::SEPARATOR}$/, '') - - # Make the path completely canonical - path = Pathname.new(File.expand_path(path)) - - # Make sure the path is a valid directory - unless path.directory? - raise RuntimeError, "The path supplied is not a valid directory.", caller - end - - # Now that we've confirmed it exists, get the full, cononical path - path = ::File.expand_path(path) - npaths << path - - # Identify any fastlib archives inside of this path - Dir["#{path}/**/*.fastlib"].each do |fp| - npaths << fp - end - end - - # Update the module paths appropriately - self.module_paths = (module_paths + npaths).flatten.uniq - - # Load all of the modules from the new paths - counts = nil - npaths.each { |d| - counts = load_modules(d, false) - } - - return counts - end - - # - # Removes a path from which to search for modules. - # - def remove_module_path(path) - module_paths.delete(path) - module_paths.delete(::File.expand_path(path)) - end - - def register_type_extension(type, ext) - end - - # - # Reloads modules from all module paths - # - def reload_modules - - self.module_history = {} - self.clear - - self.enabled_types.each_key do |type| - module_sets[type].clear - init_module_set(type) - end - - # The number of loaded modules in the following categories: - # auxiliary/encoder/exploit/nop/payload/post - count = 0 - module_paths.each do |path| - mods = load_modules(path, true) - mods.each_value {|c| count += c} - end - - rebuild_cache - - count - end - - # - # Reloads the module specified in mod. This can either be an instance of a - # module or a module class. - # - def reload_module(mod) - omod = mod - refname = mod.refname - ds = mod.datastore - - dlog("Reloading module #{refname}...", 'core') - - # Set the target file - file = mod.file_path - wrap = ::Module.new - - # Load the module into a new Module wrapper - begin - wrap.module_eval(load_module_source(file), file) - if(wrap.const_defined?(:RequiredVersions)) - mins = wrap.const_get(:RequiredVersions) - if( mins[0] > ::Msf::Framework::VersionCore or - mins[1] > ::Msf::Framework::VersionAPI - ) - errmsg = "Failed to load module from #{file} due to version check (requires Core:#{mins[0]} API:#{mins[1]})" - elog(errmsg) - self.module_failed[mod.file_path] = errmsg - return false - end - end - rescue ::Exception => e - - # Hide eval errors when the module version is not compatible - if(wrap.const_defined?(:RequiredVersions)) - mins = wrap.const_get(:RequiredVersions) - if( mins[0] > ::Msf::Framework::VersionCore or - mins[1] > ::Msf::Framework::VersionAPI - ) - errmsg = "Failed to reload module from #{file} due to version check (requires Core:#{mins[0]} API:#{mins[1]})" - elog(errmsg) - self.module_failed[mod.file_path] = errmsg - return - end - end - - errmsg = "Failed to reload module from #{file}: #{e.class} #{e}" - elog(errmsg) - self.module_failed[mod.file_path] = errmsg - return - end - - added = nil - ::Msf::Framework::Major.downto(1) do |major| - if wrap.const_defined?("Metasploit#{major}") - added = wrap.const_get("Metasploit#{major}") - break - end - end - - if not added - errmsg = "Reloaded file did not contain a valid module (#{file})." - elog(errmsg) - self.module_failed[mod.file_path] = errmsg - return nil - end - - self.module_failed.delete(mod.file_path) - - # Remove the original reference to this module - self.delete(mod.refname) - - # Indicate that the module is being loaded again so that any necessary - # steps can be taken to extend it properly. - on_module_load(added, mod.type, refname, { - 'files' => [ mod.file_path ], - 'noup' => true}) - - # Create a new instance of the module - if (mod = create(refname)) - mod.datastore.update(ds) - else - elog("Failed to create instance of #{refname} after reload.", 'core') - # Return the old module instance to avoid a strace trace - return omod - end - - # Let the specific module sets have an opportunity to handle the fact - # that this module was reloaded. - module_sets[mod.type].on_module_reload(mod) - - # Rebuild the cache for just this module - rebuild_cache(mod) - - mod - end - - # - # Overrides the module set method for adding a module so that some extra - # steps can be taken to subscribe the module and notify the event - # dispatcher. - # - def add_module(mod, name, file_paths) - # Call the module set implementation of add_module - dup = super - - # Automatically subscribe a wrapper around this module to the necessary - # event providers based on whatever events it wishes to receive. We - # only do this if we are the module manager instance, as individual - # module sets need not subscribe. - auto_subscribe_module(dup) - - # Notify the framework that a module was loaded - framework.events.on_module_load(name, dup) - end - - # - # Provide a list of the types of modules in the set - # - def module_types - module_sets.keys.dup - end - - # - # Provide a list of module names of a specific type - # - def module_names(set) - module_sets[set] ? module_sets[set].keys.dup : [] - end - - # - # Read the module code from the file on disk - # - def load_module_source(file) - ::File.read(file, ::File.size(file)) - end - - # - # Rebuild the cache for the module set - # - def rebuild_cache(mod = nil) - return if not (framework.db and framework.db.migrated) - if mod - framework.db.update_module_details(mod) - else - framework.db.update_all_module_details - end - refresh_cache - end - - # - # Return a listing of all cached modules - # - def cache_entries - return {} if not (framework.db and framework.db.migrated) - res = {} - ::Mdm::ModuleDetail.find(:all).each do |m| - res[m.file] = { :mtype => m.mtype, :refname => m.refname, :file => m.file, :mtime => m.mtime } - unless module_set(m.mtype).has_key?(m.refname) - module_set(m.mtype)[m.refname] = SymbolicModule - end - end - - res - end - - # - # Reset the module cache - # - def refresh_cache - self.cache = cache_entries - end - - def has_module_file_changed?(file) - begin - cfile = self.cache[file] - return true if not cfile - - # Payloads can't be cached due to stage/stager matching - return true if cfile[:mtype] == "payload" - return cfile[:mtime].to_i != ::File.mtime(file).to_i - rescue ::Errno::ENOENT - return true - end - end - - def has_archive_file_changed?(arch, file) - begin - cfile = self.cache[file] - return true if not cfile - - # Payloads can't be cached due to stage/stager matching - return true if cfile[:mtype] == "payload" - - return cfile[:mtime].to_i != ::File.mtime(file).to_i - rescue ::Errno::ENOENT - return true - end - end - - def demand_load_module(mtype, mname) - n = self.cache.keys.select { |k| - self.cache[k][:mtype] == mtype and - self.cache[k][:refname] == mname - }.first - - return nil unless n - m = self.cache[n] - - path = nil - if m[:file] =~ /^(.*)\/#{m[:mtype]}s?\// - path = $1 - load_module_from_file(path, m[:file], nil, nil, nil, true) - else - dlog("Could not demand load module #{mtype}/#{mname} (unknown base name in #{m[:file]})", 'core', LEV_2) - nil - end - end - - attr_accessor :cache # :nodoc: - -protected - - - # - # Load all of the modules from the supplied directory or archive - # - def load_modules(bpath, demand = false) - ( bpath =~ /\.fastlib$/ ) ? - load_modules_from_archive(bpath, demand) : - load_modules_from_directory(bpath, demand) - end - - # - # Load all of the modules from the supplied module path (independent of - # module type). - # - def load_modules_from_directory(bpath, demand = false) - loaded = {} - recalc = {} - counts = {} - delay = {} - ks = true - - dbase = ::Dir.new(bpath) - dbase.entries.each do |ent| - next if ent.downcase == '.svn' - - path = ::File.join(bpath, ent) - mtype = ent.gsub(/s$/, '') - - next if not ::File.directory?(path) - next if not MODULE_TYPES.include?(mtype) - next if not enabled_types[mtype] - - # Try to load modules from all the files in the supplied path - Rex::Find.find(path) do |file| - - # Skip non-ruby files - next if file[-3,3] != ".rb" - - # Skip unit test files - next if (file =~ /rb\.(ut|ts)\.rb$/) - - # Skip files with a leading period - next if file[0,1] == "." - - load_module_from_file(bpath, file, loaded, recalc, counts, demand) - end - end - - recalc.each_key do |mtype| - module_set(mtype).recalculate - end - - # Return per-module loaded counts - return counts - end - - - # - # Load all of the modules from the supplied fastlib archive - # - def load_modules_from_archive(bpath, demand = false) - loaded = {} - recalc = {} - counts = {} - delay = {} - ks = true - - ::FastLib.list(bpath).each do |ent| - - next if ent.index(".svn/") - - mtype, path = ent.split("/", 2) - mtype.sub!(/s$/, '') - - next if not MODULE_TYPES.include?(mtype) - next if not enabled_types[mtype] - - # Skip non-ruby files - next if ent[-3,3] != ".rb" - - # Skip unit test files - next if (ent =~ /rb\.(ut|ts)\.rb$/) - - # Skip files with a leading period - next if ent[0,1] == "." - - load_module_from_archive(bpath, ent, loaded, recalc, counts, demand) - end - - recalc.each_key do |mtype| - module_set(mtype).recalculate - end - - # Return per-module loaded counts - return counts - end - - # - # Loads a module from the supplied file. - # - def load_module_from_file(path, file, loaded, recalc, counts, demand = false) - - if not ( demand or has_module_file_changed?(file)) - dlog("Cached module from file #{file} has not changed.", 'core', LEV_2) - return false - end - - # Substitute the base path - path_base = file.sub(path + File::SEPARATOR, '') - - # Derive the name from the path with the exclusion of the .rb - name = path_base.match(/^(.+?)#{File::SEPARATOR}(.*)(.rb?)$/)[2] - - # Chop off the file name - path_base.sub!(/(.+)(#{File::SEPARATOR}.+)(.rb?)$/, '\1') - - if (m = path_base.match(/^(.+?)#{File::SEPARATOR}+?/)) - type = m[1] - else - type = path_base - end - - type.sub!(/s$/, '') - - - added = nil - - wrap = self.class.wrapper_module(name) - - begin - wrap.module_eval(load_module_source(file), file) - if(wrap.const_defined?(:RequiredVersions)) - mins = wrap.const_get(:RequiredVersions) - if( mins[0] > ::Msf::Framework::VersionCore or - mins[1] > ::Msf::Framework::VersionAPI - ) - errmsg = "Failed to load module from #{file} due to version check (requires Core:#{mins[0]} API:#{mins[1]})" - elog(errmsg) - self.module_failed[file] = errmsg - return false - end - end - rescue ::Interrupt - raise $! - rescue ::Exception => e - # Hide eval errors when the module version is not compatible - if(wrap.const_defined?(:RequiredVersions)) - mins = wrap.const_get(:RequiredVersions) - if( mins[0] > ::Msf::Framework::VersionCore or - mins[1] > ::Msf::Framework::VersionAPI - ) - errmsg = "Failed to load module from #{file} due to error and failed version check (requires Core:#{mins[0]} API:#{mins[1]})" - elog(errmsg) - self.module_failed[file] = errmsg - return false - end - end - errmsg = "#{e.class} #{e}" - self.module_failed[file] = errmsg - elog(errmsg) - return false - end - - ::Msf::Framework::Major.downto(1) do |major| - if wrap.const_defined?("Metasploit#{major}") - added = wrap.const_get("Metasploit#{major}") - break - end - end - - if not added - errmsg = "Missing Metasploit class constant" - self.module_failed[file] = errmsg - elog(errmsg) - return false - end - - # If the module indicates that it is not usable on this system, then we - # will not try to use it. - usable = false - - begin - usable = respond_to?(:is_usable) ? added.is_usable : true - rescue - elog("Exception caught during is_usable check: #{$!}") - end - - if (usable == false) - ilog("Skipping module in #{file} because is_usable returned false.", 'core', LEV_1) - return false - end - - ilog("Loaded #{type} module #{added} from #{file}.", 'core', LEV_2) - self.module_failed.delete(file) - - # Do some processing on the loaded module to get it into the - # right associations - on_module_load(added, type, name, { - 'files' => [ file ], - 'paths' => [ path ], - 'type' => type }) - - # Set this module type as needing recalculation - recalc[type] = true if (recalc) - - # Append the added module to the hash of file->module - loaded[file] = added if (loaded) - - # The number of loaded modules this round - if (counts) - counts[type] = (counts[type]) ? (counts[type] + 1) : 1 - end - - return true - end - - - # - # Loads a module from the supplied archive path - # - def load_module_from_archive(path, file, loaded, recalc, counts, demand = false) - - if not ( demand or has_archive_module_file_changed?(file)) - dlog("Cached module from file #{file} has not changed.", 'core', LEV_2) - return false - end - - # Derive the name from the path with the exclusion of the .rb - name = file.match(/^(.+?)#{File::SEPARATOR}(.*)(.rb?)$/)[2] - - # Chop off the file name - base = file.sub(/(.+)(#{File::SEPARATOR}.+)(.rb?)$/, '\1') - - if (m = base.match(/^(.+?)#{File::SEPARATOR}+?/)) - type = m[1] - else - type = base - end - - type.sub!(/s$/, '') - - added = nil - - begin - wrap = ::Module.new - wrap.module_eval( ::FastLib.load(path, file), file ) - if(wrap.const_defined?(:RequiredVersions)) - mins = wrap.const_get(:RequiredVersions) - if( mins[0] > ::Msf::Framework::VersionCore or - mins[1] > ::Msf::Framework::VersionAPI - ) - errmsg = "Failed to load module from #{path}::#{file} due to version check (requires Core:#{mins[0]} API:#{mins[1]})" - elog(errmsg) - self.module_failed[file] = errmsg - return false - end - end - rescue ::Interrupt - raise $! - rescue ::Exception => e - # Hide eval errors when the module version is not compatible - if(wrap.const_defined?(:RequiredVersions)) - mins = wrap.const_get(:RequiredVersions) - if( mins[0] > ::Msf::Framework::VersionCore or - mins[1] > ::Msf::Framework::VersionAPI - ) - errmsg = "Failed to load module from #{path}::#{file}due to error and failed version check (requires Core:#{mins[0]} API:#{mins[1]})" - elog(errmsg) - self.module_failed[file] = errmsg - return false - end - end - errmsg = "#{e.class} #{e}" - self.module_failed[file] = errmsg - elog(errmsg) - return false - end - - ::Msf::Framework::Major.downto(1) do |major| - if wrap.const_defined?("Metasploit#{major}") - added = wrap.const_get("Metasploit#{major}") - break - end - end - - if not added - errmsg = "Missing Metasploit class constant" - self.module_failed[file] = errmsg - elog(errmsg) - return false - end - - # If the module indicates that it is not usable on this system, then we - # will not try to use it. - usable = false - - begin - usable = respond_to?(:is_usable) ? added.is_usable : true - rescue - elog("Exception caught during is_usable check: #{$!}") - end - - if (usable == false) - ilog("Skipping module in #{path}::#{file} because is_usable returned false.", 'core', LEV_1) - return false - end - - ilog("Loaded #{type} module #{added} from #{path}::#{file}.", 'core', LEV_2) - self.module_failed.delete(file) - - # Do some processing on the loaded module to get it into the - # right associations - on_module_load(added, type, name, { - 'files' => [ file ], - 'paths' => [ path ], - 'type' => type }) - - # Set this module type as needing recalculation - recalc[type] = true if (recalc) - - # Append the added module to the hash of file->module - loaded[file] = added if (loaded) - - # The number of loaded modules this round - if (counts) - counts[type] = (counts[type]) ? (counts[type] + 1) : 1 - end - - return true - end - - - # - # Called when a module is initially loaded such that it can be - # categorized accordingly. - # - def on_module_load(mod, type, name, modinfo) - # Payload modules require custom loading as the individual files - # may not directly contain a logical payload that a user would - # reference, such as would be the case with a payload stager or - # stage. As such, when payload modules are loaded they are handed - # off to a special payload set. The payload set, in turn, will - # automatically create all the permutations after all the payload - # modules have been loaded. - - if (type != MODULE_PAYLOAD) - # Add the module class to the list of modules and add it to the - # type separated set of module classes - add_module(mod, name, modinfo) - end - - module_sets[type].add_module(mod, name, modinfo) - end - - # - # This method automatically subscribes a module to whatever event providers - # it wishes to monitor. This can be used to allow modules to automatically - # execute or perform other tasks when certain events occur. For instance, - # when a new host is detected, other aux modules may wish to run such - # that they can collect more information about the host that was detected. - # - def auto_subscribe_module(mod) - # If auto-subscribe has been disabled - if (framework.datastore['DisableAutoSubscribe'] and - framework.datastore['DisableAutoSubscribe'] =~ /^(y|1|t)/) - return - end - - # If auto-subscription is enabled (which it is by default), figure out - # if it subscribes to any particular interfaces. - inst = nil - - # - # Exploit event subscriber check - # - if (mod.include?(ExploitEvent) == true) - framework.events.add_exploit_subscriber((inst) ? inst : (inst = mod.new)) - end - - # - # Session event subscriber check - # - if (mod.include?(SessionEvent) == true) - framework.events.add_session_subscriber((inst) ? inst : (inst = mod.new)) - end - end - - attr_accessor :modules, :module_sets # :nodoc: - attr_accessor :module_paths # :nodoc: - attr_accessor :module_failed # :nodoc: - attr_accessor :enabled_types # :nodoc: - - # Returns a nested module to wrap the Metasploit(1|2|3) class so that it doesn't overwrite other (metasploit) module's - # classes. The wrapper module must be named so that active_support's autoloading code doesn't break when searching - # constants from inside the Metasploit(1|2|3) class. + ### # - # @return [Module] Msf::Modules:: - def self.wrapper_module(name) - relative_module_name = name.camelize - fully_qualified_module_name = "#{parent.name}::Modules::#{relative_module_name}" + # Upper management decided to throw in some middle management + # because the modules were getting out of hand. This bad boy + # takes care of the work of managing the interaction with + # modules in terms of loading and instantiation. + # + # TODO: + # + # - add unload support + # + ### + class ModuleManager < ModuleSet + require 'msf/core/payload_set' - wrapper_module = Object - module_names = fully_qualified_module_name.split('::') + # require here so that Msf::ModuleManager is already defined + require 'msf/core/module_manager/cache' + require 'msf/core/module_manager/loading' + require 'msf/core/module_manager/module_paths' + require 'msf/core/module_manager/module_sets' + require 'msf/core/module_manager/reloading' - until module_names.empty? - parent = wrapper_module - child_name = module_names.shift + include Msf::ModuleManager::Cache + include Msf::ModuleManager::Loading + include Msf::ModuleManager::ModulePaths + include Msf::ModuleManager::ModuleSets + include Msf::ModuleManager::Reloading - # constant names can't contain a leading digit or any non-alphanumeric characters, so convert to hexcode with - # 'X' prefix. - child_name = child_name.gsub(/^[0-9]|[^A-Za-z0-9]/) do |invalid_constant_name_character| - unpacked = invalid_constant_name_character.unpack('H*') - # unpack always returns an array, so get first value to get character's encoding - hex_code = unpacked[0] + # + # CONSTANTS + # - # as a convention start each hex-code with X so that it'll make a valid constant name since constants can't - # start with digits. - "X#{hex_code}" - end + # Regex for parsing the module type from a module name. + MODULE_TYPE_FROM_NAME_REGEX = /^(#{MODULE_TYPES.join('|')})\/(.*)$/ - # don't look for constants in ancestors since the namespace modules should be defined directly on the parent and - # previously by this same method. - inherit = false + # Overrides the module set method for adding a module so that some extra + # steps can be taken to subscribe the module and notify the event + # dispatcher. + def add_module(mod, name, file_paths) + # Call the module set implementation of add_module + dup = super - if module_names.empty? and parent.const_defined?(child_name, inherit) - ilog("Removing #{child_name} constant from #{parent.name} so it can be reloaded", 'core', LEV_1) - # if this is the leaf module name then it needs to be destroyed and recreated if it already exists as this is - # a reload - # remove_const is private, so invoke in instance context - parent.instance_eval do - remove_const child_name - end - end + # Automatically subscribe a wrapper around this module to the necessary + # event providers based on whatever events it wishes to receive. We + # only do this if we are the module manager instance, as individual + # module sets need not subscribe. + auto_subscribe_module(dup) - if parent.const_defined?(child_name, inherit) - wrapper_module = parent.const_get(child_name) + # Notify the framework that a module was loaded + framework.events.on_module_load(name, dup) + end + + # + # Creates a module using the supplied name. + # + def create(name) + # Check to see if it has a module type prefix. If it does, + # try to load it from the specific module set for that type. + match = name.match(MODULE_TYPE_FROM_NAME_REGEX) + + if match + type = match[1] + module_set = module_set_by_type[type] + + module_reference_name = match[2] + module_set.create(module_reference_name) + # Otherwise, just try to load it by name. else - # stub module to represent namespace - # use ruby Module, not Msf::Module - namespace_module = ::Module.new - - ilog("Adding #{child_name} constant to #{parent.name} as a namespace Module", 'core', LEV_1) - # once it's assigned to a constant, then wrapper_module.name will work. - wrapper_module = parent.const_set(child_name, namespace_module) + super end end - wrapper_module + def demand_load_module(mtype, mname) + n = self.cache.keys.select { |k| + self.cache[k][:mtype] == mtype and + self.cache[k][:refname] == mname + }.first + + return nil unless n + m = self.cache[n] + + if m[:file] =~ /^(.*)\/#{m[:mtype]}s?\// + path = $1 + load_module_from_file(path, m[:file], nil, nil, nil, true) + else + dlog("Could not demand load module #{mtype}/#{mname} (unknown base name in #{m[:file]})", 'core', LEV_2) + nil + end + end + + # + # Initializes an instance of the overall module manager using the supplied + # framework instance. The types parameter can be used to only load specific + # module types on initialization + # + def initialize(framework, types=MODULE_TYPES) + # + # defaults + # + + self.cache = {} + self.enablement_by_type = {} + self.module_load_error_by_reference_name = {} + self.module_paths = [] + self.module_set_by_type = {} + + # + # from arguments + # + + self.framework = framework + + types.each { |type| + init_module_set(type) + } + + super(nil) + end + + def register_type_extension(type, ext) + end + + protected + + # + # This method automatically subscribes a module to whatever event providers + # it wishes to monitor. This can be used to allow modules to automatically + # execute or perform other tasks when certain events occur. For instance, + # when a new host is detected, other aux modules may wish to run such + # that they can collect more information about the host that was detected. + # + def auto_subscribe_module(mod) + # If auto-subscribe has been disabled + if (framework.datastore['DisableAutoSubscribe'] and + framework.datastore['DisableAutoSubscribe'] =~ /^(y|1|t)/) + return + end + + # If auto-subscription is enabled (which it is by default), figure out + # if it subscribes to any particular interfaces. + inst = nil + + # + # Exploit event subscriber check + # + if (mod.include?(ExploitEvent) == true) + framework.events.add_exploit_subscriber((inst) ? inst : (inst = mod.new)) + end + + # + # Session event subscriber check + # + if (mod.include?(SessionEvent) == true) + framework.events.add_session_subscriber((inst) ? inst : (inst = mod.new)) + end + end + + attr_accessor :modules # :nodoc: end end - -end - diff --git a/lib/msf/core/module_manager/cache.rb b/lib/msf/core/module_manager/cache.rb new file mode 100644 index 0000000000..f9138e48a0 --- /dev/null +++ b/lib/msf/core/module_manager/cache.rb @@ -0,0 +1,61 @@ +# Concerns the module cache maintained by the {Msf::ModuleManager}. +module Msf::ModuleManager::Cache + extend ActiveSupport::Concern + + attr_accessor :cache # :nodoc: + + # + # Return a listing of all cached modules + # + def cache_entries + module_detail_by_file = {} + + if framework_migrated? + ::Mdm::ModuleDetail.find(:all).each do |module_detail| + module_type = module_detail.mtype + refname = module_detail.refname + + module_detail_by_file[module_detail.file] = { + :mtype => module_type, + :refname => refname, + :file => module_detail.file, + :mtime => module_detail.mtime + } + + module_set(module_type)[refname] ||= SymbolicModule + end + end + + module_detail_by_file + end + + # + # Rebuild the cache for the module set + # + def rebuild_cache(mod = nil) + unless framework_migrated? + if mod + framework.db.update_module_details(mod) + else + framework.db.update_all_module_details + end + + refresh_cache + end + end + + def framework_migrated? + if framework.db and framework.db.migrated + true + else + false + end + end + + # + # Reset the module cache + # + def refresh_cache + self.cache = cache_entries + end +end \ No newline at end of file diff --git a/lib/msf/core/module_manager/loading.rb b/lib/msf/core/module_manager/loading.rb new file mode 100644 index 0000000000..8cb1b90c3b --- /dev/null +++ b/lib/msf/core/module_manager/loading.rb @@ -0,0 +1,111 @@ +require 'msf/core/modules/loader/archive' +require 'msf/core/modules/loader/directory' + +# Deals with loading modules for the {Msf::ModuleManager} +module Msf::ModuleManager::Loading + extend ActiveSupport::Concern + + # + # CONSTANTS + # + + # Classes that can be used to load modules. + LOADER_CLASSES = [ + Msf::Modules::Loader::Archive, + Msf::Modules::Loader::Directory + ] + + # + # Returns the set of modules that failed to load. + # + def failed + return module_load_error_by_reference_name + end + + def file_changed?(path) + changed = false + + module_details = self.cache[path] + + # if uncached then it counts as changed + # Payloads can't be cached due to stage/stager matching + if module_details.nil? or module_details[:mtype] == MODULE_PAYLOAD + changed = true + else + begin + current_modification_time = ::File.mtime(path).to_i + rescue ::Errno::ENOENT + # if the file does not exist now, that's a change + changed = true + else + cached_modification_time = module_details[:mtime].to_i + + # if the file's modification time's different from the cache, then it's changed + if current_modification_time != cached_modification_time + changed = true + end + end + end + + changed + end + + attr_accessor :module_load_error_by_reference_name + + # Called when a module is initially loaded such that it can be + # categorized accordingly. + # + def on_module_load(mod, type, name, modinfo) + # Payload modules require custom loading as the individual files + # may not directly contain a logical payload that a user would + # reference, such as would be the case with a payload stager or + # stage. As such, when payload modules are loaded they are handed + # off to a special payload set. The payload set, in turn, will + # automatically create all the permutations after all the payload + # modules have been loaded. + + if (type != Msf::MODULE_PAYLOAD) + # Add the module class to the list of modules and add it to the + # type separated set of module classes + add_module(mod, name, modinfo) + end + + module_set_by_type[type].add_module(mod, name, modinfo) + end + + protected + + # Return list of {LOADER_CLASSES} instances that load modules into this module manager + def loaders + unless instance_variable_defined? :@loaders + @loaders = LOADER_CLASSES.collect { |klass| + klass.new(self) + } + end + + @loaders + end + + # Load all of the modules from the supplied directory or archive + # + # @param [String] path Path to a directory or Fastlib archive + # @param [Hash] options + # @option options [Boolean] :force Whether the force loading the modules even if they are unchanged and already + # loaded. + # @return [Hash{String => Integer}] Maps module type to number of modules loaded + def load_modules(path, options={}) + options.assert_valid_keys(:force) + + count_by_type = {} + + loaders.each do |loader| + if loader.loadable?(path) + count_by_type = loader.load_modules(path, options) + + break + end + end + + count_by_type + end +end \ No newline at end of file diff --git a/lib/msf/core/module_manager/module_paths.rb b/lib/msf/core/module_manager/module_paths.rb new file mode 100644 index 0000000000..455368afd8 --- /dev/null +++ b/lib/msf/core/module_manager/module_paths.rb @@ -0,0 +1,67 @@ +# Deals with module paths in the {Msf::ModuleManager} +module Msf::ModuleManager::ModulePaths + extend ActiveSupport::Concern + + # Adds a path to be searched for new modules. + # + # @param [String] path + # @return (see Msf::Modules::Loader::Base#load_modules) + def add_module_path(path) + nested_paths = [] + + if path =~ /\.fastlib$/ + unless ::File.exist?(path) + raise RuntimeError, "The path supplied does not exist", caller + end + + nested_paths << ::File.expand_path(path) + else + path.sub!(/#{File::SEPARATOR}$/, '') + + # Make the path completely canonical + path = Pathname.new(path).expand_path + + # Make sure the path is a valid directory + unless path.directory? + raise RuntimeError, "The path supplied is not a valid directory.", caller + end + + nested_paths << path + + # Identify any fastlib archives inside of this path + fastlib_glob = path.join('**', '*.fastlib') + Dir.glob(fastlib_glob).each do |fp| + nested_paths << fp + end + end + + # Update the module paths appropriately + self.module_paths = (module_paths + nested_paths).flatten.uniq + + # Load all of the modules from the nested paths + count_by_type = {} + nested_paths.each { |path| + path_count_by_type = load_modules(path, :force => false) + + # merge hashes + path_count_by_type.each do |type, path_count| + accumulated_count = count_by_type.fetch(type, 0) + count_by_type[type] = accumulated_count + path_count + end + } + + return count_by_type + end + + # + # Removes a path from which to search for modules. + # + def remove_module_path(path) + module_paths.delete(path) + module_paths.delete(::File.expand_path(path)) + end + + protected + + attr_accessor :module_paths # :nodoc: +end \ No newline at end of file diff --git a/lib/msf/core/module_manager/module_sets.rb b/lib/msf/core/module_manager/module_sets.rb new file mode 100644 index 0000000000..028db8ed08 --- /dev/null +++ b/lib/msf/core/module_manager/module_sets.rb @@ -0,0 +1,94 @@ +# Defines the MODULE_* constants +require 'msf/core/constants' + +# Concerns the various type-specific module sets in a {Msf::ModuleManager} +module Msf::ModuleManager::ModuleSets + extend ActiveSupport::Concern + + # + # Returns the set of loaded auxiliary module classes. + # + def auxiliary + module_set(Msf::MODULE_AUX) + end + + # + # Returns the set of loaded encoder module classes. + # + def encoders + module_set(Msf::MODULE_ENCODER) + end + + # + # Returns the set of loaded exploit module classes. + # + def exploits + module_set(Msf::MODULE_EXPLOIT) + end + + def init_module_set(type) + self.enablement_by_type[type] = true + case type + when Msf::MODULE_PAYLOAD + instance = Msf::PayloadSet.new(self) + else + instance = Msf::ModuleSet.new(type) + end + + self.module_set_by_type[type] = instance + + # Set the module set's framework reference + instance.framework = self.framework + end + + # + # Provide a list of module names of a specific type + # + def module_names(set) + module_set_by_type[set] ? module_set_by_type[set].keys.dup : [] + end + + # + # Returns all of the modules of the specified type + # + def module_set(type) + module_set_by_type[type] + end + + # + # Provide a list of the types of modules in the set + # + def module_types + module_set_by_type.keys.dup + end + + # + # Returns the set of loaded nop module classes. + # + def nops + module_set(Msf::MODULE_NOP) + end + + # + # Returns the set of loaded payload module classes. + # + def payloads + module_set(Msf::MODULE_PAYLOAD) + end + + # + # Returns the set of loaded auxiliary module classes. + # + def post + module_set(Msf::MODULE_POST) + end + + def type_enabled?(type) + enablement_by_type[type] || false + end + + protected + + attr_accessor :enablement_by_type # :nodoc: + attr_accessor :module_set_by_type # :nodoc: +end \ No newline at end of file diff --git a/lib/msf/core/module_manager/reloading.rb b/lib/msf/core/module_manager/reloading.rb new file mode 100644 index 0000000000..ed9d97b8ea --- /dev/null +++ b/lib/msf/core/module_manager/reloading.rb @@ -0,0 +1,49 @@ +# Concerns reloading modules +module Msf::ModuleManager::Reloading + extend ActiveSupport::Concern + + # Reloads the module specified in mod. This can either be an instance of a module or a module class. + # + # @param [Msf::Module, Class] mod either an instance of a module or a module class + def reload_module(mod) + refname = mod.refname + + dlog("Reloading module #{refname}...", 'core') + + # if it's can instance, then get its class + if mod.is_a? Msf::Module + metasploit_class = mod.class + else + metasploit_class = mod + end + + namespace_module = metasploit_class.parent + loader = namespace_module.loader + loader.reload_module(mod) + end + + # + # Reloads modules from all module paths + # + def reload_modules + self.module_history = {} + self.clear + + self.enablement_by_type.each_key do |type| + module_set_by_type[type].clear + init_module_set(type) + end + + # The number of loaded modules in the following categories: + # auxiliary/encoder/exploit/nop/payload/post + count = 0 + module_paths.each do |path| + mods = load_modules(path, true) + mods.each_value {|c| count += c} + end + + rebuild_cache + + count + end +end \ No newline at end of file diff --git a/lib/msf/core/module_set.rb b/lib/msf/core/module_set.rb new file mode 100644 index 0000000000..f11617a108 --- /dev/null +++ b/lib/msf/core/module_set.rb @@ -0,0 +1,290 @@ +# -*- coding: binary -*- +require 'msf/core' +require 'fastlib' +require 'pathname' + +module Msf + + # + # Define used for a place-holder module that is used to indicate that the + # module has not yet been demand-loaded. Soon to go away. + # + SymbolicModule = "__SYMBOLIC__" + + ### + # + # A module set contains zero or more named module classes of an arbitrary + # type. + # + ### + class ModuleSet < Hash + include Framework::Offspring + + # Wrapper that detects if a symbolic module is in use. If it is, it creates an instance to demand load the module + # and then returns the now-loaded class afterwords. + def [](name) + if (super == SymbolicModule) + create(name) + end + + super + end + + # Create an instance of the supplied module by its name + # + def create(name) + klass = fetch(name, nil) + instance = nil + + # If there is no module associated with this class, then try to demand + # load it. + if klass.nil? or klass == SymbolicModule + # If we are the root module set, then we need to try each module + # type's demand loading until we find one that works for us. + if module_type.nil? + MODULE_TYPES.each { |type| + framework.modules.demand_load_module(type, name) + } + else + framework.modules.demand_load_module(module_type, name) + end + + recalculate + + klass = get_hash_val(name) + end + + # If the klass is valid for this name, try to create it + if klass and klass != SymbolicModule + instance = klass.new + end + + # Notify any general subscribers of the creation event + if instance + self.framework.events.on_module_created(instance) + end + + return instance + end + + # + # Overrides the builtin 'each' operator to avoid the following exception on Ruby 1.9.2+ + # "can't add a new key into hash during iteration" + # + def each(&block) + list = [] + self.keys.sort.each do |sidx| + list << [sidx, self[sidx]] + end + list.each(&block) + end + + # + # Enumerates each module class in the set. + # + def each_module(opts = {}, &block) + demand_load_modules + + self.mod_sorted = self.sort + + each_module_list(mod_sorted, opts, &block) + end + + # + # Custom each_module filtering if an advanced set supports doing extended + # filtering. Returns true if the entry should be filtered. + # + def each_module_filter(opts, name, entry) + return false + end + + # + # Enumerates each module class in the set based on their relative ranking + # to one another. Modules that are ranked higher are shown first. + # + def each_module_ranked(opts = {}, &block) + demand_load_modules + + self.mod_ranked = rank_modules + + each_module_list(mod_ranked, opts, &block) + end + + # + # Forces all modules in this set to be loaded. + # + def force_load_set + each_module { |name, mod| } + end + + # + # Initializes a module set that will contain modules of a specific type and + # expose the mechanism necessary to create instances of them. + # + def initialize(type = nil) + # + # Defaults + # + self.ambiguous_module_reference_name_set = Set.new + # Hashes that convey the supported architectures and platforms for a + # given module + self.mod_arch_hash = {} + self.mod_platform_hash = {} + self.mod_sorted = nil + self.mod_ranked = nil + self.mod_extensions = [] + + # + # Arguments + # + self.module_type = type + end + + attr_reader :module_type + + # + # Gives the module set an opportunity to handle a module reload event + # + def on_module_reload(mod) + end + + # + # Whether or not recalculations should be postponed. This is used from the + # context of the each_module_list handler in order to prevent the demand + # loader from calling recalc for each module if it's possible that more + # than one module may be loaded. This field is not initialized until used. + # + attr_accessor :postpone_recalc + + + # + # Dummy placeholder to relcalculate aliases and other fun things. + # + def recalculate + end + + # + # Checks to see if the supplied module name is valid. + # + def valid?(name) + create(name) + (self[name]) ? true : false + end + + protected + + # + # Adds a module with a the supplied name. + # + def add_module(mod, name, modinfo = nil) + # Set the module's name so that it can be referenced when + # instances are created. + mod.framework = framework + mod.refname = name + mod.file_path = ((modinfo and modinfo['files']) ? modinfo['files'][0] : nil) + mod.orig_cls = mod + + cached_module = self[name] + + if (cached_module and cached_module != SymbolicModule) + ambiguous_module_reference_name_set.add(name) + + wlog("The module #{mod.refname} is ambiguous with #{self[name].refname}.") + else + self[name] = mod + end + + mod + end + + # + # Load all modules that are marked as being symbolic. + # + def demand_load_modules + # Pre-scan the module list for any symbolic modules + self.each_pair { |name, mod| + if (mod == SymbolicModule) + self.postpone_recalc = true + + mod = create(name) + + next if (mod.nil?) + end + } + + # If we found any symbolic modules, then recalculate. + if (self.postpone_recalc) + self.postpone_recalc = false + + recalculate + end + end + + # + # Enumerates the modules in the supplied array with possible limiting + # factors. + # + def each_module_list(ary, opts, &block) + ary.each { |entry| + name, mod = entry + + # Skip any lingering symbolic modules. + next if (mod == SymbolicModule) + + # Filter out incompatible architectures + if (opts['Arch']) + if (!mod_arch_hash[mod]) + mod_arch_hash[mod] = mod.new.arch + end + + next if ((mod_arch_hash[mod] & opts['Arch']).empty? == true) + end + + # Filter out incompatible platforms + if (opts['Platform']) + if (!mod_platform_hash[mod]) + mod_platform_hash[mod] = mod.new.platform + end + + next if ((mod_platform_hash[mod] & opts['Platform']).empty? == true) + end + + # Custom filtering + next if (each_module_filter(opts, name, entry) == true) + + block.call(name, mod) + } + end + + attr_accessor :ambiguous_module_reference_name_set + attr_accessor :mod_arch_hash + attr_accessor :mod_extensions + attr_accessor :mod_platform_hash + attr_accessor :mod_ranked + attr_accessor :mod_sorted + attr_writer :module_type + attr_accessor :module_history + + # + # Ranks modules based on their constant rank value, if they have one. + # + def rank_modules + self.mod_ranked = self.sort { |a, b| + a_name, a_mod = a + b_name, b_mod = b + + # Dynamically loads the module if needed + a_mod = create(a_name) if a_mod == SymbolicModule + b_mod = create(b_name) if b_mod == SymbolicModule + + # Extract the ranking between the two modules + a_rank = a_mod.const_defined?('Rank') ? a_mod.const_get('Rank') : NormalRanking + b_rank = b_mod.const_defined?('Rank') ? b_mod.const_get('Rank') : NormalRanking + + # Compare their relevant rankings. Since we want highest to lowest, + # we compare b_rank to a_rank in terms of higher/lower precedence + b_rank <=> a_rank + } + end + end +end diff --git a/lib/msf/core/modules.rb b/lib/msf/core/modules.rb new file mode 100644 index 0000000000..74b3722df3 --- /dev/null +++ b/lib/msf/core/modules.rb @@ -0,0 +1,4 @@ +# Namespace for loading Metasploit modules +module Msf::Modules + +end \ No newline at end of file diff --git a/lib/msf/core/modules/loader.rb b/lib/msf/core/modules/loader.rb new file mode 100644 index 0000000000..e85040ea16 --- /dev/null +++ b/lib/msf/core/modules/loader.rb @@ -0,0 +1,6 @@ +require 'msf/core/modules' + +# Namespace for module loaders +module Msf::Modules::Loader + +end \ No newline at end of file diff --git a/lib/msf/core/modules/loader/archive.rb b/lib/msf/core/modules/loader/archive.rb new file mode 100644 index 0000000000..dfd42edcaf --- /dev/null +++ b/lib/msf/core/modules/loader/archive.rb @@ -0,0 +1,77 @@ +require 'msf/core/modules/loader/base' + +# Concerns loading modules form fastlib archives +class Msf::Modules::Loader::Archive < Msf::Modules::Loader::Base + # + # CONSTANTS + # + + # The extension for Fastlib archives. + ARCHIVE_EXTENSION = '.fastlib' + + # Returns true if the path is a Fastlib archive. + # + # @param (see Msf::Modules::Loader::Base#loadable?) + # @return [true] if path has the {ARCHIVE_EXTENSION} extname. + # @return [false] otherwise + def loadable?(path) + if File.extname(path) == ARCHIVE_EXTENSION + true + else + false + end + end + + protected + + # Yields the module_reference_name for each module file in the Fastlib archive at path. + # + # @param path [String] The path to the Fastlib archive file. + # @yield (see Msf::Modules::Loader::Base#each_module_reference_name) + # @yieldparam (see Msf::Modules::Loader::Base#each_module_reference_name) + # @return (see Msf::Modules::Loader::Base#each_module_reference_name) + def each_module_reference_name(path) + entries = ::FastLib.list(path) + + entries.each do |entry| + if entry.include?('.svn/') + next + end + + type = entry.split('/', 2)[0] + type = type.singularize + + unless module_manager.enablement_by_type[type] + next + end + + if module_path?(entry) + # The module_reference_name doesn't have a file extension + module_reference_name = module_reference_name_from_path(entry) + + yield path, type, module_reference_name + end + end + end + + # Returns the path to the module inside the Fastlib archive. The path to the archive is separated from the path to + # the file inside the archive by '::'. + # + # @param (see Msf::Modules::Loader::Base#module_path) + # @return [String] Path to module file inside the Fastlib archive. + def module_path(parent_path, type, module_reference_name) + file_path = typed_path(type, module_reference_name) + module_path = "#{parent_path}::#{file_path}" + + module_path + end + + # Loads the module content from the Fastlib archive. + # + # @return (see Msf::Modules::Loader::Base#read_module_content) + def read_module_content(path, type, module_reference_name) + file_path = typed_path(type, module_reference_name) + + ::FastLib.load(path, file_path) + end +end \ No newline at end of file diff --git a/lib/msf/core/modules/loader/base.rb b/lib/msf/core/modules/loader/base.rb new file mode 100644 index 0000000000..56627d71eb --- /dev/null +++ b/lib/msf/core/modules/loader/base.rb @@ -0,0 +1,477 @@ +# +# Project +# +require 'msf/core/modules/loader' +require 'msf/core/modules/namespace' + +# Responsible for loading modules for {Msf::ModuleManagers}. +# +# @abstract Subclass and override {#base_path}, {#each_module_reference_name}, {#loadable?}, and +# {#read_module_content}. +class Msf::Modules::Loader::Base + # + # CONSTANTS + # + + # This must calculate the first line of the NAMESPACE_MODULE_CONTENT string so that errors are reported correctly + NAMESPACE_MODULE_LINE = __LINE__ + 4 + # By calling module_eval from inside the module definition, the lexical scope is captured and available to the code in + # module_content. + NAMESPACE_MODULE_CONTENT = <<-EOS + # ensure the namespace module can respond to checks during loading + extend Msf::Modules::Namespace + + class << self + # The loader that originally loaded this module + # + # @return [Msf::Modules::Loader::Base] the loader that loaded this namespace module and can reload it. + attr_accessor :loader + + # @return [String] The path under which the module of the given type was found. + attr_accessor :parent_path + end + + # Calls module_eval on the module_content, but the lexical scope of the namespace_module is passed through + # module_eval, so that module_content can act like it was written inline in the namespace_module. + # + # @param [String] module_content The content of the {Msf::Module}. + # @param [String] module_path The path to the module, so that error messages in evaluating the module_content can + # be reported correctly. + def self.module_eval_with_lexical_scope(module_content, module_path) + # By calling module_eval from inside the module definition, the lexical scope is captured and available to the + # code in module_content. + module_eval(module_content) + end + EOS + + # The extension for metasploit modules. + MODULE_EXTENSION = '.rb' + # String used to separate module names in a qualified module name. + MODULE_SEPARATOR = '::' + # The base namespace name under which {#namespace_module #namespace_modules} are created. + NAMESPACE_MODULE_NAMES = ['Msf', 'Modules'] + # Regex that can distinguish regular ruby source from unit test source. + UNIT_TEST_REGEX = /rb\.(ut|ts)\.rb$/ + # Not all types are pluralized when a directory name, so here's the mapping that currently exists + DIRECTORY_BY_TYPE = { + Msf::MODULE_AUX => 'auxiliary', + Msf::MODULE_ENCODER => 'encoders', + Msf::MODULE_EXPLOIT => 'exploits', + Msf::MODULE_NOP => 'nops', + Msf::MODULE_PAYLOAD => 'payloads', + Msf::MODULE_POST => 'post' + } + + # @param [Msf::ModuleManager] module_manager The module manager that caches the loaded modules. + def initialize(module_manager) + @module_manager = module_manager + end + + # Returns whether the path can be loaded this module loader. + # + # @param path (see #load_modules) + # @return [Boolean] + def loadable?(path) + raise ::NotImplementedError + end + + # Loads all of the modules from the supplied path. + # + # @note Only paths where {#loadable?} returns true should be passed to this method. + # + # @param [String] path Path under which there are modules + # @param [Hash] options + # @option options [Boolean] force (false) whether to force loading of the module even if the module has not changed. + # @return [Hash{String => Integer}] Maps module type to number of modules loaded + def load_modules(path, options={}) + options.assert_valid_keys(:force) + + force = options[:force] + count_by_type = {} + recalculate_by_type = {} + + each_module_reference_name(path) do |parent_path, type, module_reference_name| + load_module( + parent_path, + type, + module_reference_name, + :recalculate_by_type => recalculate_by_type, + :count_by_type => count_by_type, + :force => force + ) + end + + recalculate_by_type.each do |type, recalculate| + if recalculate + module_set = module_manager.module_set(type) + module_set.recalculate + end + end + + count_by_type + end + + # Reloads the specified module. + # + # @param [Class, Msf::Module] original_metasploit_class_or_instance either an instance of a module or a module class + def reload_module(original_metasploit_class_or_instance) + if original_metasploit_class_or_instance.is_a? Msf::Module + original_metasploit_instance = original_metasploit_class_or_instance + original_metasploit_class = original_metasploit_class_or_instance.class + else + original_metasploit_instance = nil + original_metasploit_class = original_metasploit_class_or_instance + end + + namespace_module = original_metasploit_class.parent + parent_path = namespace_module.parent_path + + type = original_metasploit_class_or_instance.type + module_reference_name = original_metasploit_class_or_instance.refname + + dlog("Reloading module #{module_reference_name}...", 'core') + + if load_module(parent_path, type, module_reference_name, :force => true) + # Create a new instance of the module + reloaded_module_instance = module_manager.create(module_reference_name) + + if reloaded_module_instance and original_metasploit_instance + # copy over datastore + reloaded_module_instance.datastore.update(original_metasploit_instance.datastore) + else + elog("Failed to create instance of #{refname} after reload.", 'core') + + # Return the old module instance to avoid a strace trace + return original_module + end + else + elog("Failed to reload #{module_reference_name}") + + # Return the old module isntance to avoid a strace trace + return original_metasploit_class_or_instance + end + + # Let the specific module sets have an opportunity to handle the fact + # that this module was reloaded. + module_set = module_manager.module_set(type) + module_set.on_module_reload(reloaded_module_instance) + + # Rebuild the cache for just this module + module_manager.rebuild_cache(reloaded_module_instance) + + reloaded_module_instance + end + + protected + + # Yields module reference names under path. + # + # @param path (see #load_modules) + # @yield [parent_path, type, module_reference_name] Gives the path and the module_reference_name of the module found + # under the path. + # @yieldparam parent_path [String] the path under which the module of the given type was found. + # @yieldparam type [String] the type of the module. + # @yieldparam module_reference_name [String] The canonical name for referencing the module. + # @return [void] + def each_module_reference_name(path) + raise ::NotImplementedError + end + + # Loads a module from the supplied path and module_reference_name. + # + # @param [String] parent_path The path under which the module exists. This is not necessarily the same path as passed + # to {#load_modules}: it may just be derived from that path. + # @param [String] type The type of module. + # @param [String] module_reference_name The canonical name for referring to the module. + # @param [Hash] options Options used to force loading and track statistics + # @option options [Hash{String => Integer}] :count_by_type Maps the module type to the number of module loaded + # @option options [Boolean] :force (false) whether to force loading of the module even if the module has not changed. + # @option options [Hash{String => Boolean}] :recalculate_by_type Maps type to whether its + # {Msf::ModuleManager#module_set} needs to be recalculated. + def load_module(parent_path, type, module_reference_name, options={}) + options.assert_valid_keys(:count_by_type, :force, :recalculate_by_type) + force = options[:force] || false + + unless force or module_manager.file_changed?(parent_path) + dlog("Cached module from #{parent_path} has not changed.", 'core', LEV_2) + + return false + end + + namespace_module = self.namespace_module(module_reference_name) + + # set the parent_path so that the module can be reloaded with #load_module + namespace_module.parent_path = parent_path + + module_content = read_module_content(parent_path, type, module_reference_name) + module_path = module_path(parent_path, type, module_reference_name) + + begin + namespace_module.module_eval_with_lexical_scope(module_content, module_path) + # handle interrupts as pass-throughs unlike other Exceptions + rescue ::Interrupt + raise + rescue ::Exception => error + # Hide eval errors when the module version is not compatible + begin + namespace_module.version_compatible! + rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error + error_message = "Failed to load module (#{module_path}) due to error and #{version_compatibility_error}" + else + error_message = "#{error.class} #{error}:\n#{error.backtrace}" + end + + elog(error_message) + module_manager.module_load_error_by_reference_name[module_reference_name] = error_message + + return false + end + + metasploit_class = namespace_module.metasploit_class + + unless metasploit_class + error_message = "Missing Metasploit class constant" + + elog(error_message) + module_manager.module_load_error_by_reference_name[module_reference_name] = error_message + end + + unless usable?(metasploit_class) + ilog("Skipping module #{module_reference_name} under #{parent_path} because is_usable returned false.", 'core', LEV_1) + + return false + end + + ilog("Loaded #{type} module #{module_reference_name} under #{parent_path}", 'core', LEV_2) + + # if this is a reload, then there may be a pre-existing error that should now be cleared + module_manager.module_load_error_by_reference_name.delete(module_reference_name) + + # Do some processing on the loaded module to get it into the right associations + module_manager.on_module_load( + metasploit_class, + type, + module_reference_name, + { + # files[0] is stored in the {Msf::Module#file_path} and is used to reload the module, so it needs to be a + # full path + 'files' => [ + module_path + ], + 'paths' => [ + module_reference_name + ], + 'type' => type + } + ) + + # Set this module type as needing recalculation + recalculate_by_type = options[:recalculate_by_type] + + if recalculate_by_type + recalculate_by_type[type] = true + end + + # The number of loaded modules this round + count_by_type = options[:count_by_type] + + if count_by_type + count_by_type[type] ||= 0 + count_by_type[type] += 1 + end + + return true + end + + attr_reader :module_manager + + # Returns path to module that can be used for reporting errors in evaluating the + # {#read_module_content module_content}. + # + # @param path (see #load_module) + # @param type (see #load_module) + # @param module_reference_name (see #load_module) + # @return [String] The path to module. + def module_path(parent_path, type, module_reference_name) + raise ::NotImplementedError + end + + # Returns whether the path could refer to a module. The path would still need to be loaded in order to check if it + # actually is a valid module. + # + # @param [String] path to module without the type directory. + # @return [true] if the extname is {MODULE_EXTENSION} AND + # the path does not match {UNIT_TEXT_REGEX} AND + # the path is not hidden (starts with '.') + # @return [false] otherwise + def module_path?(path) + module_path = false + + extension = File.extname(path) + + unless (path.starts_with?('.') or + extension != MODULE_EXTENSION or + path =~ UNIT_TEST_REGEX) + module_path = true + end + + module_path + end + + # Changes a file name path to a canonical module reference name. + # + # @param [String] path Relative path to module. + # @return [String] MODULE_EXTENSION removed from path. + def module_reference_name_from_path(path) + path.gsub(/#{MODULE_EXTENSION}$/, '') + end + + # Returns a nested module to wrap the Metasploit(1|2|3) class so that it doesn't overwrite other (metasploit) + # module's classes. The wrapper module must be named so that active_support's autoloading code doesn't break when + # searching constants from inside the Metasploit(1|2|3) class. + def namespace_module(module_reference_name) + namespace_module_names = self.namespace_module_names(module_reference_name) + + # If this is a reload, then the pre-existing namespace_module needs to be removed. + + # don't check ancestors for the constants + inherit = false + + # Don't want to trigger ActiveSupport's const_missing, so can't use constantize. + namespace_module = namespace_module_names.inject(Object) { |parent, module_name| + if parent.const_defined?(module_name, inherit) + parent.const_get(module_name, inherit) + else + break + end + } + + # if a reload + unless namespace_module.nil? + parent_module = namespace_module.parent + + # remove_const is private, so invoke in instance context + parent_module.instance_eval do + remove_const namespace_module_names.last + end + end + + # In order to have constants defined in Msf resolve without the Msf qualifier in the module_content, the + # Module.nesting must resolve for the entire nesting. Module.nesting is strictly lexical, and can't be faked with + # module_eval(&block). (There's actually code in ruby's implementation to stop module_eval from being added to + # Module.nesting when using the block syntax.) All this means is the modules have to be declared as a string that + # gets module_eval'd. + + nested_module_names = namespace_module_names.reverse + + namespace_module_content = nested_module_names.inject(NAMESPACE_MODULE_CONTENT) { |wrapped_content, module_name| + lines = [] + lines << "module #{module_name}" + lines << wrapped_content + lines << "end" + + lines.join("\n") + } + + Object.module_eval(namespace_module_content, __FILE__, NAMESPACE_MODULE_LINE) + + # The namespace_module exists now, so no need to use constantize to do const_missing + namespace_module = namespace_module_names.inject(Object) { |parent, module_name| + parent.const_get(module_name, inherit) + } + # record the loader, so that the namespace module and its metasploit_class can be reloaded + namespace_module.loader = self + + namespace_module + end + + def namespace_module_name(module_reference_name) + namespace_module_names = self.namespace_module_names(module_reference_name) + namespace_module_name = namespace_module_names.join(MODULE_SEPARATOR) + + namespace_module_name + end + + # Returns a fully qualified module name to wrap the Metasploit(1|2|3) class so that it doesn't overwrite other + # (metasploit) module's classes. Invalid module name characters are escaped by using 'H*' unpacking and prefixing + # each code with X so the code remains a valid module name when it starts with a digit. + # + # @param [String] module_reference_name The canonical name for the module. + # @return [Module] Msf::Modules:: + # + # @see namespace_module + def namespace_module_names(module_reference_name) + relative_module_name = module_reference_name.camelize + + module_names = relative_module_name.split('::') + + # The module_reference_name is path-like, so it can include characters that are invalid in module names + valid_module_names = module_names.collect { |module_name| + valid_module_name = module_name.gsub(/^[0-9]|[^A-Za-z0-9]/) { |invalid_constant_name_character| + unpacked = invalid_constant_name_character.unpack('H*') + # unpack always returns an array, so get first value to get character's encoding + hex_code = unpacked[0] + + # as a convention start each hex-code with X so that it'll make a valid constant name since constants can't + # start with digits. + "X#{hex_code}" + } + + valid_module_name + } + + namespace_module_names = NAMESPACE_MODULE_NAMES + valid_module_names + + namespace_module_names + end + + # Read the content of the module from under path. + # + # @param parent_path (see #load_module) + # @param type (see #load_module) + # @param module_reference_name (see #load_module) + # @return [String] module content that can be module_evaled into the {#namespace_module} + def read_module_content(parent_path, type, module_reference_name) + raise ::NotImplementedError + end + + + # The path to the module qualified by the type directory. + # + # @note To get the full path to the module, use {#module_path} + # + # @param [String] type The type of the module. + # @param [String] module_reference_name The canonical name for the module. + # @return [String] path to the module starting with the type directory. + # + # @see DIRECTORY_BY_TYPE + def typed_path(type, module_reference_name) + file_name = module_reference_name + MODULE_EXTENSION + type_directory = DIRECTORY_BY_TYPE[type] + typed_path = File.join(type_directory, file_name) + + typed_path + end + + # Returns whether the metasploit_class is usable on the current system. Defer's to metasploit_class's #is_usable if + # it is defined. + # + # @param [Msf::Module] metasploit_class As returned by {Msf::Modules::Namespace#metasploit_class} + def usable?(metasploit_class) + # If the module indicates that it is not usable on this system, then we + # will not try to use it. + usable = false + + if metasploit_class.respond_to? :is_usable + begin + usable = metasploit_class.is_usable + rescue => error + elog("Exception caught during is_usable check: #{error}") + end + else + usable = true + end + + usable + end +end \ No newline at end of file diff --git a/lib/msf/core/modules/loader/directory.rb b/lib/msf/core/modules/loader/directory.rb new file mode 100644 index 0000000000..3e04bb2464 --- /dev/null +++ b/lib/msf/core/modules/loader/directory.rb @@ -0,0 +1,79 @@ +# Concerns loading module from a directory +class Msf::Modules::Loader::Directory < Msf::Modules::Loader::Base + # Returns true if the path is a directory + # + # @param (see Msf::Modules::Loader::Base#loadable?) + # @return [true] if path is a directory + # @return [false] otherwise + def loadable?(path) + if File.directory?(path) + true + else + false + end + end + + protected + + # Yields the module_reference_name for each module file found under the directory path. + # + # @param [String] path The path to the directory. + # @yield (see Msf::Modules::Loader::Base#each_module_reference_name) + # @yieldparam [String] path The path to the directory. + # @yieldparam [String] type The type correlated with the directory under path. + # @yieldparam module_reference_name (see Msf::Modules::Loader::Base#each_module_reference_name) + # @return (see Msf::Modules::Loader::Base#each_module_reference_name) + def each_module_reference_name(path) + ::Dir.foreach(path) do |entry| + if entry.downcase == '.svn' + next + end + + full_entry_path = ::File.join(path, entry) + type = entry.singularize + + unless ::File.directory?(full_entry_path) and + module_manager.type_enabled? type + next + end + + full_entry_pathname = Pathname.new(full_entry_path) + + # Try to load modules from all the files in the supplied path + Rex::Find.find(full_entry_path) do |entry_descendant_path| + if module_path?(entry_descendant_path) + entry_descendant_pathname = Pathname.new(entry_descendant_path) + relative_entry_descendant_pathname = entry_descendant_pathname.relative_path_from(full_entry_pathname) + relative_entry_descendant_path = relative_entry_descendant_pathname.to_path + + # The module_reference_name doesn't have a file extension + module_reference_name = module_reference_name_from_path(relative_entry_descendant_path) + + yield path, type, module_reference_name + end + end + end + end + + # Returns the full path to the module file on disk. + # + # @param (see Msf::Modules::Loader::Base#module_path) + # @return [String] Path to module file on disk. + def module_path(parent_path, type, module_reference_name) + file_name = module_reference_name + MODULE_EXTENSION + type_directory = DIRECTORY_BY_TYPE[type] + full_path = File.join(parent_path, type_directory, file_name) + + full_path + end + + # Loads the module content from the on disk file. + # + # @param (see Msf::Modules::Loader::Base#read_module_content) + # @return (see Msf::Modules::Loader::Base#read_module_content) + def read_module_content(parent_path, type, module_reference_name) + full_path = module_path(parent_path, type, module_reference_name) + + ::File.read(full_path) + end +end \ No newline at end of file diff --git a/lib/msf/core/modules/namespace.rb b/lib/msf/core/modules/namespace.rb new file mode 100644 index 0000000000..d930437156 --- /dev/null +++ b/lib/msf/core/modules/namespace.rb @@ -0,0 +1,51 @@ +# Concern for behavior that all namespace modules that wrap Msf::Modules must support like version checking and +# grabbing the version specific-Metasploit* class. +module Msf::Modules::Namespace + # Returns the Metasploit(3|2|1) class from the module_evalled content. + # + # @note The module content must be module_evalled into this namespace module before the return of + # {metasploit_class} is valid. + # + # @return [Msf::Module] if a Metasploit(3|2|1) class exists in this module + # @return [nil] if such as class is not defined. + def metasploit_class + metasploit_class = nil + # don't search ancestors for the metasploit_class + inherit = false + + ::Msf::Framework::Major.downto(1) do |major| + if const_defined?("Metasploit#{major}", inherit) + metasploit_class = const_get("Metasploit#{major}") + + break + end + end + + metasploit_class + end + + # Raises an error unless {Msf::Framework::VersionCore} and {Msf::Framework::VersionAPI} meet the minimum required + # versions defined in RequiredVersions in the module content. + # + # @note The module content must be module_evalled into this namespace module using module_eval_with_lexical_scope + # before calling {version_compatible!} is valid. + # + # @raise [Msf::Modules::VersionCompatibilityError] if RequiredVersion[0] > Msf::Framework::VersionCore or + # RequiredVersion[1] > Msf::Framework::VersionApi + # @return [void] + def version_compatible! + if const_defined?(:RequiredVersions) + required_versions = const_get(:RequiredVersions) + minimum_core_version = required_versions[0] + minimum_api_version = required_versions[1] + + if (minimum_core_version > ::Msf::Framework::VersionCore or + minimum_api_version > ::Msf::Framework::VersionAPI) + raise Msf::Modules::VersionCompatibilityError.new( + :minimum_api_version => minimum_api_version, + :minimum_core_version => minimum_core_version + ) + end + end + end +end \ No newline at end of file diff --git a/lib/msf/core/modules/version_compatibility_error.rb b/lib/msf/core/modules/version_compatibility_error.rb new file mode 100644 index 0000000000..14dd0cb556 --- /dev/null +++ b/lib/msf/core/modules/version_compatibility_error.rb @@ -0,0 +1,12 @@ +class Msf::Modules::VersionCompatibilityError < StandardError + def initialize(attributes={}) + @minimum_api_version = attributes[:minimum_api_version] + @minimum_core_version = attributes[:minimum_core_version] + + super("Failed to reload module (#{name}) due to version check " \ + "(requires API:#{minimum_api_version} Core:#{minimum_core_version})") + end + + attr_reader :minimum_api_version + attr_reader :minimum_core_version +end \ No newline at end of file