From e5241b412a3b1b4f0e1f19c1bd8efda90ea391e9 Mon Sep 17 00:00:00 2001 From: Alan Foster Date: Fri, 25 Jun 2021 11:46:44 +0100 Subject: [PATCH] Add tests for aux and exploit cmd_check and cmd_run --- .gitignore | 5 +- .../console/command_dispatcher/auxiliary.rb | 2 +- .../singles/generic/no_session_payload.rb | 38 + .../command_dispatcher/auxiliary_spec.rb | 725 +++++++++++++++++- .../command_dispatcher/exploit_spec.rb | 438 ++++++++++- .../contexts/msf/framework/threads/cleaner.rb | 8 +- spec/support/shared/contexts/msf/ui_driver.rb | 63 +- .../support/shared/contexts/rex/job/inline.rb | 17 + 8 files changed, 1251 insertions(+), 45 deletions(-) create mode 100644 spec/file_fixtures/modules/payloads/singles/generic/no_session_payload.rb create mode 100644 spec/support/shared/contexts/rex/job/inline.rb diff --git a/.gitignore b/.gitignore index d2ff8ecf3e..f146c4244b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ Gemfile.local.lock config/database.yml # target config file for testing features/support/targets.yml +# Generated test files +spec/dummy # simplecov coverage data coverage doc/ @@ -94,6 +96,5 @@ docker-compose.local* *.pyc rspec.failures - #Ignore any base disk store files -db/modules_metadata_base.pstore \ No newline at end of file +db/modules_metadata_base.pstore diff --git a/lib/msf/ui/console/command_dispatcher/auxiliary.rb b/lib/msf/ui/console/command_dispatcher/auxiliary.rb index 876824242f..977f3f832e 100644 --- a/lib/msf/ui/console/command_dispatcher/auxiliary.rb +++ b/lib/msf/ui/console/command_dispatcher/auxiliary.rb @@ -51,7 +51,7 @@ class Auxiliary jobify = true end - rhosts = datastore['RHOSTS'] + rhosts = mod.datastore['RHOSTS'] begin # Check if this is a scanner module or doesn't target remote hosts if rhosts.blank? || mod.class.included_modules.include?(Msf::Auxiliary::Scanner) diff --git a/spec/file_fixtures/modules/payloads/singles/generic/no_session_payload.rb b/spec/file_fixtures/modules/payloads/singles/generic/no_session_payload.rb new file mode 100644 index 0000000000..4e1ef427bd --- /dev/null +++ b/spec/file_fixtures/modules/payloads/singles/generic/no_session_payload.rb @@ -0,0 +1,38 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + + CachedSize = 12 + + include Msf::Payload::Single + include Msf::Sessions::CommandShellOptions + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'mock payload which gives no session', + 'Description' => 'mock payload which gives no session', + 'Author' => ['unknown'], + 'License' => MSF_LICENSE, + 'Platform' => ['unix'], + 'Arch' => ARCH_CMD, + # 'Handler' => Msf::Handler::ReverseTcp, + 'Session' => Msf::Sessions::CommandShell, + 'PayloadType' => 'cmd', + 'Payload' => { 'Offsets' => {}, 'Payload' => '' } + ) + ) + end + + def wait_for_session(_t = wfs_delay) + # noop + end + + def generate + 'mock payload' + end +end diff --git a/spec/lib/msf/ui/console/command_dispatcher/auxiliary_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/auxiliary_spec.rb index e032e8cc94..daa7c09eb4 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/auxiliary_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/auxiliary_spec.rb @@ -1,23 +1,734 @@ require 'spec_helper' - RSpec.describe Msf::Ui::Console::CommandDispatcher::Auxiliary do include_context 'Msf::DBManager' include_context 'Msf::UIDriver' + include_context 'Rex::Job#start run inline' + include_context 'Msf::Framework#threads cleaner', verify_cleanup_required: false - subject(:aux) do - described_class.new(driver) + let(:aux_mod) do + mod_klass = Class.new(Msf::Auxiliary) do + def initialize + super( + 'Name' => 'mock module', + 'Description' => 'mock module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE + ) + + register_options( + [ + Msf::Opt::RHOSTS, + Msf::Opt::RPORT(3000), + Msf::OptFloat.new('FloatValue', [false, 'A FloatValue which should be normalized before framework runs this module', 3.5]) + ] + ) + end + + def check + print_status("Checking for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def run + print_status("Running for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def cleanup + print_status("Cleanup for target #{datastore['RHOSTS']}:#{datastore['RPORT']}") + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(framework) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + Msf::Simple::Framework.simplify_module(mod, false) + mod end - describe "#cmd_run" do + let(:smb_scanner_run_host_mod) do + mod_klass = Class.new(Msf::Auxiliary) do + include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::SMB::Client + + # Scanner mixin should be near last + include Msf::Auxiliary::Scanner + include Msf::Auxiliary::Report + + def initialize + super( + 'Name' => 'mock smb module', + 'Description' => 'mock smb module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE + ) + + register_options( + [ + Msf::Opt::RPORT(445), + Msf::OptFloat.new('FloatValue', [false, 'A FloatValue which should be normalized before framework runs this module', 3.5]) + ] + ) + end + + def check_host(_ip) + print_status("Checking for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def run_host(_ip) + print_status("Running for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def cleanup + print_status("Cleanup for target #{datastore['RHOSTS']}:#{datastore['RPORT']}") + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(framework) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + Msf::Simple::Framework.simplify_module(mod, false) + mod end - describe "#cmd_rerun" do + let(:smb_scanner_run_batch_mod) do + mod_klass = Class.new(Msf::Auxiliary) do + include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::SMB::Client + + # Scanner mixin should be near last + include Msf::Auxiliary::Scanner + include Msf::Auxiliary::Report + + def initialize + super( + 'Name' => 'mock smb module', + 'Description' => 'mock smb module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE + ) + + register_options( + [ + Msf::Opt::RPORT(445), + Msf::OptFloat.new('FloatValue', [false, 'A FloatValue which should be normalized before framework runs this module', 3.5]) + ] + ) + end + + def check_host(_ip) + print_status("Checking for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def run_batch(batch) + print_status("Running batch #{batch.inspect}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def run_batch_size + 2 + end + + def cleanup + print_status("Cleanup for target #{datastore['RHOSTS']}:#{datastore['RPORT']}") + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(framework) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + Msf::Simple::Framework.simplify_module(mod, false) + mod end - describe "#cmd_exploit" do + subject do + instance = described_class.new(driver) + instance end - describe "#cmd_reload" do + before(:each) do + run_rex_jobs_inline! + allow(driver).to receive(:input).and_return(driver_input) + allow(driver).to receive(:output).and_return(driver_output) + current_mod.init_ui(driver_input, driver_output) + allow(subject).to receive(:mod).and_return(current_mod) + end + + describe '#cmd_check' do + context 'when running a run_host scanner module' do + let(:current_mod) { smb_scanner_run_host_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + + subject.cmd_check + expected_output = [ + 'Check failed: Msf::OptionValidateError One or more options failed to validate: RHOSTS.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs a single RHOST value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_check + expected_output = [ + '192.0.2.1:445 - Checking for target 192.0.2.1:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Check failed: The state could not be determined.', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_check + expected_output = [ + '192.0.2.1:445 - Checking for target 192.0.2.1:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Check failed: The state could not be determined.', + 'Checked 1 of 2 hosts (050% complete)', + '192.0.2.2:445 - Checking for target 192.0.2.2:445 with normalized datastore value 3.5', + '192.0.2.2:445 - Cleanup for target 192.0.2.2:445', + '192.0.2.2:445 - Check failed: The state could not be determined.', + 'Checked 2 of 2 hosts (100% complete)', + 'Cleanup for target 192.0.2.1 192.0.2.2:445' + ] + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check + expected_output = [ + '192.0.2.1:445 - Checking for target 192.0.2.1:445 with normalized datastore value 5.0', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Check failed: The state could not be determined.', + 'Checked 1 of 2 hosts (050% complete)', + '192.0.2.2:445 - Checking for target 192.0.2.2:445 with normalized datastore value 5.0', + '192.0.2.2:445 - Cleanup for target 192.0.2.2:445', + '192.0.2.2:445 - Check failed: The state could not be determined.', + 'Checked 2 of 2 hosts (100% complete)', + 'Cleanup for target 192.0.2.1 192.0.2.2:445' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + pending("cmd_check doesn't support inline methods, only cmd_run") + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + '192.0.2.5:445 - Checking for target 192.0.2.5:445 with normalized datastore value 10.0', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Check failed: The state could not be determined.', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple RHOST inline options' do + pending("cmd_check doesn't support inline methods, only cmd_run") + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + '192.0.2.5:445 - Checking for target 192.0.2.5:445 with normalized datastore value 10.0', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Check failed: The state could not be determined.', + 'Checked 1 of 2 hosts (050% complete)', + '192.0.2.6:445 - Checking for target 192.0.2.6:445 with normalized datastore value 10.0', + '192.0.2.6:445 - Cleanup for target 192.0.2.6:445', + '192.0.2.6:445 - Check failed: The state could not be determined.', + 'Checked 2 of 2 hosts (100% complete)', + 'Cleanup for target 192.0.2.5 192.0.2.6:445' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports targeting a single host as an inline argument' do + subject.cmd_check('192.0.2.5') + expected_output = [ + '192.0.2.5:445 - Checking for target 192.0.2.5:445 with normalized datastore value 3.5', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Check failed: The state could not be determined.', + 'Cleanup for target :445' + ] + expect(@combined_output).to match_array(expected_output) + end + + it 'supports targeting multiple hosts as an inline argument' do + subject.cmd_check('192.0.2.5 192.0.2.6') + + expected_output = [ + '192.0.2.5:445 - Checking for target 192.0.2.5:445 with normalized datastore value 3.5', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Check failed: The state could not be determined.', + 'Checked 1 of 2 hosts (050% complete)', + '192.0.2.6:445 - Checking for target 192.0.2.6:445 with normalized datastore value 3.5', + '192.0.2.6:445 - Cleanup for target 192.0.2.6:445', + '192.0.2.6:445 - Check failed: The state could not be determined.', + 'Checked 2 of 2 hosts (100% complete)', + 'Cleanup for target :445' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'incorrectly handles unknown flags, and inadvertently run the exploit with the old rhosts value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_check('-unknown-flag') + expected_output = [ + '192.0.2.1:445 - Checking for target 192.0.2.1:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Check failed: The state could not be determined.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + + context 'when running an auxiliary module' do + let(:current_mod) { aux_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + subject.cmd_check + expected_output = [ + 'Check failed: Msf::OptionValidateError One or more options failed to validate: RHOSTS.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs a single RHOST value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_check + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + '192.0.2.1:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.1:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_check + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + '192.0.2.1:3000 - Check failed: The state could not be determined.', + 'Checking for target 192.0.2.2:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.2:3000', + '192.0.2.2:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.1 192.0.2.2:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.1:3000', + '192.0.2.1:3000 - Check failed: The state could not be determined.', + 'Checking for target 192.0.2.2:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.2:3000', + '192.0.2.2:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.1 192.0.2.2:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + pending('cmd_check does not support inline values yet') + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + 'Checking for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + '192.0.2.5:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.5:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple inlined RHOST values' do + pending('pending as inline module options are evaluated too late, and the module is therefore treated as a scanner') + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + 'Checking for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + '192.0.2.5:3000 - Check failed: The state could not be determined.', + 'Checking for target 192.0.2.6:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.6:3000', + '192.0.2.6:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.5 192.0.2.6:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'ignores the -j flag, and the module is not run as a job' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_check('-j') + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + '192.0.2.1:3000 - Check failed: The state could not be determined.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + end + + describe '#cmd_run' do + context 'when running a scanner run_host module' do + let(:current_mod) { smb_scanner_run_host_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + + subject.cmd_run + expected_output = [ + 'Auxiliary failed: Msf::OptionValidateError One or more options failed to validate: RHOSTS.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs a single RHOST value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run + expected_output = [ + '192.0.2.1:445 - Running for target 192.0.2.1:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Scanned 1 of 1 hosts (100% complete)', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_run + expected_output = [ + '192.0.2.1:445 - Running for target 192.0.2.1:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Scanned 1 of 2 hosts (50% complete)', + '192.0.2.2:445 - Running for target 192.0.2.2:445 with normalized datastore value 3.5', + '192.0.2.2:445 - Cleanup for target 192.0.2.2:445', + '192.0.2.2:445 - Scanned 2 of 2 hosts (100% complete)', + '192.0.2.2:445 - Cleanup for target 192.0.2.2:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run + expected_output = [ + '192.0.2.1:445 - Running for target 192.0.2.1:445 with normalized datastore value 5.0', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Scanned 1 of 2 hosts (50% complete)', + '192.0.2.2:445 - Running for target 192.0.2.2:445 with normalized datastore value 5.0', + '192.0.2.2:445 - Cleanup for target 192.0.2.2:445', + '192.0.2.2:445 - Scanned 2 of 2 hosts (100% complete)', + '192.0.2.2:445 - Cleanup for target 192.0.2.2:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + '192.0.2.5:445 - Running for target 192.0.2.5:445 with normalized datastore value 10.0', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Scanned 1 of 1 hosts (100% complete)', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple RHOST inline options' do + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + '192.0.2.5:445 - Running for target 192.0.2.5:445 with normalized datastore value 10.0', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Scanned 1 of 2 hosts (50% complete)', + '192.0.2.6:445 - Running for target 192.0.2.6:445 with normalized datastore value 10.0', + '192.0.2.6:445 - Cleanup for target 192.0.2.6:445', + '192.0.2.6:445 - Scanned 2 of 2 hosts (100% complete)', + '192.0.2.6:445 - Cleanup for target 192.0.2.6:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs the scanner as a background job when the -j flag is used' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run('-j') + expected_output = [ + '192.0.2.1:445 - Running rex job 0 inline', + '192.0.2.1:445 - Running for target 192.0.2.1:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Scanned 1 of 1 hosts (100% complete)', + 'Auxiliary module running as background job 0.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + + context 'when running a scanner run_batch module' do + let(:current_mod) { smb_scanner_run_batch_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + + subject.cmd_run + expected_output = [ + 'Auxiliary failed: Msf::OptionValidateError One or more options failed to validate: RHOSTS.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs a single RHOST value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run + expected_output = [ + '192.0.2.1:445 - Running batch ["192.0.2.1"]:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Scanned 1 of 1 hosts (100% complete)', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_run + expected_output = [ + 'Running batch ["192.0.2.1", "192.0.2.2"]:445 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1 192.0.2.2:445', + 'Scanned 2 of 2 hosts (100% complete)', + 'Cleanup for target 192.0.2.1 192.0.2.2:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run + expected_output = [ + 'Running batch ["192.0.2.1", "192.0.2.2"]:445 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.1 192.0.2.2:445', + 'Scanned 2 of 2 hosts (100% complete)', + 'Cleanup for target 192.0.2.1 192.0.2.2:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + '192.0.2.5:445 - Running batch ["192.0.2.5"]:445 with normalized datastore value 10.0', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + '192.0.2.5:445 - Scanned 1 of 1 hosts (100% complete)', + '192.0.2.5:445 - Cleanup for target 192.0.2.5:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple RHOST inline options' do + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + 'Running batch ["192.0.2.5", "192.0.2.6"]:445 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5 192.0.2.6:445', + 'Scanned 2 of 2 hosts (100% complete)', + 'Cleanup for target 192.0.2.5 192.0.2.6:445', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs the scanner as a background job when the -j flag is used' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run('-j') + expected_output = [ + '192.0.2.1:445 - Running rex job 0 inline', + '192.0.2.1:445 - Running batch ["192.0.2.1"]:445 with normalized datastore value 3.5', + '192.0.2.1:445 - Cleanup for target 192.0.2.1:445', + '192.0.2.1:445 - Scanned 1 of 1 hosts (100% complete)', + 'Auxiliary module running as background job 0.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + + context 'when running an auxiliary module' do + let(:current_mod) { aux_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + subject.cmd_run + expected_output = [ + 'Auxiliary failed: Msf::OptionValidateError One or more options failed to validate: RHOSTS.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs a single RHOST value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run + expected_output = [ + 'Running module against 192.0.2.1', + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_run + expected_output = [ + 'Running module against 192.0.2.1', + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + 'Running module against 192.0.2.2', + 'Running for target 192.0.2.2:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.2:3000', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run + expected_output = [ + 'Running module against 192.0.2.1', + 'Running for target 192.0.2.1:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.1:3000', + 'Running module against 192.0.2.2', + 'Running for target 192.0.2.2:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.2:3000', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + 'Running for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple inlined RHOST values' do + pending('fails as inline module options are evaluated too late, and the module is therefore treated as a scanner') + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + 'Running module against 192.0.2.5', + 'Running for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + 'Running module against 192.0.2.6', + 'Running for target 192.0.2.6:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.6:3000', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'ignores the -j flag, and the module is not run as a job' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run('-j') + expected_output = [ + 'Running module against 192.0.2.1', + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + 'Auxiliary module execution completed' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + end + + describe '#cmd_rerun' do + end + + describe '#cmd_exploit' do + end + + describe '#cmd_reload' do end end diff --git a/spec/lib/msf/ui/console/command_dispatcher/exploit_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/exploit_spec.rb index 369d2b4e44..50efbebd6c 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/exploit_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/exploit_spec.rb @@ -1,30 +1,450 @@ require 'spec_helper' - RSpec.describe Msf::Ui::Console::CommandDispatcher::Exploit do include_context 'Msf::DBManager' include_context 'Msf::UIDriver' + include_context 'Rex::Job#start run inline' + include_context 'Msf::Framework#threads cleaner', verify_cleanup_required: false - subject(:exp) do - described_class.new(driver) + let(:remote_exploit_mod) do + mod_klass = Class.new(Msf::Exploit) do + def initialize + super( + 'Name' => 'mock module', + 'Description' => 'mock module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE, + 'Arch' => ARCH_CMD, + 'Platform' => ['unix'], + 'Targets' => [['Automatic', {}]], + 'DefaultTarget' => 0, + ) + + register_options( + [ + Msf::Opt::RHOSTS, + Msf::Opt::RPORT(3000), + Msf::OptFloat.new('FloatValue', [false, 'A FloatValue which should be normalized before framework runs this module', 3.5]) + ] + ) + end + + def check + print_status("Checking for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + def run + print_status("Running for target #{datastore['RHOSTS']}:#{datastore['RPORT']} with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + alias_method :exploit, :run + + def cleanup + print_status("Cleanup for target #{datastore['RHOSTS']}:#{datastore['RPORT']}") + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(framework) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + Msf::Simple::Framework.simplify_module(mod, false) + mod end - describe "#cmd_exploit" do + let(:non_remote_exploit_mod) do + mod_klass = Class.new(Msf::Exploit) do + def initialize + super( + 'Name' => 'mock module', + 'Description' => 'mock module', + 'Author' => ['Unknown'], + 'License' => MSF_LICENSE, + 'Arch' => ARCH_CMD, + 'Platform' => ['unix'], + 'Targets' => [['Automatic', {}]], + 'DefaultTarget' => 0, + ) + + register_options( + [ + Msf::OptFloat.new('FloatValue', [false, 'A FloatValue which should be normalized before framework runs this module', 3.5]) + ] + ) + end + + def run + print_status("Running with normalized datastore value #{datastore['FloatValue'].inspect}") + end + + alias_method :exploit, :run + + def cleanup + print_status('Cleanup') + end + end + + mod = mod_klass.new + datastore = Msf::ModuleDataStore.new(mod) + allow(mod).to receive(:framework).and_return(framework) + allow(mod).to receive(:datastore).and_return(datastore) + datastore.import_options(mod.options) + Msf::Simple::Framework.simplify_module(mod, false) + mod end - describe "#cmd_rcheck" do + subject do + instance = described_class.new(driver) + instance end - describe "#cmd_rexploit" do + def set_default_payload(mod) + mod.datastore['PAYLOAD'] = 'generic/no_session_payload' + mod.datastore['LHOST'] = '127.0.0.1' end - describe "#cmd_reload" do + before do + run_rex_jobs_inline! + + allow(driver).to receive(:input).and_return(driver_input) + allow(driver).to receive(:output).and_return(driver_output) + current_mod.init_ui(driver_input, driver_output) + allow(subject).to receive(:mod).and_return(current_mod) + + framework.modules.add_module_path(File.join(FILE_FIXTURES_PATH, 'modules')) end - describe "#cmd_run" do + describe '#cmd_check' do + context 'when checking a remote exploit module' do + let(:current_mod) { remote_exploit_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + subject.cmd_check + expected_output = [ + 'Check failed: Msf::OptionValidateError One or more options failed to validate: RHOSTS.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs a single RHOST value' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_check + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + '192.0.2.1:3000 - Check failed: The state could not be determined.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_check + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 3.5', + '192.0.2.1:3000 - Check failed: The state could not be determined.', + 'Checking for target 192.0.2.2:3000 with normalized datastore value 3.5', + '192.0.2.2:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.1 192.0.2.2:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 5.0', + '192.0.2.1:3000 - Check failed: The state could not be determined.', + 'Checking for target 192.0.2.2:3000 with normalized datastore value 5.0', + '192.0.2.2:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.1 192.0.2.2:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + pending('cmd_check does not support inline values yet') + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + 'Checking for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + '192.0.2.5:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.5:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple inlined RHOST values' do + pending('pending as inline module options are evaluated too late, and the module is therefore treated as a scanner') + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_check('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + 'Checking for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + '192.0.2.5:3000 - Check failed: The state could not be determined.', + 'Checking for target 192.0.2.6:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.6:3000', + '192.0.2.6:3000 - Check failed: The state could not be determined.', + 'Cleanup for target 192.0.2.5 192.0.2.6:3000' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'ignores the -j flag, and the module is not run as a job' do + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_check('-j') + expected_output = [ + 'Checking for target 192.0.2.1:3000 with normalized datastore value 3.5', + '192.0.2.1:3000 - Check failed: The state could not be determined.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + + context 'when checking a non remote exploit module' do + let(:current_mod) { non_remote_exploit_mod } + + it 'notifies the user that this module does not support check' do + subject.cmd_check + expected_output = [ + 'Check failed: NoMethodError This module does not support check.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end end - describe "#cmd_rerun" do + describe '#cmd_run' do + before do + set_default_payload(current_mod) + end + + context 'when running a remote exploit module' do + let(:current_mod) { remote_exploit_mod } + + it 'reports missing RHOST values' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = nil + subject.cmd_run + expected_output = [ + 'Exploit failed: One or more options failed to validate: RHOSTS.', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'attempts to run modules with blank RHOSTS' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['RHOSTS'] = '' + subject.cmd_run + expected_output = [ + 'Exploit failed: One or more options failed to validate: RHOSTS.', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'reports a missing payload value' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['PAYLOAD'] = nil + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run + expected_output = [ + 'Exploit failed: A payload has not been selected.', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs a single RHOST value' do + set_default_payload(current_mod) + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run + expected_output = [ + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'runs multiple RHOST values' do + set_default_payload(current_mod) + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_run + expected_output = [ + 'Exploiting target {:address=>"192.0.2.1", :hostname=>nil}', + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.1:3000', + 'Exploiting target {:address=>"192.0.2.2", :hostname=>nil}', + 'Running for target 192.0.2.2:3000 with normalized datastore value 3.5', + 'Cleanup for target 192.0.2.2:3000', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalizes the datastore before running' do + set_default_payload(current_mod) + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run + expected_output = [ + 'Exploiting target {:address=>"192.0.2.1", :hostname=>nil}', + 'Running for target 192.0.2.1:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.1:3000', + 'Exploiting target {:address=>"192.0.2.2", :hostname=>nil}', + 'Running for target 192.0.2.2:3000 with normalized datastore value 5.0', + 'Cleanup for target 192.0.2.2:3000', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + set_default_payload(current_mod) + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5', 'FloatValue=10.0') + expected_output = [ + 'Running for target 192.0.2.5:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5:3000', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports multiple inlined RHOST values' do + set_default_payload(current_mod) + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run('RHOSTS=192.0.2.5 192.0.2.6', 'FloatValue=10.0') + expected_output = [ + 'Running for target 192.0.2.5 192.0.2.6:3000 with normalized datastore value 10.0', + 'Cleanup for target 192.0.2.5 192.0.2.6:3000', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'honors the -j flag, and the module is run as a job' do + set_default_payload(current_mod) + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run('-j') + expected_output = [ + 'Running rex job 0 inline', + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Exploit running as background job 0.', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'honors the -j flag, and the module is run as a job when there are multiple hosts' do + set_default_payload(current_mod) + current_mod.datastore['RHOSTS'] = '192.0.2.1 192.0.2.2' + subject.cmd_run('-j') + expected_output = [ + 'Exploiting target {:address=>"192.0.2.1", :hostname=>nil}', + 'Running rex job 0 inline', + 'Running for target 192.0.2.1:3000 with normalized datastore value 3.5', + 'Exploiting target {:address=>"192.0.2.2", :hostname=>nil}', + 'Running rex job 1 inline', + 'Running for target 192.0.2.2:3000 with normalized datastore value 3.5', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end + + context 'when running a non remote exploit module' do + let(:current_mod) { non_remote_exploit_mod } + + it 'reports a missing payload value' do + allow(current_mod).to receive(:run).and_call_original + current_mod.datastore['PAYLOAD'] = nil + current_mod.datastore['RHOSTS'] = '192.0.2.1' + subject.cmd_run + expected_output = [ + 'Exploit failed: A payload has not been selected.', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + expect(subject.mod).not_to have_received(:run) + end + + it 'runs when a payload is set' do + set_default_payload(current_mod) + subject.cmd_run + expected_output = [ + 'Running with normalized datastore value 3.5', + 'Cleanup', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'normalized the datastore before running' do + set_default_payload(current_mod) + current_mod.datastore.store('FloatValue', '5.0') + subject.cmd_run + expected_output = [ + 'Running with normalized datastore value 5.0', + 'Cleanup', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + + it 'supports inline options' do + set_default_payload(current_mod) + subject.cmd_run('FloatValue=10.0') + expected_output = [ + 'Running with normalized datastore value 10.0', + 'Cleanup', + 'Exploit completed, but no session was created.' + ] + + expect(@combined_output).to match_array(expected_output) + end + end end + describe '#cmd_rerun' do + end + + describe '#cmd_exploit' do + end + + describe '#cmd_reload' do + end end diff --git a/spec/support/shared/contexts/msf/framework/threads/cleaner.rb b/spec/support/shared/contexts/msf/framework/threads/cleaner.rb index 707bbc8d96..758fd0988b 100644 --- a/spec/support/shared/contexts/msf/framework/threads/cleaner.rb +++ b/spec/support/shared/contexts/msf/framework/threads/cleaner.rb @@ -1,10 +1,10 @@ -RSpec.shared_context 'Msf::Framework#threads cleaner' do +RSpec.shared_context 'Msf::Framework#threads cleaner' do |options = {}| after(:example) do |example| - unless framework.threads? + if options.fetch(:verify_cleanup_required, true) && !framework.threads? fail RuntimeError.new( "framework.threads was never initialized. There are no threads to clean up. " \ "Remove `include_context Msf::Framework#threads cleaner` from context around " \ - "'#{example.metadata.full_description}'" + "'#{example.metadata[:full_description]}'" ) end @@ -21,4 +21,4 @@ RSpec.shared_context 'Msf::Framework#threads cleaner' do # ensure killed thread is cleaned up by VM thread_manager.monitor.join end -end \ No newline at end of file +end diff --git a/spec/support/shared/contexts/msf/ui_driver.rb b/spec/support/shared/contexts/msf/ui_driver.rb index 23e183ac55..5afb06d697 100644 --- a/spec/support/shared/contexts/msf/ui_driver.rb +++ b/spec/support/shared/contexts/msf/ui_driver.rb @@ -1,26 +1,45 @@ RSpec.shared_context 'Msf::UIDriver' do let(:driver) do - double( - 'Driver', - :framework => framework - ).tap { |driver| - allow(driver).to receive(:on_command_proc=).with(kind_of(Proc)) - allow(driver).to receive(:print_line).with(kind_of(String)) do |string| - @output ||= [] - @output.concat string.split("\n") - end - allow(driver).to receive(:print_status).with(kind_of(String)) do |string| - @output ||= [] - @output.concat string.split("\n") - end - allow(driver).to receive(:print_error).with(kind_of(String)) do |string| - @error ||= [] - @error.concat string.split("\n") - end - allow(driver).to receive(:print_bad).with(kind_of(String)) do |string| - @error ||= [] - @error.concat string.split("\n") - end - } + instance = double('Driver', framework: framework) + allow(instance).to receive(:on_command_proc=).with(kind_of(Proc)) + capture_logging(instance) + instance + end + + let(:driver_input) do + double(Rex::Ui::Text::Input) + end + + let(:driver_output) do + instance = double( + Rex::Ui::Text::Output, + prompting?: false + ) + + capture_logging(instance) + instance + end + + def capture_logging(target) + append_output = proc do |string| + lines = string.split("\n") + @output ||= [] + @output.concat(lines) + @combined_output ||= [] + @combined_output.concat(lines) + end + append_error = proc do |string| + lines = string.split("\n") + @error ||= [] + @error.concat(lines) + @combined_output ||= [] + @combined_output.concat(lines) + end + + allow(target).to receive(:print_line).with(kind_of(String), &append_output) + allow(target).to receive(:print_status).with(kind_of(String), &append_output) + allow(target).to receive(:print_warning).with(kind_of(String), &append_error) + allow(target).to receive(:print_error).with(kind_of(String), &append_error) + allow(target).to receive(:print_bad).with(kind_of(String), &append_error) end end diff --git a/spec/support/shared/contexts/rex/job/inline.rb b/spec/support/shared/contexts/rex/job/inline.rb new file mode 100644 index 0000000000..a868a49e76 --- /dev/null +++ b/spec/support/shared/contexts/rex/job/inline.rb @@ -0,0 +1,17 @@ +RSpec.shared_context 'Rex::Job#start run inline' do + # Intercepts calls to Rex::Job objects, and ensures that async rex jobs are immediately run inline instead of having + # their execution deferred until later. This ensures that Jobs deterministically complete during a test run. + def run_rex_jobs_inline! + allow_any_instance_of(Rex::Job).to receive(:start).and_wrap_original do |original_method, original_async_value| + original_receiver = original_method.receiver + ctx = original_receiver.ctx + if ctx.first.is_a?(Msf::Module) + mod = ctx.first + mod.print_status("Running rex job #{original_receiver.jid} inline") + end + expect(original_async_value).to be(true) + new_async_value = false + original_method.call(new_async_value) + end + end +end