Land #18704, Leverage the module metadata cache in the module_sets

This commit is contained in:
adfoster-r7 2024-02-02 14:16:46 +00:00 committed by GitHub
commit 48221e594d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 356 additions and 244 deletions

View File

@ -76,7 +76,10 @@ jobs:
include:
- os: ubuntu-latest
ruby: '3.1'
test_cmd: 'bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" DATASTORE_FALLBACKS=1'
test_cmd: 'bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" MSF_FEATURE_DATASTORE_FALLBACKS=1'
- os: ubuntu-latest
ruby: '3.1'
test_cmd: 'bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" MSF_FEATURE_DEFER_MODULE_LOADS=1'
test_cmd:
- bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content"
- bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag ~content"

View File

@ -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
@ -237,9 +237,9 @@ class EncodedPayload
begin
eout = self.encoder.encode(eout, reqs['BadChars'], nil, pinst.platform)
rescue EncodingError
wlog("#{err_start}: Encoder #{encoder.refname} failed: #{$!}", 'core', LEV_1)
dlog("#{err_start}: Call stack\n#{$@.join("\n")}", 'core', LEV_3)
rescue EncodingError => e
wlog("#{err_start}: Encoder #{encoder.refname} failed: #{e}", 'core', LEV_1)
dlog("#{err_start}: Call stack\n#{e.backtrace}", 'core', LEV_3)
next_encoder = true
break
@ -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

View File

@ -48,9 +48,8 @@ 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|
# If the place holder isn't __SYMBOLIC__, then that means the module is initialized,
# and that's gotta be the active browser autopwn.
framework.exploits.module_refnames.each do |fullname|
next if !fullname.include?('browser') || self.fullname == "exploit/#{fullname}"
# The user gets to specify which modules to include/exclude
@ -269,7 +268,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]

View File

@ -131,7 +131,6 @@ class Msf::Module::Platform
# the string).
#
def self.find_portion(mod, str)
# Check to see if we've built the abbreviated cache
if (not (
mod.const_defined?('Abbrev') and

View File

@ -25,11 +25,7 @@ class Msf::Module::PlatformList
# convenient.
#
def self.transform(src)
if (src.kind_of?(Array))
from_a(src)
else
from_a([src])
end
from_a(Array.wrap(src))
end
#

View File

@ -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<Array<String, Class>>] 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] = module_metadata.platform_list
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,23 @@ class Msf::ModuleSet < Hash
# @return [Array<Array<String, Class>>] 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 do |refname, metadata|
[metadata.rank || Msf::NormalRanking, refname]
end.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::Module>, 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_metadata
Msf::Modules::Metadata::Cache.instance.module_metadata(module_type)
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 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

View File

@ -86,6 +86,14 @@ class Cache
end
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
#######
@ -154,7 +162,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
}

View File

@ -28,8 +28,10 @@ class Obj
attr_reader :description
# @return [Array<String>]
attr_reader :references
# @return [Boolean]
# @return [String]
attr_reader :platform
# @return [Msf::Module::PlatformList]
attr_reader :platform_list
# @return [String]
attr_reader :arch
# @return [Integer]
@ -90,6 +92,7 @@ class Obj
@default_credential = module_instance.default_cred?
@platform = module_instance.platform_to_s
@platform_list = module_instance.platform
# Done to ensure that differences do not show up for the same array grouping
sort_platform_string
@ -235,6 +238,7 @@ class Obj
@author = obj_hash['author'].nil? ? [] : obj_hash['author']
@references = obj_hash['references']
@platform = obj_hash['platform']
@platform_list = parse_platform_list(@platform)
@arch = obj_hash['arch']
@rport = obj_hash['rport']
@mod_time = Time.parse(obj_hash['mod_time'])
@ -288,6 +292,18 @@ class Obj
@references = @references.map {|r| r.dup.force_encoding(encoding)}
end
def parse_platform_list(platform_string)
return nil if platform_string.nil?
if platform_string.casecmp('All')
# empty string represents all platforms in Msf::Module::PlatformList
platforms = ['']
else
platforms = platform_string.split(',')
end
Msf::Module::PlatformList.transform(platforms)
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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)

View File

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

View File

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

View File

@ -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)

View File

@ -256,7 +256,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 ],

View File

@ -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
#

View File

@ -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

View File

@ -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').

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,18 +5,28 @@ RSpec.describe Msf::EncodedPayload do
include_context 'Msf::Simple::Framework#modules loading'
before do
ancestor_reference_names = [
# Excellent rank
'x86/shikata_ga_nai',
# Great rank
'x86/call4_dword_xor',
'x86/xor_dynamic',
'generic/none',
]
expect_to_load_module_ancestors(
ancestor_reference_names: [
# Excellent rank
'x86/shikata_ga_nai',
# Great rank
'x86/call4_dword_xor',
'x86/xor_dynamic',
'generic/none',
],
ancestor_reference_names: ancestor_reference_names,
module_type: 'encoder',
modules_path: modules_path,
)
# Improve test performance - return only the test modules that we're interested in
allow(framework.encoders).to receive(:rank_modules).and_wrap_original do |original, *args|
ranked_modules = original.call(*args)
ranked_modules.select do |ref_name, _metadata|
ancestor_reference_names.include?(ref_name)
end
end
end
let(:ancestor_reference_names) {

View File

@ -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
}

View File

@ -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,174 @@ 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
context 'with the same rank' do
before(:example) do
allow(module_metadata_a).to receive(:rank).and_return(Msf::AverageRanking)
allow(module_metadata_b).to receive(:rank).and_return(Msf::AverageRanking)
allow(module_metadata_c).to receive(:rank).and_return(Msf::AverageRanking)
allow(Msf::Modules::Metadata::Cache.instance).to receive(:module_metadata).with(anything).and_return(module_metadata)
end
it 'ranks the modules consistently' do
expect(rank_modules).to eq(
[
['c', module_metadata_c],
['b', module_metadata_b],
['a', module_metadata_a]
]
)
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

View File

@ -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

View File

@ -159,12 +159,18 @@ RSpec.configure do |config|
end
end
if ENV['DATASTORE_FALLBACKS']
if ENV['MSF_FEATURE_DATASTORE_FALLBACKS']
config.before(:suite) do
Msf::FeatureManager.instance.set(Msf::FeatureManager::DATASTORE_FALLBACKS, true)
end
end
if ENV['MSF_FEATURE_DEFER_MODULE_LOADS']
config.before(:suite) do
Msf::FeatureManager.instance.set(Msf::FeatureManager::DEFER_MODULE_LOADS, true)
end
end
# rex-text table performs word wrapping on msfconsole tables:
# https://github.com/rapid7/rex-text/blob/11e59416f7d8cce18b8b8b9893b3277e6ad0bea1/lib/rex/text/wrapped_table.rb#L74
# This can cause some integration tests to fail if the tests are run from smaller consoles

View File

@ -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)

View File

@ -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

View File

@ -23,7 +23,7 @@ require 'rex'
# Initialize the simplified framework instance.
$framework = Msf::Simple::Framework.create('DisableDatabase' => true)
# XXX: 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 = {}