rapid7/metasploit-framework

View on GitHub
lib/msf/core/modules/loader/base.rb

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: binary -*-
#
# Project
#
require 'msf/core/constants'
# Responsible for loading modules for {Msf::ModuleManager}.
#
# @abstract Subclass and override {#each_module_reference_name}, {#loadable?}, {#module_path}, and
#   {#read_module_content}.
class Msf::Modules::Loader::Base
  #
  # CONSTANTS
  #

  # 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',
    Msf::MODULE_EVASION => 'evasion'
  }
  TYPE_BY_DIRECTORY = DIRECTORY_BY_TYPE.invert
  # 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
    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, module_path)
    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 {#create_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$/

  # @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.
  #
  # @abstract Override and determine from properties of the path or the
  #   file to which the path points whether it is loadable using
  #   {#load_modules} for the subclass.
  #
  # @param path (see #load_modules)
  # @return [Boolean]
  def loadable?(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::ModuleSets#module_set}
  #   needs to be recalculated.
  # @option options [Boolean] :reload (false) whether this is a reload.
  #
  # @return [false] if :force is false and parent_path has not changed.
  # @return [false] if exception encountered while parsing module content
  # @return [false] if the module is incompatible with the Core or API version.
  # @return [false] if the module does not implement a Metasploit class.
  # @return [false] if the module's is_usable method returns false.
  # @return [true] if all those condition pass and the module is
  #   successfully loaded.
  #
  # @see #read_module_content
  # @see Msf::ModuleManager::Loading#file_changed?
  def load_module(parent_path, type, module_reference_name, options = {})
    options.assert_valid_keys(:count_by_type, :force, :recalculate_by_type, :reload, :cached_metadata)
    force = options[:force] || false
    reload = options[:reload] || false

    if options[:cached_metadata]
      module_path = options[:cached_metadata].path
    else
      module_path = self.module_path(parent_path, type, module_reference_name)
    end

    file_changed = module_manager.file_changed?(module_path)

    unless force or file_changed
      dlog("Cached module from #{module_path} has not changed.", 'core', LEV_2)

      return false
    end

    reload ||= force || file_changed

    module_content = read_module_content_from_path(module_path)

    if module_content.empty?
      # read_module_content is responsible for calling {#load_error}, so just return here.
      return false
    end

    klass = nil
    try_eval_module = lambda { |namespace_module|
      # set the parent_path so that the module can be reloaded with #load_module
      namespace_module.parent_path = parent_path

      begin
        namespace_module.module_eval_with_lexical_scope(module_content, module_path)
      # handle interrupts as pass-throughs unlike other Exceptions so users can bail with Ctrl+C
      rescue ::Interrupt
        raise
      rescue ::Exception => error
        load_error(module_path, error)
        return false
      end

      if namespace_module.const_defined?('Metasploit3', false)
        klass = namespace_module.const_get('Metasploit3', false)
        load_warning(module_path, "Please change the module's class name from Metasploit3 to MetasploitModule")
      elsif namespace_module.const_defined?('Metasploit4', false)
        klass = namespace_module.const_get('Metasploit4', false)
        load_warning(module_path, "Please change the module's class name from Metasploit4 to MetasploitModule")
      elsif namespace_module.const_defined?('MetasploitModule', false)
        klass = namespace_module.const_get('MetasploitModule', false)
      else
        load_error(module_path, Msf::Modules::Error.new(
          module_path:           module_path,
          module_reference_name: module_reference_name,
          causal_message:        'invalid module class name (must be MetasploitModule)'
        ))
        return false
      end

      if reload
        ilog("Reloading #{type} module #{module_reference_name}. Ambiguous module warnings are safe to ignore", 'core', LEV_2)
      else
        ilog("Loaded #{type} module #{module_reference_name} under #{parent_path}", 'core', LEV_2)
      end

      module_manager.module_load_error_by_path.delete(module_path)

      true
    }

    begin
      loaded = namespace_module_transaction("#{type}/#{module_reference_name}", reload: reload, &try_eval_module)
      return false unless loaded
    rescue NameError
      load_error(module_path, Msf::Modules::Error.new(
        module_path:           module_path,
        module_reference_name: module_reference_name,
        causal_message:        'invalid module filename (must be lowercase alphanumeric snake case)'
      ))
      return false
    rescue => e
      load_error(module_path, Msf::Modules::Error.new(
        module_path:           module_path,
        module_reference_name: module_reference_name,
        causal_message:        "unknown error #{e.message}"
      ))
      return false
    end


    # Do some processing on the loaded module to get it into the right associations
    module_manager.on_module_load(
        klass,
        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,
            'cached_metadata' => options[:cached_metadata]
        }
    )

    # 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

  # 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.
  # @option options [Array] whitelist An array of regex patterns to search for specific modules
  # @return [Hash{String => Integer}] Maps module type to number of
  #   modules loaded
  def load_modules(path, options={})
    options.assert_valid_keys(:force, :recalculate)

    force = options[:force]
    count_by_type = {}
    recalculate_by_type = {}

    each_module_reference_name(path, options) 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
    if options[:recalculate]
      recalculate_by_type.each do |type, recalculate|
        if recalculate
          module_set = module_manager.module_set(type)
          module_set.recalculate
        end
      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.
  #   If an instance is given, then the datastore will be copied to the new instance returned by this method.
  # @return [Class, Msf::Module] original_metasploit_class_or_instance if an instance of the reloaded module cannot be
  #   created.
  # @return [Msf::Module] new instance of original_metasploit_class with datastore copied from
  #   original_metasploit_instance.
  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.module_parent
    parent_path = namespace_module.parent_path

    type = original_metasploit_class.type
    module_reference_name = original_metasploit_class.refname
    module_fullname = original_metasploit_class.fullname
    module_used_name = original_metasploit_instance.fullname if original_metasploit_instance

    dlog("Reloading module #{module_fullname}...", 'core')

    if load_module(parent_path, type, module_reference_name, :force => true, :reload => true)
      # Create a new instance of the module, using the alias if one was used
      reloaded_module_instance = module_manager.create(module_used_name || module_fullname)
      if !reloaded_module_instance && module_fullname != module_used_name
        reloaded_module_instance = module_manager.create(module_fullname)
        reloaded_module_instance&.add_warning "Alias #{module_used_name} no longer available after reloading, using #{module_fullname}"
      end

      if reloaded_module_instance
        if original_metasploit_instance
          # copy over datastore
          reloaded_module_instance.datastore.update(original_metasploit_instance.datastore)
        end
      else
        elog("Failed to create instance of #{original_metasploit_class_or_instance.refname} after reload.")

        # Return the old module instance to avoid an strace trace
        return original_metasploit_class_or_instance
      end
    else
      elog("Failed to reload #{module_fullname}")

      return nil
    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.refresh_cache_from_module_files(reloaded_module_instance)

    reloaded_module_instance
  end

  protected

  # Returns a nested module to wrap the MetasploitModule 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 class.
  #
  # @param namespace_module_names [Array<String>]
  #   {NAMESPACE_MODULE_NAMES} + <derived-constant-safe names>
  # @return [Module] module that can wrap the module content from {#read_module_content} using
  #   module_eval_with_lexical_scope.
  #
  # @see NAMESPACE_MODULE_CONTENT
  def create_namespace_module(namespace_module_names)
    # 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")
    }

    # - because the added wrap lines have to act like they were written before NAMESPACE_MODULE_CONTENT
    line_with_wrapping = NAMESPACE_MODULE_LINE - nested_module_names.length
    Object.module_eval(namespace_module_content, __FILE__, line_with_wrapping)

    # The namespace_module exists now, so no need to use constantize to do const_missing
    namespace_module = current_module(namespace_module_names)
    # record the loader, so that the namespace module and its metasploit_class can be reloaded
    namespace_module.loader = self

    namespace_module
  end

  # Returns the module with `module_names` if it exists.
  #
  # @param [Array<String>] module_names a list of module names to resolve from Object downward.
  # @return [Module] module that wraps the previously loaded content from {#read_module_content}.
  # @return [nil] if any module name along the chain does not exist.
  def current_module(module_names)
    # Don't want to trigger ActiveSupport's const_missing, so can't use constantize.
    named_module = module_names.reduce(Object) do |parent, module_name|
      # Since we're searching parent namespaces first anyway, this is
      # semantically equivalent to providing false for the 1.9-only
      # "inherit" parameter to const_defined?. If we ever drop 1.8
      # support, we can save a few cycles here by adding it back.
      return unless parent.const_defined?(module_name)
      parent.const_get(module_name)
    end

    named_module
  end

  # Yields module reference names under path.
  #
  # @abstract Override and search the path for modules.
  #
  # @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

  # Records the load error to {Msf::ModuleManager::Loading#module_load_error_by_path} and the log.
  #
  # @param [String] module_path Path to the module as returned by {#module_path}.
  # @param [Exception, #class, #to_s, #backtrace] error the error that cause the module not to load.
  # @return [void]
  #
  # @see #module_path
  def load_error(module_path, error)
    # module_load_error_by_path does not get the backtrace because the value is echoed to the msfconsole where
    # backtraces should not appear.
    module_manager.module_load_error_by_path[module_path] = "#{error.class} #{error}"

    elog("#{module_path} failed to load", error: error)
  end

  # Records the load warning to {Msf::ModuleManager::Loading#module_load_warnings} and the log.
  #
  # @param [String] module_path Path to the module as returned by {#module_path}.
  # @param [String] error Error message that caused the warning.
  # @return [void]
  #
  # @see #module_path
  def load_warning(module_path, error)
    module_manager.module_load_warnings[module_path] = error.to_s

    log_lines = []
    log_lines << "#{module_path} generated a warning during load:"
    log_lines << error.to_s
    log_message = log_lines.join(' ')
    wlog(log_message)
  end

  # @return [Msf::ModuleManager] The module manager for which this loader is loading modules.
  attr_reader :module_manager

  # Returns path to module that can be used for reporting errors in evaluating the
  # {#read_module_content module_content}.
  #
  # @abstract Override to return the path to the module on the file system so that errors can be reported correctly.
  #
  # @param parent_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_TEST_REGEX} AND
  #                   the path is not hidden (starts with '.')
  # @return [false] otherwise
  def module_path?(path)
    path.ends_with?(MODULE_EXTENSION) &&
      File.file?(path) &&
      !path.starts_with?(".") &&
      !path.match?(UNIT_TEST_REGEX) &&
      !script_path?(path)
  end

  # Tries to determine if a file might be executable,
  def script_path?(path)
    File.file?(path) &&
      File.executable?(path) &&
      ['#!', '//'].include?(File.read(path, 2))
  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 the fully-qualified name to the {#create_namespace_module} that wraps the module with the given module
  # reference name.
  #
  # @param [String] module_full_name The canonical name for referring to the
  #   module.
  # @return [String] name of module.
  #
  # @see MODULE_SEPARATOR
  # @see #namespace_module_names
  def namespace_module_name(module_full_name)
    namespace_module_names = self.namespace_module_names(module_full_name)
    namespace_module_name = namespace_module_names.join(MODULE_SEPARATOR)

    namespace_module_name
  end

  # Returns an Array of names to make a fully qualified module name to
  # wrap the MetasploitModule class so that it doesn't overwrite other
  # (metasploit) module's classes.
  #
  # @param [String] module_full_name The unique canonical name
  #   for the module including type.
  # @return [Array<String>] {NAMESPACE_MODULE_NAMES} + <derived-constant-safe names>
  #
  # @see namespace_module
  def namespace_module_names(module_full_name)
    relative_name = module_full_name.split('/').map(&:capitalize).join('__')
    NAMESPACE_MODULE_NAMES + [relative_name]
  end

  # This reverses a namespace module's relative name to a module full name
  #
  # @param [String] relative_name The namespace module's relative name
  # @return [String] The module full name
  #
  # @see namespace_module_names
  def self.reverse_relative_name(relative_name)
    relative_name.split('__').map(&:downcase).join('/')
  end

  def namespace_module_transaction(module_full_name, options={}, &block)
    options.assert_valid_keys(:reload)

    reload = options[:reload] || false
    namespace_module_names = self.namespace_module_names(module_full_name)

    previous_namespace_module = current_module(namespace_module_names)

    if previous_namespace_module and not reload
      elog("Reloading namespace_module #{previous_namespace_module} when :reload => false")
    end

    relative_name = namespace_module_names.last

    if previous_namespace_module
      parent_module = previous_namespace_module.module_parent
      # remove_const is private, so use send to bypass
      parent_module.send(:remove_const, relative_name)
    end

    namespace_module = create_namespace_module(namespace_module_names)
    # Get the parent module from the created module so that
    # restore_namespace_module can remove namespace_module's constant if
    # needed.
    parent_module = namespace_module.module_parent

    begin
      loaded = block.call(namespace_module)
    rescue Exception
      restore_namespace_module(parent_module, relative_name, previous_namespace_module)

      # re-raise the original exception in the original context
      raise
    else
      unless loaded
        restore_namespace_module(parent_module, relative_name, previous_namespace_module)
      end

      loaded
    end
  end

  # Read the content of the module from under path.
  #
  # @abstract Override to read the module content based on the method of the loader subclass and return a string.
  #
  # @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 {#create_namespace_module}
  def read_module_content(parent_path, type, module_reference_name)
    raise ::NotImplementedError
  end

  # Read the content of a module
  #
  # @abstract Override to read the module content based on the method of the loader subclass and return a string.
  #
  # @param full_path Path to the module to be read
  # @return [String] module content that can be module_evaled into the {#create_namespace_module}
  def read_module_content_from_path(full_path)
    raise ::NotImplementedError
  end

  # Restores the namespace module to its original name under its original parent Module if there was a previous
  # namespace module.
  #
  # @param [Module] parent_module The .parent of namespace_module before it was removed from the constant tree.
  # @param [String] relative_name The name of the constant under parent_module where namespace_module was attached.
  # @param [Module, nil] namespace_module The previous namespace module containing the old module content.  If `nil`,
  #   then the relative_name constant is removed from parent_module, but nothing is set as the new constant.
  # @return [void]
  def restore_namespace_module(parent_module, relative_name, namespace_module)
    if parent_module
      # If there is a current module with relative_name
      if parent_module.const_defined?(relative_name)
        # if the current value isn't the value to be restored.
        if parent_module.const_get(relative_name) != namespace_module
          # remove_const is private, so use send to bypass
          parent_module.send(:remove_const, relative_name)

          # if there was a previous module, not set it to the name
          if namespace_module
            parent_module.const_set(relative_name, namespace_module)
          end
        end
      else
        # if there was a previous module, but there isn't a current module, then restore the previous module
        if namespace_module
          parent_module.const_set(relative_name, namespace_module)
        end
      end
    end
  end

  # The path to the module qualified by the type directory.
  #
  # @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 self.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

  # The path to the module qualified by the type directory.
  #
  # @note To get the full path to the module, use {#module_path}.
  #
  # @param (see typed_path)
  # @return (see typed_path)
  def typed_path(type, module_reference_name)
    self.class.typed_path(type, module_reference_name)
  end

end