From 2cf045d3c423a50e5a9431028d544684fcf6b032 Mon Sep 17 00:00:00 2001 From: Dean Welch Date: Mon, 15 Jan 2024 14:56:46 +0000 Subject: [PATCH] Leverage the module metadata cache in the module_sets --- lib/msf/core/encoded_payload.rb | 4 +- .../core/exploit/remote/browser_autopwn2.rb | 4 +- lib/msf/core/module/platform.rb | 1 + lib/msf/core/module/platform_list.rb | 8 +- lib/msf/core/module_set.rb | 108 +++--- lib/msf/core/modules/metadata/cache.rb | 10 +- lib/msf/core/payload_generator.rb | 2 +- lib/msf/core/rpc/v10/rpc_module.rb | 8 +- lib/msf/core/rpc/v10/rpc_session.rb | 2 +- .../ui/console/command_dispatcher/evasion.rb | 6 +- .../ui/console/command_dispatcher/exploit.rb | 6 +- lib/msf/ui/console/command_dispatcher/jobs.rb | 4 +- .../ui/console/command_dispatcher/payload.rb | 2 +- .../console/module_option_tab_completion.rb | 4 +- .../ui/console/command_dispatcher/core.rb | 2 +- .../ui/console/command_dispatcher/core.rb | 2 +- .../multi/recon/local_exploit_suggester.rb | 5 +- modules/post/windows/manage/peinjector.rb | 2 +- plugins/nessus.rb | 2 +- .../msf/core/exploit/browser_autopwn2_spec.rb | 4 +- spec/lib/msf/core/module_set_spec.rb | 315 ++++++++++++------ spec/lib/msf/core/payload_generator_spec.rb | 6 +- test/modules/post/test/all.rb | 2 +- tools/modules/missing_payload_tests.rb | 2 +- tools/modules/module_ports.rb | 2 +- 25 files changed, 294 insertions(+), 219 deletions(-) diff --git a/lib/msf/core/encoded_payload.rb b/lib/msf/core/encoded_payload.rb index f732a72b79..9e473f9d90 100644 --- a/lib/msf/core/encoded_payload.rb +++ b/lib/msf/core/encoded_payload.rb @@ -140,7 +140,7 @@ class EncodedPayload # as the framework's list of encoder names so we can compare them later. # This is important for when we get input from RPC. if reqs['Encoder'] - reqs['Encoder'] = reqs['Encoder'].encode(framework.encoders.keys[0].encoding) + reqs['Encoder'] = reqs['Encoder'].encode(framework.encoders.module_refnames[0].encoding) end # If the caller had a preferred encoder, use this encoder only @@ -342,7 +342,7 @@ class EncodedPayload wlog("#{pinst.refname}: Failed to find preferred nop #{reqs['Nop']}") end - nops.each { |nopname, nopmod| + nops.each_module { |nopname, nopmod| # Create an instance of the nop module self.nop = nopmod.new diff --git a/lib/msf/core/exploit/remote/browser_autopwn2.rb b/lib/msf/core/exploit/remote/browser_autopwn2.rb index b5a0bdc4a4..0498199295 100644 --- a/lib/msf/core/exploit/remote/browser_autopwn2.rb +++ b/lib/msf/core/exploit/remote/browser_autopwn2.rb @@ -48,7 +48,7 @@ module Msf # @return [void] def init_exploits # First we're going to avoid using #find_all because that gets very slow. - framework.exploits.each_pair do |fullname, place_holder| + framework.exploits.module_refnames.each do | fullname | # If the place holder isn't __SYMBOLIC__, then that means the module is initialized, # and that's gotta be the active browser autopwn. next if !fullname.include?('browser') || self.fullname == "exploit/#{fullname}" @@ -269,7 +269,7 @@ module Msf # The payload is legit, we can use it. # Avoid #create seems faster - return payload_name if framework.payloads.keys.include?(payload_name) + return payload_name if framework.payloads.module_refnames.include?(payload_name) default = DEFAULT_PAYLOADS[platform][:payload] diff --git a/lib/msf/core/module/platform.rb b/lib/msf/core/module/platform.rb index 34ecf4f1d2..a025d29a84 100644 --- a/lib/msf/core/module/platform.rb +++ b/lib/msf/core/module/platform.rb @@ -131,6 +131,7 @@ class Msf::Module::Platform # the string). # def self.find_portion(mod, str) + return [mod, ''] if str == Short # Check to see if we've built the abbreviated cache if (not ( diff --git a/lib/msf/core/module/platform_list.rb b/lib/msf/core/module/platform_list.rb index e62f1c8d16..4c14479a71 100644 --- a/lib/msf/core/module/platform_list.rb +++ b/lib/msf/core/module/platform_list.rb @@ -25,11 +25,13 @@ class Msf::Module::PlatformList # convenient. # def self.transform(src) - if (src.kind_of?(Array)) - from_a(src) + if src.is_a?(String) + # Platforms are stored in the metadata cache as a comma separated string + platforms = src.split(',') else - from_a([src]) + platforms = Array.wrap(src) end + from_a(platforms) end # diff --git a/lib/msf/core/module_set.rb b/lib/msf/core/module_set.rb index 6ec6a900b4..e9a6ea534d 100644 --- a/lib/msf/core/module_set.rb +++ b/lib/msf/core/module_set.rb @@ -20,11 +20,11 @@ class Msf::ModuleSet < Hash # and then returns the now-loaded class afterwards. # # @param [String] name the module reference name - # @return [Msf::Module] instance of the of the Msf::Module subclass with the given reference name + # @return [Msf::Module] Class of the of the Msf::Module with the given reference name def [](name) - module_instance = super - if module_instance == Msf::SymbolicModule || module_instance.nil? - create(name) + module_class = super + if module_class == Msf::SymbolicModule || module_class.nil? + load_module_class(name) end super @@ -36,14 +36,8 @@ class Msf::ModuleSet < Hash # @return [Msf::Module,nil] Instance of the named module or nil if it # could not be created. def create(reference_name, cache_type: Msf::ModuleManager::Cache::FILESYSTEM) - klass = fetch(reference_name, nil) + klass = load_module_class(reference_name, cache_type: cache_type) instance = nil - # If there is no module associated with this class, then try to demand load it. - if klass.nil? or klass == Msf::SymbolicModule - framework.modules.load_cached_module(module_type, reference_name, cache_type: cache_type) - klass = fetch(reference_name, nil) - end - # If the klass is valid for this reference_name, try to create it unless klass.nil? or klass == Msf::SymbolicModule instance = klass.new @@ -56,7 +50,7 @@ class Msf::ModuleSet < Hash self.delete(reference_name) end - return instance + instance end # Overrides the builtin 'each' operator to avoid the following exception on Ruby 1.9.2+ @@ -68,7 +62,7 @@ class Msf::ModuleSet < Hash # @return [void] def each(&block) list = [] - self.keys.sort.each do |sidx| + module_metadata.keys.sort.each do |sidx| list << [sidx, self[sidx]] end list.each(&block) @@ -81,9 +75,7 @@ class Msf::ModuleSet < Hash # @yieldparam (see #each_module_list) # @return (see #each_module_list) def each_module(opts = {}, &block) - demand_load_modules - - self.mod_sorted = self.sort + self.mod_sorted = module_metadata.sort each_module_list(mod_sorted, opts, &block) end @@ -107,8 +99,6 @@ class Msf::ModuleSet < Hash # @yieldparam (see #each_module_list) # @return (see #each_module_list) def each_module_ranked(opts = {}, &block) - demand_load_modules - each_module_list(rank_modules, opts, &block) end @@ -166,7 +156,6 @@ class Msf::ModuleSet < Hash # @return [true] if the module can be {#create created} and cached. # @return [false] otherwise def valid?(reference_name) - create(reference_name) (self[reference_name]) ? true : false end @@ -203,28 +192,12 @@ class Msf::ModuleSet < Hash klass end - protected - - # Load all modules that are marked as being symbolic. - # - # @return [void] - def demand_load_modules - found_symbolics = false - # Pre-scan the module list for any symbolic modules - self.each_pair { |name, mod| - if (mod == Msf::SymbolicModule) - found_symbolics = true - mod = create(name) - next if (mod.nil?) - end - } - - # If we found any symbolic modules, then recalculate. - if (found_symbolics) - recalculate - end + def module_refnames + module_metadata.keys end + protected + # Enumerates the modules in the supplied array with possible limiting factors. # # @param [Array>] ary Array of module reference name and module class pairs @@ -238,35 +211,32 @@ class Msf::ModuleSet < Hash # @yieldparam [Class] module The module class: a subclass of {Msf::Module}. # @return [void] def each_module_list(ary, opts, &block) - ary.each { |entry| - name, mod = entry - - # Skip any lingering symbolic modules. - next if (mod == Msf::SymbolicModule) + ary.each do |entry| + name, module_metadata = entry # Filter out incompatible architectures if (opts['Arch']) - if (!architectures_by_module[mod]) - architectures_by_module[mod] = mod.new.arch + if (!architectures_by_module[name]) + architectures_by_module[name] = Array.wrap(module_metadata.arch) end - next if ((architectures_by_module[mod] & opts['Arch']).empty? == true) + next if ((architectures_by_module[name] & opts['Arch']).empty? == true) end # Filter out incompatible platforms if (opts['Platform']) - if (!platforms_by_module[mod]) - platforms_by_module[mod] = mod.new.platform + if (!platforms_by_module[name]) + platforms_by_module[name] = Msf::Module::PlatformList.transform(module_metadata.platform) end - next if ((platforms_by_module[mod] & opts['Platform']).empty? == true) + next if ((platforms_by_module[name] & opts['Platform']).empty? == true) end # Custom filtering next if (each_module_filter(opts, name, entry) == true) - block.call(name, mod) - } + block.call(name, self[name]) + end end # @!attribute [rw] ambiguous_module_reference_name_set @@ -304,33 +274,31 @@ class Msf::ModuleSet < Hash # @return [Array>] Array of arrays where the inner array is a pair of the module reference name # and the module class. def rank_modules - self.sort_by { |pair| module_rank(*pair) }.reverse! + module_metadata.sort_by { |refname, _metadata| module_rank(refname) }.reverse! end # Retrieves the rank from a loaded, not-yet-loaded, or unloadable Metasploit Module. # # @param reference_name [String] The reference name of the Metasploit Module - # @param metasploit_module_class [Class, Msf::SymbolicModule] The loaded `Class` for the Metasploit - # Module, or {Msf::SymbolicModule} if the Metasploit Module is not loaded yet. # @return [Integer] an `Msf::*Ranking`. `Msf::ManualRanking` if `metasploit_module_class` is `nil` or # {Msf::SymbolicModule} and it could not be loaded by {#create}. Otherwise, the `Rank` constant of the # `metasploit_module_class` or {Msf::NormalRanking} if `metasploit_module_class` does not define `Rank`. - def module_rank(reference_name, metasploit_module_class) - if metasploit_module_class.nil? - Msf::ManualRanking - elsif metasploit_module_class == Msf::SymbolicModule - # TODO don't create an instance just to get the Class. - created_metasploit_module_instance = create(reference_name) + def module_rank(reference_name) + module_metadata[reference_name].rank || Msf::NormalRanking + end - if created_metasploit_module_instance.nil? - module_rank(reference_name, nil) - else - module_rank(reference_name, created_metasploit_module_instance.class) - end - elsif metasploit_module_class.const_defined? :Rank - metasploit_module_class.const_get :Rank - else - Msf::NormalRanking + def module_metadata + Msf::Modules::Metadata::Cache.instance.module_metadata(module_type) + end + + def load_module_class(reference_name, cache_type: Msf::ModuleManager::Cache::FILESYSTEM) + klass = fetch(reference_name, nil) + + # If there is no module associated with this class, then try to demand load it. + if klass.nil? || klass == Msf::SymbolicModule + framework.modules.load_cached_module(module_type, reference_name, cache_type: cache_type) + klass = fetch(reference_name, nil) end + klass end end diff --git a/lib/msf/core/modules/metadata/cache.rb b/lib/msf/core/modules/metadata/cache.rb index 170c121435..0d53fb99ba 100644 --- a/lib/msf/core/modules/metadata/cache.rb +++ b/lib/msf/core/modules/metadata/cache.rb @@ -87,6 +87,14 @@ class Cache } end + def module_metadata(type) + @mutex.synchronize do + wait_for_load + # TODO: Should probably figure out a way to cache this + @module_metadata_cache.filter_map { |_, metadata| [metadata.ref_name, metadata] if metadata.type == type }.to_h + end + end + ####### private ####### @@ -155,7 +163,7 @@ class Cache @module_metadata_cache = {} @store_loaded = false @console = Rex::Ui::Text::Output::Stdio.new - @load_thread = Thread.new { + @load_thread = Thread.new { init_store @store_loaded = true } diff --git a/lib/msf/core/payload_generator.rb b/lib/msf/core/payload_generator.rb index b70d813f56..29c124afdc 100644 --- a/lib/msf/core/payload_generator.rb +++ b/lib/msf/core/payload_generator.rb @@ -598,7 +598,7 @@ module Msf # @return [True] if the payload is a valid Metasploit Payload # @return [False] if the payload is not a valid Metasploit Payload def payload_is_valid? - (framework.payloads.keys + ['stdin']).include? payload + (framework.payloads.module_refnames + ['stdin']).include? payload end end diff --git a/lib/msf/core/rpc/v10/rpc_module.rb b/lib/msf/core/rpc/v10/rpc_module.rb index e9e2195140..076d5a5512 100644 --- a/lib/msf/core/rpc/v10/rpc_module.rb +++ b/lib/msf/core/rpc/v10/rpc_module.rb @@ -13,7 +13,7 @@ class RPC_Module < RPC_Base # @example Here's how you would use this from the client: # rpc.call('module.exploits') def rpc_exploits - { "modules" => self.framework.exploits.keys } + { "modules" => self.framework.exploits.module_refnames } end @@ -24,7 +24,7 @@ class RPC_Module < RPC_Base # @example Here's how you would use this from the client: # rpc.call('module.evasion') def rpc_evasion - { "modules" => self.framework.evasion.keys } + { "modules" => self.framework.evasion.module_refnames } end @@ -35,7 +35,7 @@ class RPC_Module < RPC_Base # @example Here's how you would use this from the client: # rpc.call('module.auxiliary') def rpc_auxiliary - { "modules" => self.framework.auxiliary.keys } + { "modules" => self.framework.auxiliary.module_refnames } end @@ -185,7 +185,7 @@ class RPC_Module < RPC_Base # @example Here's how you would use this from the client: # rpc.call('module.post') def rpc_post - { "modules" => self.framework.post.keys } + { "modules" => self.framework.post.module_refnames } end diff --git a/lib/msf/core/rpc/v10/rpc_session.rb b/lib/msf/core/rpc/v10/rpc_session.rb index 51d9bbf362..83d490ff3b 100644 --- a/lib/msf/core/rpc/v10/rpc_session.rb +++ b/lib/msf/core/rpc/v10/rpc_session.rb @@ -468,7 +468,7 @@ class RPC_Session < RPC_Base ret = [] mtype = "post" - names = self.framework.post.keys.map{ |x| "post/#{x}" } + names = self.framework.post.module_refnames.map{ |x| "post/#{x}" } names.each do |mname| m = _find_module(mtype, mname) next if not m.session_compatible?(sid) diff --git a/lib/msf/ui/console/command_dispatcher/evasion.rb b/lib/msf/ui/console/command_dispatcher/evasion.rb index 858db3af37..d710625f47 100644 --- a/lib/msf/ui/console/command_dispatcher/evasion.rb +++ b/lib/msf/ui/console/command_dispatcher/evasion.rb @@ -66,14 +66,14 @@ class Evasion # def cmd_run_tabs(str, words) fmt = { - '-e' => [ framework.encoders.map { |refname, mod| refname } ], + '-e' => [ framework.encoders.module_refnames ], '-f' => [ nil ], '-h' => [ nil ], '-j' => [ nil ], '-J' => [ nil ], - '-n' => [ framework.nops.map { |refname, mod| refname } ], + '-n' => [ framework.nops.module_refnames ], '-o' => [ true ], - '-p' => [ framework.payloads.map { |refname, mod| refname } ], + '-p' => [ framework.payloads.module_refnames ], '-r' => [ nil ], '-t' => [ true ], '-z' => [ nil ] diff --git a/lib/msf/ui/console/command_dispatcher/exploit.rb b/lib/msf/ui/console/command_dispatcher/exploit.rb index 311d4287b3..392fccbf86 100644 --- a/lib/msf/ui/console/command_dispatcher/exploit.rb +++ b/lib/msf/ui/console/command_dispatcher/exploit.rb @@ -66,14 +66,14 @@ class Exploit # def cmd_run_tabs(str, words) fmt = { - '-e' => [ framework.encoders.map { |refname, mod| refname } ], + '-e' => [ framework.encoders.module_refnames ], '-f' => [ nil ], '-h' => [ nil ], '-j' => [ nil ], '-J' => [ nil ], - '-n' => [ framework.nops.map { |refname, mod| refname } ], + '-n' => [ framework.nops.module_refnames ], '-o' => [ true ], - '-p' => [ framework.payloads.map { |refname, mod| refname } ], + '-p' => [ framework.payloads.module_refnames ], '-r' => [ nil ], '-t' => [ true ], '-z' => [ nil ] diff --git a/lib/msf/ui/console/command_dispatcher/jobs.rb b/lib/msf/ui/console/command_dispatcher/jobs.rb index 9efd8850c6..7cae6ad0fe 100644 --- a/lib/msf/ui/console/command_dispatcher/jobs.rb +++ b/lib/msf/ui/console/command_dispatcher/jobs.rb @@ -438,10 +438,10 @@ module Msf fmt = { '-h' => [ nil ], '-x' => [ nil ], - '-p' => [ framework.payloads.map { |refname, mod| refname } ], + '-p' => [ framework.payloads.module_refnames ], '-P' => [ true ], '-H' => [ :address ], - '-e' => [ framework.encoders.map { |refname, mod| refname } ], + '-e' => [ framework.encoders.module_refnames ], '-n' => [ true ] } tab_complete_generic(fmt, str, words) diff --git a/lib/msf/ui/console/command_dispatcher/payload.rb b/lib/msf/ui/console/command_dispatcher/payload.rb index 1207bd9b49..927691c19d 100644 --- a/lib/msf/ui/console/command_dispatcher/payload.rb +++ b/lib/msf/ui/console/command_dispatcher/payload.rb @@ -213,7 +213,7 @@ module Msf fmt = { '-b' => [ true ], '-E' => [ nil ], - '-e' => [ framework.encoders.map { |refname, _mod| refname } ], + '-e' => [ framework.encoders.module_refnames ], '-h' => [ nil ], '-o' => [ :file ], '-P' => [ true ], diff --git a/lib/msf/ui/console/module_option_tab_completion.rb b/lib/msf/ui/console/module_option_tab_completion.rb index 20cfeef29d..f8ecd07216 100644 --- a/lib/msf/ui/console/module_option_tab_completion.rb +++ b/lib/msf/ui/console/module_option_tab_completion.rb @@ -305,14 +305,14 @@ module Msf # Provide valid nops options for the current exploit # def option_values_nops - framework.nops.map { |refname, _mod| refname } + framework.nops.module_refnames end # # Provide valid encoders options for the current exploit or payload # def option_values_encoders - framework.encoders.map { |refname, _mod| refname } + framework.encoders.module_refnames end # diff --git a/lib/rex/post/hwbridge/ui/console/command_dispatcher/core.rb b/lib/rex/post/hwbridge/ui/console/command_dispatcher/core.rb index 4787c54ff0..4f87d29308 100644 --- a/lib/rex/post/hwbridge/ui/console/command_dispatcher/core.rb +++ b/lib/rex/post/hwbridge/ui/console/command_dispatcher/core.rb @@ -572,7 +572,7 @@ protected end def tab_complete_postmods - tabs = client.framework.modules.post.map { |name, klass| + tabs = client.framework.modules.post.module_refnames.each { | name | mod = client.framework.modules.post.create(name) if mod && mod.session_compatible?(client) mod.fullname.dup diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb index 8147705bb4..471ec6e6a9 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb @@ -1697,7 +1697,7 @@ protected end end - client.framework.modules.post.map do |name,klass| + client.framework.modules.post.module_refnames.each do | name | tabs << 'post/' + name end client.framework.modules.module_names('exploit'). diff --git a/modules/post/multi/recon/local_exploit_suggester.rb b/modules/post/multi/recon/local_exploit_suggester.rb index 71dc64c85d..ff0b76d8c6 100644 --- a/modules/post/multi/recon/local_exploit_suggester.rb +++ b/modules/post/multi/recon/local_exploit_suggester.rb @@ -140,8 +140,9 @@ class MetasploitModule < Msf::Post # Collects exploits into an array @local_exploits = [] - framework.exploits.each_with_index do |(name, _obj), index| - print "%bld%blu[*]%clr Collecting exploit #{index + 1} / #{framework.exploits.count}\r" + exploit_refnames = framework.exploits.module_refnames + exploit_refnames.each_with_index do |name, index| + print "%bld%blu[*]%clr Collecting exploit #{index + 1} / #{exploit_refnames.count}\r" mod = framework.exploits.create name next unless mod diff --git a/modules/post/windows/manage/peinjector.rb b/modules/post/windows/manage/peinjector.rb index f161e57cbb..031f2bed9e 100644 --- a/modules/post/windows/manage/peinjector.rb +++ b/modules/post/windows/manage/peinjector.rb @@ -45,7 +45,7 @@ class MetasploitModule < Msf::Post print_status("Running module against #{sysinfo['Computer']}") if !sysinfo.nil? # Check that the payload is a Windows one and on the list - if !session.framework.payloads.keys.grep(/windows/).include?(datastore['PAYLOAD']) + if !session.framework.payloads.module_refnames.grep(/windows/).include?(datastore['PAYLOAD']) print_error("The Payload specified #{datastore['PAYLOAD']} is not a valid for this system") return end diff --git a/plugins/nessus.rb b/plugins/nessus.rb index 9a881e7ee6..088942732d 100644 --- a/plugins/nessus.rb +++ b/plugins/nessus.rb @@ -104,7 +104,7 @@ module Msf File.open(xindex.to_s, 'w+') do |f| # need to add version line. f.puts(Msf::Framework::Version) - framework.exploits.sort.each do |refname, mod| + framework.exploits.each_module do |refname, mod| stuff = '' o = nil begin diff --git a/spec/lib/msf/core/exploit/browser_autopwn2_spec.rb b/spec/lib/msf/core/exploit/browser_autopwn2_spec.rb index e790f1c042..c486b63e04 100644 --- a/spec/lib/msf/core/exploit/browser_autopwn2_spec.rb +++ b/spec/lib/msf/core/exploit/browser_autopwn2_spec.rb @@ -361,7 +361,7 @@ RSpec.describe Msf::Exploit::Remote::BrowserAutopwn2 do # Prepare framework.exploits exploits = double('exploits') allow(exploits).to receive(:create) { |arg| mock_exploit_create(arg) } - allow(exploits).to receive(:each_pair).and_yield(available_exploits[0].fullname, '__SYMBOLIC__').and_yield(available_exploits[1].fullname, '__SYMBOLIC__').and_yield(available_exploits[2].fullname, '__SYMBOLIC__') + allow(exploits).to receive(:module_refnames).and_return(available_exploits.map(&:fullname)) allow(framework).to receive(:exploits).and_return(exploits) # Prepare jobs @@ -401,7 +401,7 @@ RSpec.describe Msf::Exploit::Remote::BrowserAutopwn2 do payload_class_by_reference_name[reference_name] = klass end - allow(payloads).to receive(:keys) { + allow(payloads).to receive(:module_refnames) { payload_class_by_reference_name.keys } diff --git a/spec/lib/msf/core/module_set_spec.rb b/spec/lib/msf/core/module_set_spec.rb index a8d075818b..5d9980ec87 100644 --- a/spec/lib/msf/core/module_set_spec.rb +++ b/spec/lib/msf/core/module_set_spec.rb @@ -1,18 +1,38 @@ require 'spec_helper' RSpec.describe Msf::ModuleSet do - subject(:module_set) { + subject(:module_set) do described_class.new(module_type) - } + end - let(:module_type) { + let(:module_type) do FactoryBot.generate :mdm_module_detail_mtype - } + end - context '#rank_modules' do - subject(:rank_modules) { + describe '#rank_modules' do + subject(:rank_modules) do module_set.send(:rank_modules) - } + end + + let(:module_metadata_a) do + instance_double(Msf::Modules::Metadata::Obj) + end + + let(:module_metadata_b) do + instance_double(Msf::Modules::Metadata::Obj) + end + + let(:module_metadata_c) do + instance_double(Msf::Modules::Metadata::Obj) + end + + let(:module_metadata) do + { + 'a' => module_metadata_a, + 'b' => module_metadata_b, + 'c' => module_metadata_c + } + end context 'with Msf::SymbolicModule' do before(:example) do @@ -21,47 +41,41 @@ RSpec.describe Msf::ModuleSet do module_set['c'] = Msf::SymbolicModule end - context 'create' do + describe '#create' do # # lets # - let(:b_class) { + let(:b_class) do Class.new - } + end - let(:c_class) { + let(:c_class) do Class.new - } + end context 'returns nil' do before(:example) do - hide_const('A::Rank') - allow(module_set).to receive(:create).with('a').and_return(nil) - - stub_const('B', b_class) - stub_const('B::Rank', Msf::LowRanking) - allow(module_set).to receive(:create).with('b').and_return(b_class.new) - - stub_const('C', c_class) - stub_const('C::Rank', Msf::AverageRanking) - allow(module_set).to receive(:create).with('c').and_return(c_class.new) + allow(module_metadata_a).to receive(:rank).and_return(nil) + allow(module_metadata_b).to receive(:rank).and_return(Msf::AverageRanking) + allow(module_metadata_c).to receive(:rank).and_return(Msf::GoodRanking) + allow(Msf::Modules::Metadata::Cache.instance).to receive(:module_metadata).with(anything).and_return(module_metadata) end - specify { - expect { + specify do + expect do rank_modules - }.not_to raise_error - } + end.not_to raise_error + end - it 'is ranked as Manual' do + it 'is ranked as Normal' do expect(rank_modules).to eq( - [ - ['c', Msf::SymbolicModule], - ['b', Msf::SymbolicModule], - ['a', Msf::SymbolicModule] - ] - ) + [ + ['c', module_metadata_c], + ['a', module_metadata_a], + ['b', module_metadata_b] + ] + ) end end @@ -70,9 +84,9 @@ RSpec.describe Msf::ModuleSet do # lets # - let(:a_class) { + let(:a_class) do Class.new - } + end # # Callbacks @@ -86,47 +100,20 @@ RSpec.describe Msf::ModuleSet do context 'with Rank' do before(:example) do - stub_const('A', a_class) - stub_const('A::Rank', Msf::LowRanking) - - stub_const('B', b_class) - stub_const('B::Rank', Msf::AverageRanking) - - stub_const('C', c_class) - stub_const('C::Rank', Msf::GoodRanking) + allow(module_metadata_a).to receive(:rank).and_return(Msf::LowRanking) + allow(module_metadata_b).to receive(:rank).and_return(Msf::AverageRanking) + allow(module_metadata_c).to receive(:rank).and_return(Msf::GoodRanking) + allow(Msf::Modules::Metadata::Cache.instance).to receive(:module_metadata).with(anything).and_return(module_metadata) end it 'is ranked using Rank' do expect(rank_modules).to eq( - [ - ['c', Msf::SymbolicModule], - ['b', Msf::SymbolicModule], - ['a', Msf::SymbolicModule] - ] - ) - end - end - - context 'without Rank' do - before(:example) do - stub_const('A', a_class) - hide_const('A::Rank') - - stub_const('B', b_class) - stub_const('B::Rank', Msf::AverageRanking) - - stub_const('C', c_class) - stub_const('C::Rank', Msf::GoodRanking) - end - - it 'is ranked as Normal' do - expect(rank_modules).to eq( - [ - ['c', Msf::SymbolicModule], - ['a', Msf::SymbolicModule], - ['b', Msf::SymbolicModule] - ] - ) + [ + ['c', module_metadata_c], + ['b', module_metadata_b], + ['a', module_metadata_a] + ] + ) end end end @@ -138,17 +125,17 @@ RSpec.describe Msf::ModuleSet do # lets # - let(:a_class) { + let(:a_class) do Class.new - } + end - let(:b_class) { + let(:b_class) do Class.new - } + end - let(:c_class) { + let(:c_class) do Class.new - } + end # # Callbacks @@ -162,47 +149,155 @@ RSpec.describe Msf::ModuleSet do context 'with Rank' do before(:example) do - stub_const('A', a_class) - stub_const('A::Rank', Msf::LowRanking) + allow(module_metadata_a).to receive(:rank).and_return(Msf::LowRanking) + allow(module_metadata_b).to receive(:rank).and_return(Msf::AverageRanking) + allow(module_metadata_c).to receive(:rank).and_return(Msf::GoodRanking) + allow(Msf::Modules::Metadata::Cache.instance).to receive(:module_metadata).with(anything).and_return(module_metadata) + end - stub_const('B', b_class) - stub_const('B::Rank', Msf::AverageRanking) - - stub_const('C', c_class) - stub_const('C::Rank', Msf::GoodRanking) - end - - it 'is ranked using Rank' do - expect(rank_modules).to eq( - [ - ['c', c_class], - ['b', b_class], - ['a', a_class] - ] - ) - end + it 'is ranked using Rank' do + expect(rank_modules).to eq( + [ + ['c', module_metadata_c], + ['b', module_metadata_b], + ['a', module_metadata_a] + ] + ) + end end context 'without Rank' do before(:example) do - stub_const('A', a_class) - hide_const('A::Rank') - - stub_const('B', b_class) - stub_const('B::Rank', Msf::AverageRanking) - - stub_const('C', c_class) - stub_const('C::Rank', Msf::GoodRanking) + allow(module_metadata_a).to receive(:rank).and_return(nil) + allow(module_metadata_b).to receive(:rank).and_return(Msf::AverageRanking) + allow(module_metadata_c).to receive(:rank).and_return(Msf::GoodRanking) + allow(Msf::Modules::Metadata::Cache.instance).to receive(:module_metadata).with(anything).and_return(module_metadata) end it 'is ranked as Normal' do expect(rank_modules).to eq( - [ - ['c', c_class], - ['a', a_class], - ['b', b_class] - ] - ) + [ + ['c', module_metadata_c], + ['a', module_metadata_a], + ['b', module_metadata_b] + ] + ) + end + end + end + end + + describe '#[]' do + let(:module_refname) { 'module_refname' } + let(:framework) { instance_double(Msf::Framework) } + let(:module_manager) { instance_double(Msf::ModuleManager) } + let(:cache_type) { Msf::ModuleManager::Cache::FILESYSTEM } + + before(:each) do + allow(subject).to receive(:create).with(module_refname) + allow(subject).to receive(:framework).and_return(framework) + allow(framework).to receive(:modules).and_return(module_manager) + allow(module_manager).to receive(:load_cached_module) + end + + context 'when the module set is empty' do + it 'loads the module class from the cache' do + subject[module_refname] + is_expected.not_to have_received(:create).with(module_refname) + expect(module_manager).to have_received(:load_cached_module).with(module_type, module_refname, cache_type: cache_type) + end + end + + context 'when the module set has symbolic modules' do + before(:each) do + subject[module_refname] = Msf::SymbolicModule + end + it 'attempts to create the module' do + subject[module_refname] + is_expected.not_to have_received(:create).with(module_refname) + expect(module_manager).to have_received(:load_cached_module).with(module_type, module_refname, cache_type: cache_type) + end + end + + context 'when a module is contained within the set' do + let(:stored_module) { double('module') } + before(:each) do + subject[module_refname] = stored_module + end + it 'does not attempt to create the module' do + expect(subject[module_refname]).to be(stored_module) + is_expected.not_to have_received(:create).with(module_refname) + expect(module_manager).not_to have_received(:load_cached_module) + end + end + end + + describe '#fetch' do + let(:module_refname) { 'module_refname' } + + context 'when the module set is empty' do + before(:each) do + allow(subject).to receive(:create).with(module_refname) + end + + # TODO: it's unexpected that `fetch` and `[]` would act this differently + # investigate implementing `to_hash` to tell ruby we act like a hash over extending Hash + # seems like this is potentially a feature not a bug, we use `fetch` to intentionally not create modules sometimes + xit 'attempts to create the module' do + subject.fetch(module_refname) + is_expected.to have_received(:create).with(module_refname) + end + end + end + + describe 'create' do + let(:module_refname) { 'module_refname' } + let(:framework) { instance_double(Msf::Framework) } + let(:module_manager) { instance_double(Msf::ModuleManager) } + let(:events) { double('events') } + let(:cache_type) { Msf::ModuleManager::Cache::FILESYSTEM } + + before(:each) do + allow(subject).to receive(:framework).and_return(framework) + allow(framework).to receive(:modules).and_return(module_manager) + allow(framework).to receive(:events).and_return(events) + allow(events).to receive(:on_module_created) + end + + context 'when module set is empty' do + context 'when the module cannot be loaded' do + before(:each) do + allow(subject).to receive(:fetch).and_return(nil) + allow(subject).to receive(:delete) + allow(module_manager).to receive(:load_cached_module) + end + + it 'fails to create the module' do + subject.create(module_refname, cache_type: cache_type) + expect(subject).to have_received(:fetch).with(module_refname, nil).twice + expect(subject).to have_received(:delete).with(module_refname) + expect(module_manager).to have_received(:load_cached_module).with(module_type, module_refname, cache_type: cache_type) + expect(events).not_to have_received(:on_module_created) + end + end + + context 'when the module can be loaded' do + let(:loaded_module) { instance_double(Class) } + let(:module_instance) { Class.new } + + before(:each) do + allow(subject).to receive(:fetch).and_return(nil, loaded_module) + allow(subject).to receive(:delete) + allow(module_manager).to receive(:load_cached_module) + allow(loaded_module).to receive(:new).and_return(module_instance) + end + + it 'creates the module' do + expect(subject.create(module_refname, cache_type: cache_type)).to be(module_instance) + expect(subject).to have_received(:fetch).with(module_refname, nil).twice + expect(subject).not_to have_received(:delete).with(module_refname) + expect(module_manager).to have_received(:load_cached_module).with(module_type, module_refname, cache_type: cache_type) + expect(events).to have_received(:on_module_created).with(module_instance) end end end diff --git a/spec/lib/msf/core/payload_generator_spec.rb b/spec/lib/msf/core/payload_generator_spec.rb index 8e43a98f4d..3ec5ef0343 100644 --- a/spec/lib/msf/core/payload_generator_spec.rb +++ b/spec/lib/msf/core/payload_generator_spec.rb @@ -1128,7 +1128,7 @@ RSpec.describe Msf::PayloadGenerator do } it 'calls the generate_war on the payload' do - allow(framework).to receive_message_chain(:payloads, :keys).and_return ['java/meterpreter/reverse_tcp'] + allow(framework).to receive_message_chain(:payloads, :module_refnames).and_return ['java/meterpreter/reverse_tcp'] allow(framework).to receive_message_chain(:payloads, :create).and_return(payload_module) expect(payload_module).to receive(:generate_war).and_call_original payload_generator.generate_java_payload @@ -1194,7 +1194,7 @@ RSpec.describe Msf::PayloadGenerator do } it 'calls the generate_jar on the payload' do - allow(framework).to receive_message_chain(:payloads, :keys).and_return ['java/meterpreter/reverse_tcp'] + allow(framework).to receive_message_chain(:payloads, :module_refnames).and_return ['java/meterpreter/reverse_tcp'] allow(framework).to receive_message_chain(:payloads, :create).and_return(payload_module) expect(payload_module).to receive(:generate_jar).and_call_original payload_generator.generate_java_payload @@ -1232,7 +1232,7 @@ RSpec.describe Msf::PayloadGenerator do } it 'calls #generate' do - allow(framework).to receive_message_chain(:payloads, :keys).and_return ['java/jsp_shell_reverse_tcp'] + allow(framework).to receive_message_chain(:payloads, :module_refnames).and_return ['java/jsp_shell_reverse_tcp'] allow(framework).to receive_message_chain(:payloads, :create).and_return(payload_module) expect(payload_module).to receive(:generate).and_call_original payload_generator.generate_java_payload diff --git a/test/modules/post/test/all.rb b/test/modules/post/test/all.rb index 18b49cd9ef..13172be517 100644 --- a/test/modules/post/test/all.rb +++ b/test/modules/post/test/all.rb @@ -40,7 +40,7 @@ class MetasploitModule < Msf::Post session_type = session.type module_results = [] - framework.modules.post.each do |refname, _clazz| + framework.modules.post.module_refnames.each do | refname | next unless refname.start_with?('test/') && refname != self.refname mod = framework.modules.create(refname) diff --git a/tools/modules/missing_payload_tests.rb b/tools/modules/missing_payload_tests.rb index c57e88f5c4..3be0212ce7 100755 --- a/tools/modules/missing_payload_tests.rb +++ b/tools/modules/missing_payload_tests.rb @@ -26,7 +26,7 @@ options_set_by_ancestor_reference_name = Hash.new { |hash, ancestor_reference_na hash[ancestor_reference_name] = Set.new } -framework.payloads.each { |reference_name, payload_class| +framework.payloads.each_module { |reference_name, payload_class| next unless payload_class module_ancestors = payload_class.ancestors.select { |ancestor| # need to use try because name may be nil for anonymous Modules diff --git a/tools/modules/module_ports.rb b/tools/modules/module_ports.rb index 804d501877..bb249c0d41 100755 --- a/tools/modules/module_ports.rb +++ b/tools/modules/module_ports.rb @@ -23,7 +23,7 @@ require 'rex' # Initialize the simplified framework instance. $framework = Msf::Simple::Framework.create('DisableDatabase' => true) - +# TODO: this is weird, merging module sets together for different module types could lead to unforseen issues all_modules = $framework.exploits.merge($framework.auxiliary) all_ports = {}