Merge branch 'feature/MSP-11130/metasploit-framework-spec-constants' into feature/MSP-11147/thread-leak-detection

MSP-11147

Merge to get framework instance cleanup, which should clean up a lot of
thread leaks too.

Conflicts:
	Rakefile
	lib/metasploit/framework/spec.rb
	spec/spec_helper.rb
bug/bundler_fix
Luke Imhoff 2014-11-05 15:41:43 -06:00
commit d4d710cc3a
No known key found for this signature in database
GPG Key ID: 5B1FB01FB33356F8
22 changed files with 786 additions and 143 deletions

View File

@ -10,5 +10,6 @@ require 'metasploit/framework/spec/untested_payloads'
Metasploit::Framework::Require.optionally_active_record_railtie
Metasploit::Framework::Application.load_tasks
Metasploit::Framework::Spec::Constants.define_task
Metasploit::Framework::Spec::Threads::Suite.define_task
Metasploit::Framework::Spec::UntestedPayloads.define_task

View File

@ -1,5 +1,6 @@
module Metasploit::Framework::Spec
extend ActiveSupport::Autoload
autoload :Constants
autoload :Threads
end

View File

@ -0,0 +1,99 @@
require 'msf/core/modules'
# Monitor constants created by module loading to ensure that the loads in one example don't interfere with the
# assertions in another example.
module Metasploit::Framework::Spec::Constants
extend ActiveSupport::Autoload
autoload :Each
autoload :Suite
#
# CONSTANTS
#
# Regex parsing loaded module constants
LOADED_MODULE_CHILD_CONSTANT_REGEXP = /^Mod(?<unpacked_full_name>[0-9a-f]+)$/
# The parent namespace child_constant_name that can have children added when loading modules.
PARENT_CONSTANT = Msf::Modules
# Constant names under {PARENT_CONSTANT} that can persist between specs because they are part of the loader library
# and not dynamically loaded code
PERSISTENT_CHILD_CONSTANT_NAMES = %w{
Error
Loader
MetasploitClassCompatibilityError
Namespace
VersionCompatibilityError
}.map(&:to_sym)
# Cleans child constants from {PARENT_CONSTANT}.
#
# @return [true] if there were leaked constants that were cleaned.
# @return [false] if there were no leaked constants.
# @see each
def self.clean
count = each do |child_name|
PARENT_CONSTANT.send(:remove_const, child_name)
end
count != 0
end
# Adds actions to `spec` task so that `rake spec` fails if any of the following:
#
# # `log/leaked-constants.log` exists after printing out the leaked constants.
# # {Each.configured!} is unnecessary in `spec/spec_helper.rb` and should be removed.
#
# @return [void]
def self.define_task
Suite.define_task
# After Suite as Suite will kill for leaks before Each say it cleaned no leaks in case there are leaks in an
# `after(:all)` that {Each} won't catch in its `after(:each)` checks.
Each.define_task
end
# Yields each child_constant_name under {PARENT_CONSTANT}.
#
# @yield [child_name]
# @yieldparam child_name [Symbol] name of child_constant_name relative to {PARENT_CONSTANT}.
# @yieldreturn [void]
# @return [Integer] count
def self.each
inherit = false
count = 0
child_constant_names = PARENT_CONSTANT.constants(inherit)
child_constant_names.each do |child_constant_name|
unless PERSISTENT_CHILD_CONSTANT_NAMES.include? child_constant_name
count += 1
yield child_constant_name
end
end
count
end
# The module full name for `child_constant_name`
#
# @param child_constant_name [String] the name of a child constant_name under {PARENT_CONSTANT}.
# @return [String] full module name used to load `child_constant_name`.
# @return [nil] if `child_constant_name` does not correspond to a loaded module.
def self.full_name(child_constant_name)
full_name = nil
match = LOADED_MODULE_CHILD_CONSTANT_REGEXP.match(child_constant_name)
if match
potential_full_name = [match[:unpacked_full_name]].pack('H*')
module_type, _reference_name = potential_full_name.split('/', 2)
if Msf::MODULE_TYPES.include? module_type
full_name = potential_full_name
end
end
full_name
end
end

View File

@ -0,0 +1,119 @@
# @note This should only temporarily be used in `spec/spec_helper.rb` when
# `Metasploit::Framework::Spec::Constants::Suite.configure!` detects a leak. Permanently having
# `Metasploit::Framework::Spec::Constants::Each.configure!` can lead to false positives when modules are purposely
# loaded in a `before(:all)` and cleaned up in a `after(:all)`.
#
# Fails example if it leaks module loading constants.
module Metasploit::Framework::Spec::Constants::Each
#
# CONSTANTS
#
LOG_PATHNAME = Pathname.new('log/metasploit/framework/spec/constants/each.log')
#
# Module Methods
#
class << self
attr_accessor :leaks_cleaned
end
# Is {Metasploit::Framework::Spec::Constants::Each.configure!} still necessary or should it be removed?
#
# @return [true] if {configure!}'s `before(:each)` cleaned up leaked constants
# @return [false] otherwise
def self.leaks_cleaned?
!!@leaks_cleaned
end
# Configures after(:each) callback for RSpe to fail example if leaked constants.
#
# @return [void]
def self.configure!
unless @configured
RSpec.configure do |config|
config.before(:each) do |example|
leaks_cleaned = Metasploit::Framework::Spec::Constants.clean
if leaks_cleaned
$stderr.puts "Cleaned leaked constants before #{example.metadata.full_description}"
end
# clean so that leaks from earlier example aren't attributed to this example
Metasploit::Framework::Spec::Constants::Each.leaks_cleaned ||= leaks_cleaned
end
config.after(:each) do |example|
child_names = Metasploit::Framework::Spec::Constants.to_enum(:each).to_a
if child_names.length > 0
lines = ['Leaked constants:']
child_names.sort.each do |child_name|
lines << " #{child_name}"
end
lines << ''
lines << "Add `include_context 'Metasploit::Framework::Spec::Constants cleaner'` to clean up constants from #{example.metadata.full_description}"
message = lines.join("\n")
# use caller metadata so that Jump to Source in the Rubymine RSpec running jumps to the example instead of
# here
fail RuntimeError, message, example.metadata[:caller]
end
end
config.after(:suite) do
if Metasploit::Framework::Spec::Constants::Each.leaks_cleaned?
if LOG_PATHNAME.exist?
LOG_PATHNAME.delete
end
else
LOG_PATHNAME.open('w') { |f|
f.puts "No leaks were cleaned by `Metasploit::Framework::Spec::Constants::Each.configured!`. Remove " \
"it from `spec/spec_helper.rb` so it does not interfere with contexts that persist loaded " \
"modules for entire context and clean up modules in `after(:all)`"
}
end
end
end
@configured = true
end
end
# Whether {configure!} was called
#
# @return [Boolean]
def self.configured?
!!@configured
end
# Adds action to `spec` task so that `rake spec` fails if {configured!} is unnecessary in `spec/spec_helper.rb` and
# should be removed
#
# @return [void]
def self.define_task
Rake::Task.define_task('metasploit:framework:spec:constant:each:clean') do
if LOG_PATHNAME.exist?
LOG_PATHNAME.delete
end
end
Rake::Task.define_task(spec: 'metasploit:framework:spec:constant:each:clean')
Rake::Task.define_task(:spec) do
if LOG_PATHNAME.exist?
LOG_PATHNAME.open { |f|
f.each_line do |line|
$stderr.write line
end
}
exit(1)
end
end
end
end

View File

@ -0,0 +1,119 @@
# Logs if constants created by module loading are left over after suite has completed.
module Metasploit::Framework::Spec::Constants::Suite
#
# CONSTANTS
#
LOGS_PATHNAME = Pathname.new('log/metasploit/framework/spec/constants/suite')
# Logs leaked constants to {LOG_PATHNAME} and prints `message` to stderr.
#
# @param hook (see log_pathname)
# @param message [String] additional message printed to stderr when there is at least one leaked constant.
# @return [void]
def self.log_leaked_constants(hook, message)
count = 0
hook_log_pathname = log_pathname(hook)
hook_log_pathname.parent.mkpath
hook_log_pathname.open('w') do |f|
count = Metasploit::Framework::Spec::Constants.each do |child_name|
f.puts child_name
end
end
if count > 0
$stderr.puts "#{count} #{'constant'.pluralize(count)} leaked under " \
"#{Metasploit::Framework::Spec::Constants::PARENT_CONSTANT}. #{message} See #{hook_log_pathname} " \
"for details."
else
hook_log_pathname.delete
end
end
# Configures after(:suite) callback for RSpec to check for leaked constants.
def self.configure!
unless @configured
RSpec.configure do |config|
config.before(:suite) do
Metasploit::Framework::Spec::Constants::Suite.log_leaked_constants(
:before,
'Modules are being loaded outside callbacks before suite starts.'
)
end
config.after(:suite) do
Metasploit::Framework::Spec::Constants::Suite.log_leaked_constants(
:after,
'Modules are being loaded inside callbacks or examples during suite run.'
)
end
end
@configured = true
end
end
# Adds action to `spec` task so that `rake spec` fails if `log/leaked-constants.log` exists after printing out the
# leaked constants.
#
# @return [void]
def self.define_task
Rake::Task.define_task(:spec) do
leaked_before = Metasploit::Framework::Spec::Constants::Suite.print_leaked_constants(:before)
leaked_after = Metasploit::Framework::Spec::Constants::Suite.print_leaked_constants(:after)
# leaks after suite can be be cleaned up by {Metasploit::Framework::Spec::Constants::Each.configure!}, but
# leaks before suite require user intervention to find the leaks since it's a programming error in how the specs
# are written where Modules are being loaded in the context scope.
if leaked_after
$stderr.puts
$stderr.puts "Add `Metasploit::Framework::Spec::Constants::Each.configure!` to `spec/spec_helper.rb` " \
"**NOTE: `Metasploit::Framework::Spec::Constants::Each` may report false leaks if `after(:all)` " \
"is used to clean up constants instead of `after(:each)`**"
end
if leaked_before || leaked_after
exit 1
end
end
end
# @param hook [:after, :before] Whether the log is recording leaked constants `:before` the suite runs or `:after` the
# suite runs.
def self.log_pathname(hook)
LOGS_PATHNAME.join("#{hook}.log")
end
# Prints logged leaked constants to stderr.
#
# @param hook [:after, :before] Whether the log is recording leaked constants `:before` the suite runs or `:after` the
# suite runs.
# @return [true] if leaks printed
# @return [false] otherwise
def self.print_leaked_constants(hook)
hook_log_pathname = log_pathname(hook)
leaks = false
if hook_log_pathname.exist?
leaks = true
$stderr.puts "Leaked constants detected under #{Metasploit::Framework::Spec::Constants::PARENT_CONSTANT} #{hook} suite:"
hook_log_pathname.open do |f|
f.each_line do |line|
constant_name = line.strip
full_name = Metasploit::Framework::Spec::Constants.full_name(constant_name)
if full_name
formatted_full_name = " # #{full_name}"
end
$stderr.puts " #{constant_name}#{formatted_full_name}"
end
end
end
leaks
end
end

View File

@ -2,18 +2,30 @@ require 'spec_helper'
require 'msf/core/encoded_payload'
describe Msf::EncodedPayload do
PAYLOAD_FRAMEWORK = Msf::Simple::Framework.create(
:module_types => [::Msf::MODULE_PAYLOAD, ::Msf::MODULE_ENCODER, ::Msf::MODULE_NOP],
'DisableDatabase' => true,
'DisableLogging' => true
)
include_context 'Msf::Simple::Framework#modules loading'
let(:framework) { PAYLOAD_FRAMEWORK }
let(:payload) { 'linux/x86/shell_reverse_tcp' }
let(:pinst) { framework.payloads.create(payload) }
let(:ancestor_reference_names) {
%w{singles/linux/x86/shell_reverse_tcp}
}
let(:module_type) {
'payload'
}
let(:reference_name) {
'linux/x86/shell_reverse_tcp'
}
let(:payload) {
load_and_create_module(
ancestor_reference_names: ancestor_reference_names,
module_type: module_type,
reference_name: reference_name
)
}
subject(:encoded_payload) do
described_class.new(framework, pinst, {})
described_class.new(framework, payload, {})
end
it 'is an Msf::EncodedPayload' do
@ -28,7 +40,7 @@ describe Msf::EncodedPayload do
before { described_class.any_instance.stub(:generate) }
it 'returns an Msf::EncodedPayload instance' do
expect(described_class.create(pinst)).to be_a(described_class)
expect(described_class.create(payload)).to be_a(described_class)
end
end
@ -37,7 +49,13 @@ describe Msf::EncodedPayload do
describe '#arch' do
context 'when payload is linux/x86 reverse tcp' do
let(:payload) { 'linux/x86/shell_reverse_tcp' }
let(:ancestor_reference_names) {
%w{singles/linux/x86/shell_reverse_tcp}
}
let(:reference_name) {
'linux/x86/shell_reverse_tcp'
}
it 'returns ["X86"]' do
expect(encoded_payload.arch).to eq [ARCH_X86]
@ -45,7 +63,13 @@ describe Msf::EncodedPayload do
end
context 'when payload is linux/x64 reverse tcp' do
let(:payload) { 'linux/x64/shell_reverse_tcp' }
let(:ancestor_reference_names) {
%w{singles/linux/x64/shell_reverse_tcp}
}
let(:reference_name) {
'linux/x64/shell_reverse_tcp'
}
it 'returns ["X86_64"]' do
expect(encoded_payload.arch).to eq [ARCH_X86_64]

View File

@ -100,6 +100,8 @@ describe Msf::Modules::Loader::Base do
context 'NAMESPACE_MODULE_CONTENT' do
context 'derived module' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:namespace_module_names) do
['Msf', 'Modules', 'Mod617578696c696172792f72737065632f6d6f636b']
end
@ -278,6 +280,8 @@ describe Msf::Modules::Loader::Base do
end
context 'with file changed' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:module_full_name) do
File.join('auxiliary', module_reference_name)
end
@ -689,6 +693,8 @@ describe Msf::Modules::Loader::Base do
end
context '#create_namespace_module' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:namespace_module_names) do
[
'Msf',
@ -778,6 +784,8 @@ describe Msf::Modules::Loader::Base do
end
context '#current_module' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:module_names) do
[
'Msf',
@ -918,6 +926,8 @@ describe Msf::Modules::Loader::Base do
end
context '#namespace_module_transaction' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:relative_name) do
'Mod617578696c696172792f72737065632f6d6f636b'
end
@ -948,7 +958,8 @@ describe Msf::Modules::Loader::Base do
end
it 'should remove the pre-existing namespace module' do
Msf::Modules.should_receive(:remove_const).with(relative_name)
expect(Msf::Modules).to receive(:remove_const).with(relative_name.to_sym).and_call_original
expect(Msf::Modules).to receive(:remove_const).with(relative_name).and_call_original
subject.send(:namespace_module_transaction, module_full_name) do |namespace_module|
true
@ -1177,6 +1188,8 @@ describe Msf::Modules::Loader::Base do
end
context 'with namespace_module nil' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
#
# lets
#
@ -1194,7 +1207,7 @@ describe Msf::Modules::Loader::Base do
end
it 'should remove relative_name' do
parent_module.should_receive(:remove_const).with(relative_name)
expect(parent_module).to receive(:remove_const).with(relative_name).and_call_original
subject.send(:restore_namespace_module, parent_module, relative_name, namespace_module)
end
@ -1239,6 +1252,8 @@ describe Msf::Modules::Loader::Base do
end
context 'with the current constant being the namespace_module' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
it 'should not change the constant' do
parent_module.const_defined?(relative_name).should be_truthy
@ -1254,6 +1269,9 @@ describe Msf::Modules::Loader::Base do
end
it 'should not remove the constant and then set it' do
# Allow 'Metasploit::Framework::Spec::Constants cleaner' removal
expect(parent_module).to receive(:remove_const).with(relative_name.to_sym).and_call_original
parent_module.should_not_receive(:remove_const).with(relative_name)
parent_module.should_not_receive(:const_set).with(relative_name, @current_namespace_module)
@ -1262,9 +1280,13 @@ describe Msf::Modules::Loader::Base do
end
context 'without the current constant being the namespace_module' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
it 'should remove relative_name from parent_module' do
parent_module.const_defined?(relative_name).should be_truthy
parent_module.should_receive(:remove_const).with(relative_name)
expect(parent_module).to receive(:remove_const).with(relative_name).and_call_original
expect(parent_module).to receive(:remove_const).with(relative_name.to_sym).and_call_original
subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module)
end
@ -1280,6 +1302,8 @@ describe Msf::Modules::Loader::Base do
end
context 'without relative_name being a defined constant' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
it 'should set relative_name on parent_module to namespace_module' do
parent_module.const_defined?(relative_name).should be_falsey

View File

@ -27,6 +27,8 @@ describe Msf::Modules::Loader::Directory do
context '#load_module' do
context 'with existent module_path' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:framework) do
framework = double('Msf::Framework', :datastore => {})

View File

@ -2,12 +2,7 @@ require 'spec_helper'
require 'msf/core/payload_generator'
describe Msf::PayloadGenerator do
PAYLOAD_FRAMEWORK = Msf::Simple::Framework.create(
:module_types => [ ::Msf::MODULE_PAYLOAD, ::Msf::MODULE_ENCODER, ::Msf::MODULE_NOP],
'DisableDatabase' => true,
'DisableLogging' => true
)
include_context 'Msf::Simple::Framework#modules loading'
let(:lhost) { "192.168.172.1"}
let(:lport) { "8443" }
@ -15,13 +10,18 @@ describe Msf::PayloadGenerator do
let(:add_code) { false }
let(:arch) { "x86" }
let(:badchars) { "\x20\x0D\x0A" }
let(:encoder) { 'x86/shikata_ga_nai' }
let(:encoder_reference_name) {
# use encoder_module to ensure it is loaded prior to passing to generator
encoder_module.refname
}
let(:format) { "raw" }
let(:framework) { PAYLOAD_FRAMEWORK }
let(:iterations) { 1 }
let(:keep) { false }
let(:nops) { 0 }
let(:payload) { "windows/meterpreter/reverse_tcp"}
let(:payload_reference_name) {
# use payload_module to ensure it is loaded prior to passing to generator
payload_module.refname
}
let(:platform) { "Windows" }
let(:space) { 1073741824 }
let(:stdin) { nil }
@ -31,25 +31,41 @@ describe Msf::PayloadGenerator do
add_code: add_code,
arch: arch,
badchars: badchars,
encoder: encoder,
encoder: encoder_reference_name,
datastore: datastore,
format: format,
framework: framework,
iterations: iterations,
keep: keep,
nops: nops,
payload: payload,
payload: payload_reference_name,
platform: platform,
space: space,
stdin: stdin,
template: template
}
}
let(:payload_module) { framework.payloads.create(payload)}
let(:payload_module) {
load_and_create_module(
ancestor_reference_names: %w{
stagers/windows/reverse_tcp
stages/windows/meterpreter
},
module_type: 'payload',
reference_name: 'windows/meterpreter/reverse_tcp'
)
}
let(:shellcode) { "\x50\x51\x58\x59" }
let(:encoder_module) { framework.encoders.create('x86/shikata_ga_nai') }
let(:encoder_module) {
load_and_create_module(
module_type: 'encoder',
reference_name: 'x86/shikata_ga_nai'
)
}
subject(:payload_generator) { described_class.new(generator_opts) }
subject(:payload_generator) {
described_class.new(generator_opts)
}
it { should respond_to :add_code }
it { should respond_to :arch }
@ -77,13 +93,13 @@ describe Msf::PayloadGenerator do
add_code: add_code,
arch: arch,
badchars: badchars,
encoder: encoder,
encoder: encoder_reference_name,
datastore: datastore,
format: format,
iterations: iterations,
keep: keep,
nops: nops,
payload: payload,
payload: payload_reference_name,
platform: platform,
space: space,
stdin: stdin,
@ -95,19 +111,19 @@ describe Msf::PayloadGenerator do
end
context 'when not given a payload' do
let(:payload) { nil }
let(:payload_reference_name) { nil }
it { should raise_error(ArgumentError, "Invalid Payload Selected") }
end
context 'when given an invalid payload' do
let(:payload) { "beos/meterpreter/reverse_gopher" }
let(:payload_reference_name) { "beos/meterpreter/reverse_gopher" }
it { should raise_error(ArgumentError, "Invalid Payload Selected") }
end
context 'when given a payload through stdin' do
let(:payload) { "stdin" }
let(:payload_reference_name) { "stdin" }
it { should_not raise_error }
end
@ -230,7 +246,7 @@ describe Msf::PayloadGenerator do
context 'when passing a payload through stdin' do
let(:stdin) { "\x90\x90\x90"}
let(:payload) { "stdin" }
let(:payload_reference_name) { "stdin" }
context 'when no arch has been selected' do
let(:arch) { '' }
@ -330,6 +346,13 @@ describe Msf::PayloadGenerator do
end
context '#prepend_nops' do
before(:each) do
load_and_create_module(
module_type: 'nop',
reference_name: 'x86/opty2'
)
end
context 'when nops are set to 0' do
it 'returns the unmodified shellcode' do
expect(payload_generator.prepend_nops(shellcode)).to eq shellcode
@ -367,15 +390,41 @@ describe Msf::PayloadGenerator do
end
context 'when multiple encoders are selected' do
let(:encoder) { "x86/shikata_ga_nai,x86/alpha_mixed"}
#
# lets
#
let(:encoder_reference_name) {
encoder_reference_names.join(',')
}
let(:encoder_reference_names) {
%w{
x86/shikata_ga_nai
x86/alpha_mixed
}
}
#
# Callbacks
#
before(:each) do
encoder_reference_names.each do |reference_name|
load_and_create_module(
module_type: 'encoder',
reference_name: reference_name
)
end
end
it 'returns an array of the right size' do
expect(payload_generator.get_encoders.count).to eq 2
end
it 'returns each of the selected encoders in the array' do
payload_generator.get_encoders.each do |my_encoder|
expect(encoder_names).to include my_encoder.name
payload_generator.get_encoders.each do |msf_encoder|
expect(encoder_names).to include msf_encoder.name
end
end
@ -385,17 +434,17 @@ describe Msf::PayloadGenerator do
end
context 'when no encoder is selected but badchars are present' do
let(:encoder) { '' }
let(:encoder_reference_name) { '' }
it 'returns an array of all encoders with a compatible arch' do
payload_generator.get_encoders.each do |my_encoder|
expect(my_encoder.arch).to include arch
expect(encoder_module.arch).to include arch
end
end
end
context 'when no encoder or badchars are selected' do
let(:encoder) { '' }
let(:encoder_reference_name) { '' }
let(:badchars) { '' }
it 'returns an empty array' do
@ -407,9 +456,8 @@ describe Msf::PayloadGenerator do
context '#run_encoder' do
it 'should call the encoder a number of times equal to the iterations' do
my_encoder = encoder_module
my_encoder.should_receive(:encode).exactly(iterations).times.and_return(shellcode)
payload_generator.run_encoder(my_encoder, shellcode)
encoder_module.should_receive(:encode).exactly(iterations).times.and_return(shellcode)
payload_generator.run_encoder(encoder_module, shellcode)
end
context 'when the encoder makes a buffer too large' do
@ -454,12 +502,21 @@ describe Msf::PayloadGenerator do
let(:format) { 'war' }
context 'if the payload is a valid java payload' do
let(:payload) { "java/meterpreter/reverse_tcp"}
let(:payload_module) {
load_and_create_module(
ancestor_reference_names: %w{
stagers/java/reverse_tcp
stages/java/meterpreter
},
module_type: 'payload',
reference_name: 'java/meterpreter/reverse_tcp'
)
}
it 'calls the generate_war on the payload' do
java_payload = framework.payloads.create("java/meterpreter/reverse_tcp")
framework.stub_chain(:payloads, :keys).and_return ["java/meterpreter/reverse_tcp"]
framework.stub_chain(:payloads, :create).and_return(java_payload)
java_payload.should_receive(:generate_war).and_call_original
framework.stub_chain(:payloads, :keys).and_return [payload_reference_name]
framework.stub_chain(:payloads, :create).and_return(payload_module)
payload_module.should_receive(:generate_war).and_call_original
payload_generator.generate_java_payload
end
end
@ -473,23 +530,40 @@ describe Msf::PayloadGenerator do
let(:format) { 'raw' }
context 'if the payload responds to generate_jar' do
let(:payload) { "java/meterpreter/reverse_tcp"}
let(:payload_module) {
load_and_create_module(
ancestor_reference_names: %w{
stagers/java/reverse_tcp
stages/java/meterpreter
},
module_type: 'payload',
reference_name: 'java/meterpreter/reverse_tcp'
)
}
it 'calls the generate_jar on the payload' do
java_payload = framework.payloads.create("java/meterpreter/reverse_tcp")
framework.stub_chain(:payloads, :keys).and_return ["java/meterpreter/reverse_tcp"]
framework.stub_chain(:payloads, :create).and_return(java_payload)
java_payload.should_receive(:generate_jar).and_call_original
framework.stub_chain(:payloads, :keys).and_return [payload_reference_name]
framework.stub_chain(:payloads, :create).and_return(payload_module)
payload_module.should_receive(:generate_jar).and_call_original
payload_generator.generate_java_payload
end
end
context 'if the payload does not respond to generate_jar' do
let(:payload) { "java/jsp_shell_reverse_tcp"}
let(:payload_module) {
load_and_create_module(
ancestor_reference_names: %w{
singles/java/jsp_shell_reverse_tcp
},
module_type: 'payload',
reference_name: 'java/jsp_shell_reverse_tcp'
)
}
it 'calls #generate' do
java_payload = framework.payloads.create("java/jsp_shell_reverse_tcp")
framework.stub_chain(:payloads, :keys).and_return ["java/jsp_shell_reverse_tcp"]
framework.stub_chain(:payloads, :create).and_return(java_payload)
java_payload.should_receive(:generate).and_call_original
framework.stub_chain(:payloads, :keys).and_return [payload_reference_name]
framework.stub_chain(:payloads, :create).and_return(payload_module)
payload_module.should_receive(:generate).and_call_original
payload_generator.generate_java_payload
end
end
@ -510,22 +584,29 @@ describe Msf::PayloadGenerator do
context '#generate_payload' do
it 'calls each step of the process' do
my_generator = payload_generator
my_generator.should_receive(:generate_raw_payload).and_call_original
my_generator.should_receive(:add_shellcode).and_call_original
my_generator.should_receive(:encode_payload).and_call_original
my_generator.should_receive(:prepend_nops).and_call_original
my_generator.should_receive(:format_payload).and_call_original
my_generator.generate_payload
payload_generator.should_receive(:generate_raw_payload).and_call_original
payload_generator.should_receive(:add_shellcode).and_call_original
payload_generator.should_receive(:encode_payload).and_call_original
payload_generator.should_receive(:prepend_nops).and_call_original
payload_generator.should_receive(:format_payload).and_call_original
payload_generator.generate_payload
end
context 'when the payload is java' do
let(:payload) { "java/meterpreter/reverse_tcp" }
let(:payload_module) {
load_and_create_module(
ancestor_reference_names: %w{
stagers/java/reverse_tcp
stages/java/meterpreter
},
module_type: 'payload',
reference_name: 'java/meterpreter/reverse_tcp'
)
}
it 'calls generate_java_payload' do
my_generator = payload_generator
my_generator.should_receive(:generate_java_payload)
my_generator.generate_payload
payload_generator.should_receive(:generate_java_payload)
payload_generator.generate_payload
end
end
end

View File

@ -7,16 +7,12 @@ require 'spec_helper'
require 'support/shared/contexts/msf/util/exe'
describe Msf::Util::EXE do
include_context 'Msf::Simple::Framework#modules loading'
subject do
described_class
end
$framework = Msf::Simple::Framework.create(
:module_types => [ Msf::MODULE_NOP ],
'DisableDatabase' => true
)
describe '.win32_rwx_exec' do
it "should contain the shellcode" do
bin = subject.win32_rwx_exec("asdfjklASDFJKL")
@ -32,7 +28,7 @@ describe Msf::Util::EXE do
describe '.to_executable_fmt' do
it "should output nil when given a bogus format" do
bin = subject.to_executable_fmt($framework, "", "", "", "does not exist", {})
bin = subject.to_executable_fmt(framework, "", "", "", "does not exist", {})
bin.should == nil
end
@ -41,21 +37,34 @@ describe Msf::Util::EXE do
@platform_format_map.each do |plat, formats|
context "with platform=#{plat}" do
if plat == 'windows'
before(:each) do
load_and_create_module(
module_type: 'encoder',
reference_name: 'x86/shikata_ga_nai'
)
load_and_create_module(
module_type: 'nop',
reference_name: 'x86/opty2'
)
end
end
let(:platform) do
Msf::Module::PlatformList.transform(plat)
end
it "should output nil when given bogus format" do
bin = subject.to_executable_fmt($framework, formats.first[:arch], platform, "\xcc", "asdf", {})
bin = subject.to_executable_fmt(framework, formats.first[:arch], platform, "\xcc", "asdf", {})
bin.should == nil
end
it "should output nil when given bogus arch" do
bin = subject.to_executable_fmt($framework, "asdf", platform, "\xcc", formats.first[:format], {})
bin = subject.to_executable_fmt(framework, "asdf", platform, "\xcc", formats.first[:format], {})
bin.should == nil
end
[ ARCH_X86, ARCH_X64, ARCH_X86_64, ARCH_PPC, ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE ].each do |arch|
it "returns nil when given bogus format for arch=#{arch}" do
bin = subject.to_executable_fmt($framework, arch, platform, "\xcc", "asdf", {})
bin = subject.to_executable_fmt(framework, arch, platform, "\xcc", "asdf", {})
end
end
@ -69,7 +78,7 @@ describe Msf::Util::EXE do
end
it "returns an executable when given arch=#{arch}, fmt=#{fmt}" do
bin = subject.to_executable_fmt($framework, arch, platform, "\xcc", fmt, {})
bin = subject.to_executable_fmt(framework, arch, platform, "\xcc", fmt, {})
bin.should be_a String
verify_bin_fingerprint(format_hash, bin)

View File

@ -81,6 +81,8 @@ describe Msfcli do
# This one is slow because we're loading all modules
#
context ".dump_module_list" do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
it "it should dump a list of modules" do
tbl = ''
stdout = get_stdout {
@ -221,6 +223,7 @@ describe Msfcli do
end
context ".init_modules" do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
it "should inititalize an exploit module" do
args = 'exploit/windows/smb/psexec S'
@ -298,6 +301,8 @@ describe Msfcli do
end
context ".engage_mode" do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
it "should show me the summary of module auxiliary/scanner/http/http_version" do
args = 'auxiliary/scanner/http/http_version s'
stdout = get_stdout {

View File

@ -20,6 +20,8 @@ require 'rspec/rails/fixture_support'
require 'rspec/rails/matchers'
require 'rspec/rails/mocks'
require 'metasploit/framework/spec'
FILE_FIXTURES_PATH = File.expand_path(File.dirname(__FILE__)) + '/file_fixtures/'
# Load the shared examples from the following engines
@ -56,4 +58,5 @@ RSpec.configure do |config|
config.use_transactional_fixtures = true
end
Metasploit::Framework::Spec::Constants::Suite.configure!
Metasploit::Framework::Spec::Threads::Suite.configure!

View File

@ -0,0 +1,6 @@
# Use in a context to clean up the constants that are created by the module loader.
shared_context 'Metasploit::Framework::Spec::Constants cleaner' do
after(:each) do
Metasploit::Framework::Spec::Constants.clean
end
end

View File

@ -0,0 +1,185 @@
# Loads, creates, and cleans up modules.
#
# @example Load and create encoder
# include_context 'Msf::Simple::Framework#modules loading'
#
# let(:encoder) {
# load_and_create_module(
# module_type: 'encoder',
# reference_name: 'x86/shikata_ga_nai'
# )
# }
#
# @example Load and create staged payload
# include_context 'Msf::Simple::Framework#modules loading'
#
# let(:staged_payload) {
# load_and_create_module(
# ancestor_reference_names: %w{
# stagers/android/reverse_https
# stages/android/meterpreter
# },
# module_type: 'payload',
# reference_name: 'android/meterpreter/reverse_tcp'
# )
# }
#
shared_context 'Msf::Simple::Framework#modules loading' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
include_context 'Msf::Simple::Framework'
#
# Methods
#
# Derives ancestor reference names from `:reference_name`.
#
# @param options [Hash{Symbol => Array<String>,String}]
# @option options [Array<String>] :ancestor_reference_names Override derivation from `:reference_name` and uses these
# `:ancestor_reference_names` instead.
# @option options [String] :module_type the type of the module with `:reference_name`.
# @option options [String] :reference_name the name of the module under `:module_type` whose ancestor reference names
# to derive.
# @return [Array<String>] ancestor reference names.
# @raise [KeyError] if `:ancestor_reference_names` is not given when `:module_type` is `'payload'`.
# @raise [KeyError] unless `:module_type` is given.
# @raise [KeyError] unless `:reference_name` is given.
def derive_ancestor_reference_names(options={})
options.assert_valid_keys(:ancestor_reference_names, :module_type, :reference_name)
options.fetch(:ancestor_reference_names) {
module_type = options.fetch(:module_type)
if module_type == 'payload'
raise KeyError, ":ancestor_reference_names must be given when :module_type is 'payload'"
end
# non-payload's single ancestor has the same reference name as the created module.
reference_name = options.fetch(:reference_name)
[reference_name]
}
end
# The module loader that can load module ancestors from `modules_path`
#
# @param modules_path [String] path to `modules` directory from which to load ancestor reference names.
# @return [Msf::Modules::Loader::Base]
def loader_for_modules_path(modules_path)
loader = framework.modules.send(:loaders).find { |loader|
loader.loadable?(modules_path)
}
# Override load_error so that rspec will print it instead of going to framework log
def loader.load_error(module_path, error)
raise error
end
loader
end
# Expects to load `:ancestor_reference_name` of `:module_type` from `:modules_path`.
#
# @raise expectation failure if `:ancestor_reference_name` cannot be loaded
def expect_to_load_module_ancestor(options={})
options.assert_valid_keys(:ancestor_reference_name, :modules_path, :module_type)
ancestor_reference_name = options.fetch(:ancestor_reference_name)
modules_path = options.fetch(:modules_path)
module_type = options.fetch(:module_type)
loader = loader_for_modules_path(modules_path)
loaded = loader.load_module(modules_path, module_type, ancestor_reference_name)
expect(loaded).to eq(true), "#{module_type}/#{ancestor_reference_name} failed to load from #{modules_path}"
end
# Expects to laod `:ancestor_reference_names` of `:module_type` from `:modules_path`
#
# @param options [Hash{Symbol => Array<String>, String}]
# @option options [Array<String>] :ancestor_reference_names the reference names of the module ancestors of
# `:module_type` to load from `:modules_path`.
# @option options [String] :modules_path The path from which to load `:ancestor_reference_names`.
# @option options [Stirng] :module_type The type of `:ancestor_reference_names` to derive their full paths under
# `:modules_path`.
# @raise expectation failure if any `:ancestor_reference_names` cannot be loaded.
def expect_to_load_module_ancestors(options={})
options.assert_valid_keys(:ancestor_reference_names, :modules_path, :module_type)
ancestor_references_names = options.fetch(:ancestor_reference_names)
ancestor_references_names.each do |ancestor_reference_name|
expect_to_load_module_ancestor(
ancestor_reference_name: ancestor_reference_name,
modules_path: options[:modules_path],
module_type: options[:module_type]
)
end
end
# Loads module ancestors with `:module_type` and `:ancestor_reference_names` from `:module_path` and then creates
# module instance with `:module_type` and `:reference_name`.
#
# @param options [Hash{Symbol => Array<String>,<String>}]
# @option options [Array<String>] :ancestor_reference_names the reference names of the ancestor modules for the module
# to be created. Only staged payloads have two ancestors; all other modules, including single payloads, have one
# ancestor.
# @option options [String] :modules_path path to the `modules` directory from which to load
# `:ancestor_reference_names`.
# @option options [String] :module_type the type of module
# @option options [String] :modules_path (#modules_path) the 'modules' directory from which to load
# `:ancestor_reference_names`.
# @return [Msf::Module]
# @raise [KeyError] if `:ancestor_reference_names` is not given when `:module_type` is `'payload'`.
# @raise [KeyError] unless :module_type is given.
# @raise [KeyError] unless :reference_name is given.
def load_and_create_module(options={})
options.assert_valid_keys(:ancestor_reference_names, :modules_path, :module_type, :reference_name)
reference_name = options.fetch(:reference_name)
module_type = options.fetch(:module_type)
ancestor_reference_names = self.derive_ancestor_reference_names(
options.except(:modules_path)
)
modules_path = options.fetch(:modules_path) {
self.modules_path
}
expect_to_load_module_ancestors(
ancestor_reference_names: ancestor_reference_names,
modules_path: modules_path,
module_type: module_type
)
module_set = module_set_for_type(module_type)
module_instance = module_set.create(reference_name)
expect(module_instance).not_to(
be_nil,
"Could not create #{module_type}/#{reference_name} after loading #{ancestor_reference_names.sort.to_sentence}"
)
module_instance
end
# The module set for `module_type`.
#
# @param module_type [String] the module type creatable by the module set.
# @return [Msf::ModuleSet]
def module_set_for_type(module_type)
framework.modules.module_set(module_type)
end
#
# lets
#
# The default modules path for this `Rails.application`.
#
# @return [String]
let(:modules_path) {
Rails.application.paths['modules'].expanded.first
}
end

View File

@ -6,30 +6,13 @@ shared_examples_for 'all modules with module type can be instantiated' do |optio
modules_path = modules_pathname.to_path
type_directory = options.fetch(:type_directory)
include_context 'Msf::Simple::Framework'
include_context 'Msf::Simple::Framework#modules loading'
#
# lets
#
let(:loader) {
loader = framework.modules.send(:loaders).find { |loader|
loader.loadable?(modules_path)
}
# Override load_error so that rspec will print it instead of going to framework log
def loader.load_error(module_path, error)
raise error
end
loader
}
context module_type do
let(:module_set) {
framework.modules.module_set(module_type)
}
type_pathname = modules_pathname.join(type_directory)
module_extension = ".rb"
module_extension_regexp = /#{Regexp.escape(module_extension)}$/
@ -41,17 +24,11 @@ shared_examples_for 'all modules with module type can be instantiated' do |optio
context module_reference_name do
it 'can be instantiated' do
loaded = loader.load_module(modules_path, module_type, module_reference_name)
expect(loaded).to eq(true), "#{module_reference_name} failed to load from #{modules_path}"
module_instance = nil
expect {
module_instance = module_set.create(module_reference_name)
}.not_to raise_error
expect(module_instance).not_to be_nil, "Could not instantiate #{module_type}/#{module_reference_name}"
load_and_create_module(
module_type: module_type,
modules_path: modules_path,
reference_name: module_reference_name
)
end
end
end

View File

@ -710,6 +710,8 @@ shared_examples_for 'Msf::DBManager::ModuleCache' do
end
context '#update_module_details' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
def update_module_details
db_manager.update_module_details(module_instance)
end

View File

@ -21,6 +21,8 @@ shared_examples_for 'Msf::DBManager::Session' do
end
context 'with Msf::Session' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:exploit_datastore) do
Msf::ModuleDataStore.new(module_instance).tap do |datastore|
datastore['ParentModule'] = parent_module_fullname

View File

@ -7,6 +7,8 @@ shared_examples_for 'Msf::DBManager#update_all_module_details refresh' do
end
context 'with cached module in Msf::ModuleSet' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:module_set) do
framework.exploits
end

View File

@ -153,6 +153,8 @@ shared_examples_for 'Msf::ModuleManager::Cache' do
end
context 'with module info in cache' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
let(:module_info_by_path) do
{
'path/to/module' => {

View File

@ -2,6 +2,8 @@ shared_examples_for 'Msf::Simple::Framework::ModulePaths' do
it { should be_a Msf::Simple::Framework::ModulePaths }
context '#init_module_paths' do
include_context 'Metasploit::Framework::Spec::Constants cleaner'
def init_module_paths
framework.init_module_paths(options)
end

View File

@ -79,54 +79,32 @@ shared_examples_for 'payload can be instantiated' do |options|
module_type = 'payload'
include_context 'Msf::Simple::Framework'
include_context 'Msf::Simple::Framework#modules loading'
#
# lets
#
let(:loader) {
loader = framework.modules.send(:loaders).find { |loader|
loader.loadable?(modules_path)
}
# Override load_error so that rspec will print it instead of going to framework log
def loader.load_error(module_path, error)
raise error
end
loader
}
let(:module_set) {
framework.modules.module_set(module_type)
}
context reference_name do
ancestor_reference_names.each do |ancestor_reference_name|
it "can load '#{module_type}/#{ancestor_reference_name}'" do
@actual_ancestor_reference_name_set.add(ancestor_reference_name)
loaded = loader.load_module(modules_path, module_type, ancestor_reference_name)
expect(loaded).to eq(true), "#{ancestor_reference_name} failed to load from #{modules_path}"
expect_to_load_module_ancestor(
ancestor_reference_name: ancestor_reference_name,
module_type: module_type,
modules_path: modules_path
)
end
end
it 'can be instantiated' do
ancestor_reference_names.each do |ancestor_reference_name|
loaded = loader.load_module(modules_path, module_type, ancestor_reference_name)
expect(loaded).to eq(true), "#{ancestor_reference_name} failed to load from #{modules_path}"
end
module_instance = nil
expect {
module_instance = module_set.create(reference_name)
}.not_to raise_error
expect(module_instance).not_to be_nil, "Could not instantiate #{module_type}/#{reference_name}"
load_and_create_module(
ancestor_reference_names: ancestor_reference_names,
module_type: module_type,
modules_path: modules_path,
reference_name: reference_name
)
end
end
end