diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index 7c914e744c52..f83a84cbe65a 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -30,7 +30,7 @@ class CommandShell include Rex::Ui::Text::Resource @@irb_opts = Rex::Parser::Arguments.new( - '-h' => [false, 'Help menu.' ], + ['-h', '--help'] => [false, 'Help menu.' ], '-e' => [true, 'Expression to evaluate.'] ) @@ -235,35 +235,35 @@ def cmd_sessions_help end def cmd_sessions(*args) - if args.length.zero? || args[0].to_i <= 0 - # No args + if args.length != 1 + print_status "Wrong number of arguments expected: 1, received: #{args.length}" return cmd_sessions_help end - if args.length == 1 && (args[1] == '-h' || args[1] == 'help') - # One arg, and args[1] => '-h' '-H' 'help' + if args[0] == '-h' || args[0] == '--help' return cmd_sessions_help end - if args.length != 1 - # More than one argument + session_id = args[0].to_i + if session_id <= 0 + print_status 'Invalid session id' return cmd_sessions_help end - if args[0].to_s == self.name.to_s + if session_id == self.sid # Src == Dst print_status("Session #{self.name} is already interactive.") else print_status("Backgrounding session #{self.name}...") # store the next session id so that it can be referenced as soon # as this session is no longer interacting - self.next_session = args[0] + self.next_session = session_id self.interacting = false end end def cmd_resource(*args) - if args.empty? + if args.empty? || args[0] == '-h' || args[0] == '--help' cmd_resource_help return false end @@ -320,9 +320,9 @@ def cmd_shell_help() end def cmd_shell(*args) - if args.length == 1 && (args[1] == '-h' || args[1] == 'help') - # One arg, and args[1] => '-h' '-H' 'help' - return cmd_sessions_help + if args.length == 1 && (args[0] == '-h' || args[0] == '--help') + # One arg, and args[0] => '-h' '--help' + return cmd_shell_help end if platform == 'windows' @@ -570,7 +570,7 @@ def cmd_pry_help # Open the Pry debugger on the current session # def cmd_pry(*args) - if args.include?('-h') + if args.include?('-h') || args.include?('--help') cmd_pry_help return end diff --git a/lib/msf/base/sessions/meterpreter.rb b/lib/msf/base/sessions/meterpreter.rb index 166011c13a66..d97599f895ee 100644 --- a/lib/msf/base/sessions/meterpreter.rb +++ b/lib/msf/base/sessions/meterpreter.rb @@ -95,6 +95,15 @@ def initialize(rstream, opts={}) self.console = Rex::Post::Meterpreter::Ui::Console.new(self) end + def exit + begin + self.core.shutdown + rescue StandardError + nil + end + self.shutdown_passive_dispatcher + self.console.stop + end # # Returns the session type as being 'meterpreter'. # diff --git a/lib/msf/ui/console/command_dispatcher/session.rb b/lib/msf/ui/console/command_dispatcher/session.rb new file mode 100644 index 000000000000..7ab95f8183ff --- /dev/null +++ b/lib/msf/ui/console/command_dispatcher/session.rb @@ -0,0 +1,236 @@ +# frozen_string_literal: true + +module Msf + module Ui + module Console + module CommandDispatcher + module Session + include Rex::Ui::Text::DispatcherShell::CommandDispatcher + + @@irb_opts = Rex::Parser::Arguments.new( + %w[-h --help] => [false, 'Help menu.' ], + '-e' => [true, 'Expression to evaluate.'] + ) + def commands + { + '?' => 'Help menu', + 'background' => 'Backgrounds the current session', + 'bg' => 'Alias for background', + 'exit' => 'Terminate the session', + 'help' => 'Help menu', + 'irb' => 'Open an interactive Ruby shell on the current session', + 'pry' => 'Open the Pry debugger on the current session', + 'quit' => 'Terminate the session', + 'resource' => 'Run the commands stored in a file', + 'uuid' => 'Get the UUID for the current session', + 'sessions' => 'Quickly switch to another session' + } + end + + def cmd_background_help + print_line('Usage: background') + print_line + print_line('Stop interacting with this session and return to the parent prompt') + print_line + end + + def cmd_background(*args) + if args.include?('-h') || args.include?('--help') + cmd_background_help + return + end + print_status("Backgrounding session #{client.name}...") + client.interacting = false + end + + alias cmd_bg cmd_background + alias cmd_bg_help cmd_background_help + + # + # Terminates the session. + # + def cmd_exit(*args) + print_status("Shutting down session: #{client.sid}") + client.exit + end + + alias cmd_quit cmd_exit + + def cmd_irb_help + print_line('Usage: irb') + print_line + print_line('Open an interactive Ruby shell on the current session.') + print @@irb_opts.usage + end + + def cmd_irb_tabs(str, words) + return [] if words.length > 1 + + @@irb_opts.option_keys + end + + # + # Open an interactive Ruby shell on the current session + # + def cmd_irb(*args) + expressions = [] + + # Parse the command options + @@irb_opts.parse(args) do |opt, _idx, val| + case opt + when '-e' + expressions << val + when '-h', '--help' + return cmd_irb_help + end + end + + session = client + framework = client.framework + + if expressions.empty? + print_status('Starting IRB shell...') + print_status("You are in the \"client\" (session) object\n") + framework.history_manager.with_context(name: :irb) do + Rex::Ui::Text::IrbShell.new(client).run + end + else + # XXX: No vprint_status here + if framework.datastore['VERBOSE'].to_s == 'true' + print_status("You are executing expressions in #{binding.receiver}") + end + + expressions.each { |expression| eval(expression, binding) } + end + end + + def cmd_pry_help + print_line 'Usage: pry' + print_line + print_line 'Open the Pry debugger on the current session.' + print_line + end + + # + # Open the Pry debugger on the current session + # + def cmd_pry(*args) + if args.include?('-h') || args.include?('--help') + cmd_pry_help + return + end + + begin + require 'pry' + rescue LoadError + print_error('Failed to load Pry, try "gem install pry"') + return + end + + print_status('Starting Pry shell...') + print_status("You are in the \"client\" (session) object\n") + + Pry.config.history_load = false + client.framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do + client.pry + end + end + + def cmd_sessions_help + print_line('Usage: sessions ') + print_line + print_line('Interact with a different session Id.') + print_line('This works the same as calling this from the MSF shell: sessions -i ') + print_line + end + + def cmd_sessions(*args) + if args.empty? || args[0].to_i == 0 + cmd_sessions_help + elsif args[0].to_s == client.name.to_s + print_status("Session #{client.name} is already interactive.") + else + print_status("Backgrounding session #{client.name}...") + # store the next session id so that it can be referenced as soon + # as this session is no longer interacting + client.next_session = args[0] + client.interacting = false + end + end + + def cmd_resource_help + print_line 'Usage: resource path1 [path2 ...]' + print_line + print_line 'Run the commands stored in the supplied files. (- for stdin, press CTRL+D to end input from stdin)' + print_line 'Resource files may also contain ERB or Ruby code between tags.' + print_line + end + + def cmd_resource(*args) + if args.empty? || args.include?('-h') || args.include?('--help') + cmd_resource_help + return false + end + + args.each do |res| + good_res = nil + if res == '-' + good_res = res + elsif ::File.exist?(res) + good_res = res + elsif [ + ::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', + ::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter' + ].each do |dir| + res_path = dir + ::File::SEPARATOR + res + if ::File.exist?(res_path) + good_res = res_path + break + end + end + # let's check to see if it's in the scripts/resource dir (like when tab completed) + end + unless good_res + print_error("#{res} is not a valid resource file") + next + end + + client.console.load_resource(good_res) + end + end + + def cmd_resource_tabs(str, words) + tabs = [] + # return tabs if words.length > 1 + if (str && str =~ (/^#{Regexp.escape(::File::SEPARATOR)}/)) + # then you are probably specifying a full path so let's just use normal file completion + return tab_complete_filenames(str, words) + elsif (!(words[1]) || !words[1].match(%r{^/})) + # then let's start tab completion in the scripts/resource directories + begin + [ + ::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', + ::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', + '.' + ].each do |dir| + next if !::File.exist? dir + + tabs += ::Dir.new(dir).find_all do |e| + path = dir + ::File::SEPARATOR + e + ::File.file?(path) and ::File.readable?(path) + end + end + rescue StandardError => e + elog('Problem tab completing resource file names in the scripts/resource directories', error: e) + end + else + tabs += tab_complete_filenames(str, words) + end + + return tabs + end + end + end + end + end +end diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb index a1b08f7cd88a..f4563c8c7d9f 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb @@ -13,7 +13,7 @@ module Ui ### module Console::CommandDispatcher - include Rex::Ui::Text::DispatcherShell::CommandDispatcher + include Msf::Ui::Console::CommandDispatcher::Session # # The hash of file names to class names after a module has already been diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb index 972bbd28cf72..8147705bb41e 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb @@ -33,11 +33,6 @@ def initialize(shell) @transport_map = {} end - @@irb_opts = Rex::Parser::Arguments.new( - '-h' => [false, 'Help menu.' ], - '-e' => [true, 'Expression to evaluate.'] - ) - @@load_opts = Rex::Parser::Arguments.new( '-h' => [false, 'Help menu.' ], '-l' => [false, 'List all available extensions.'] @@ -301,49 +296,12 @@ def pivot_add_named_pipe(opts) print_good("Successfully created #{opts[:type]} pivot.") end - def cmd_sessions_help - print_line('Usage: sessions ') - print_line - print_line('Interact with a different session Id.') - print_line('This works the same as calling this from the MSF shell: sessions -i ') - print_line - end - - def cmd_sessions(*args) - if args.length == 0 || args[0].to_i == 0 - cmd_sessions_help - elsif args[0].to_s == client.name.to_s - print_status("Session #{client.name} is already interactive.") - else - print_status("Backgrounding session #{client.name}...") - # store the next session id so that it can be referenced as soon - # as this session is no longer interacting - client.next_session = args[0] - client.interacting = false - end - end - def cmd_secure print_status('Negotiating new encryption key ...') client.core.secure print_good('Done.') end - def cmd_background_help - print_line('Usage: background') - print_line - print_line('Stop interacting with this session and return to the parent prompt') - print_line - end - - def cmd_background - print_status("Backgrounding session #{client.name}...") - client.interacting = false - end - - alias cmd_bg cmd_background - alias cmd_bg_help cmd_background_help - # # Displays information about active channels # @@ -500,18 +458,6 @@ def cmd_close_tabs(str, words) return tab_complete_channels end - # - # Terminates the meterpreter session. - # - def cmd_exit(*args) - print_status('Shutting down Meterpreter...') - client.core.shutdown rescue nil - client.shutdown_passive_dispatcher - shell.stop - end - - alias cmd_quit cmd_exit - def cmd_detach_help print_line('Detach from the victim. Only possible for non-stream sessions (http/https)') print_line @@ -565,85 +511,6 @@ def cmd_interact(*args) alias cmd_interact_tabs cmd_close_tabs - def cmd_irb_help - print_line('Usage: irb') - print_line - print_line('Open an interactive Ruby shell on the current session.') - print @@irb_opts.usage - end - - def cmd_irb_tabs(str, words) - return [] if words.length > 1 - @@irb_opts.option_keys - end - - # - # Open an interactive Ruby shell on the current session - # - def cmd_irb(*args) - expressions = [] - - # Parse the command options - @@irb_opts.parse(args) do |opt, idx, val| - case opt - when '-e' - expressions << val - when '-h' - return cmd_irb_help - end - end - - session = client - framework = client.framework - - if expressions.empty? - print_status('Starting IRB shell...') - print_status("You are in the \"client\" (session) object\n") - framework.history_manager.with_context(name: :irb) do - Rex::Ui::Text::IrbShell.new(client).run - end - else - # XXX: No vprint_status here - if framework.datastore['VERBOSE'].to_s == 'true' - print_status("You are executing expressions in #{binding.receiver}") - end - - expressions.each { |expression| eval(expression, binding) } - end - end - - def cmd_pry_help - print_line 'Usage: pry' - print_line - print_line 'Open the Pry debugger on the current session.' - print_line - end - - # - # Open the Pry debugger on the current session - # - def cmd_pry(*args) - if args.include?('-h') - cmd_pry_help - return - end - - begin - require 'pry' - rescue LoadError - print_error('Failed to load Pry, try "gem install pry"') - return - end - - print_status('Starting Pry shell...') - print_status("You are in the \"client\" (session) object\n") - - Pry.config.history_load = false - client.framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do - client.pry - end - end - @@set_timeouts_opts = Rex::Parser::Arguments.new( '-c' => [true, 'Comms timeout (seconds)'], '-x' => [true, 'Expiration timout (seconds)'], @@ -1731,76 +1598,6 @@ def cmd_write(*args) return true end - def cmd_resource_help - print_line "Usage: resource path1 [path2 ...]" - print_line - print_line "Run the commands stored in the supplied files. (- for stdin, press CTRL+D to end input from stdin)" - print_line "Resource files may also contain ERB or Ruby code between tags." - print_line - end - - def cmd_resource(*args) - if args.empty? - cmd_resource_help - return false - end - - args.each do |res| - good_res = nil - if res == '-' - good_res = res - elsif ::File.exist?(res) - good_res = res - elsif - # let's check to see if it's in the scripts/resource dir (like when tab completed) - [ - ::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', - ::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter' - ].each do |dir| - res_path = dir + ::File::SEPARATOR + res - if ::File.exist?(res_path) - good_res = res_path - break - end - end - end - if good_res - client.console.load_resource(good_res) - else - print_error("#{res} is not a valid resource file") - next - end - end - end - - def cmd_resource_tabs(str, words) - tabs = [] - #return tabs if words.length > 1 - if ( str and str =~ /^#{Regexp.escape(::File::SEPARATOR)}/ ) - # then you are probably specifying a full path so let's just use normal file completion - return tab_complete_filenames(str,words) - elsif (not words[1] or not words[1].match(/^\//)) - # then let's start tab completion in the scripts/resource directories - begin - [ - ::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', - ::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', - '.' - ].each do |dir| - next if not ::File.exist? dir - tabs += ::Dir.new(dir).find_all { |e| - path = dir + ::File::SEPARATOR + e - ::File.file?(path) and ::File.readable?(path) - } - end - rescue Exception - end - else - tabs += tab_complete_filenames(str,words) - end - return tabs - end - def cmd_enable_unicode_encoding client.encode_unicode = true print_status('Unicode encoding is enabled') diff --git a/spec/lib/msf/base/sessions/command_shell_spec.rb b/spec/lib/msf/base/sessions/command_shell_spec.rb new file mode 100644 index 000000000000..36130a570215 --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Msf::Sessions::CommandShell do + let(:type) { 'shell' } + + describe '.type' do + it 'should have the correct type' do + expect(described_class.type).to eq(type) + end + end + + describe '.can_cleanup_files' do + it 'should be able to cleanup files' do + expect(described_class.can_cleanup_files).to eq(true) + end + end + + context 'when we have a command shell session' do + subject(:command_shell) { described_class.new(nil) } + let(:command_functions) do + %i[help background sessions resource shell download upload source irb pry].map { |command| "cmd_#{command}" } + end + let(:command_help_functions) do + command_functions.map { |command| "#{command}_help" } + end + let(:description) { 'Command shell' } + + describe '#type' do + it 'should have the correct type' do + expect(subject.type).to eq(type) + end + end + + describe '#desc' do + it 'should have the correct description' do + expect(subject.desc).to eq(description) + end + end + + describe '#abort_foreground_supported' do + it 'should not support aborting the process running in the session' do + expect(subject.abort_foreground_supported).to be(true) + end + end + + describe '#shell_init' do + it 'should initialise the shell by default' do + expect(subject.shell_init).to be(true) + end + end + + describe 'Builtin commands' do + %i[background sessions resource shell download upload source irb pry].each do |command| + before(:each) do + allow(subject).to receive("cmd_#{command}_help") + end + + describe "#cmd_#{command}" do + context 'when called with the `-h` argument' do + it 'should call the corresponding help function' do + subject.send("cmd_#{command}", '-h') + expect(subject).to have_received("cmd_#{command}_help") + end + end + + context 'when called with the `--help` argument' do + it 'should call the corresponding help function' do + subject.send("cmd_#{command}", '--help') + expect(subject).to have_received("cmd_#{command}_help") + end + end + end + end + end + + describe '#run_builtin_cmd' do + %i[help background sessions resource shell download upload source irb pry].each do |command| + before(:each) do + allow(subject).to receive("cmd_#{command}") + end + context "when called with `#{command}`" do + it "should call cmd_#{command}" do + subject.run_builtin_cmd(command.to_s, nil) + expect(subject).to have_received("cmd_#{command}") + end + end + end + end + + describe '#run_single' do + before(:each) do + allow(subject).to receive(:run_builtin_cmd) + allow(subject).to receive(:shell_write) + end + %i[help background sessions resource shell download upload source irb pry].each do |command| + context "when called with builtin command `#{command}`" do + it 'should call the builtin function' do + subject.run_single(command.to_s) + expect(subject).to have_received(:run_builtin_cmd) + end + end + end + + context 'when called with a non-builtin command' do + let(:cmd) { 'some_command' } + it 'should write the command to the shell' do + subject.run_single(cmd) + expect(subject).to have_received(:shell_write).with("#{cmd}\n") + end + end + end + + describe '#process_autoruns' do + let(:initial_auto_run_script) { 'initial_auto_run_script' } + let(:auto_run_script) { 'auto_run_script' } + + before(:each) do + allow(subject).to receive(:execute_script) + end + + context 'The datastore is empty' do + let(:datastore) do + Msf::DataStore.new + end + it 'should not execute any script' do + subject.process_autoruns(datastore) + is_expected.not_to have_received(:execute_script) + end + end + + context 'The datastore contains an `InitialAutoRunScript`' do + let(:datastore) do + datastore = Msf::DataStore.new + datastore['InitialAutoRunScript'] = initial_auto_run_script + datastore + end + + it 'should execute the script' do + subject.process_autoruns(datastore) + is_expected.to have_received(:execute_script).with(initial_auto_run_script) + end + end + + context 'The datastore contains an `AutoRunScript`' do + let(:datastore) do + datastore = Msf::DataStore.new + datastore['AutoRunScript'] = auto_run_script + datastore + end + it 'should execute the script' do + subject.process_autoruns(datastore) + is_expected.to have_received(:execute_script).with(auto_run_script) + end + end + + context 'The datastore contains both `InitialAutoRunScript` and `AutoRunScript`' do + let(:datastore) do + datastore = Msf::DataStore.new + datastore['InitialAutoRunScript'] = initial_auto_run_script + datastore['AutoRunScript'] = auto_run_script + datastore + end + it 'should execute initial script before the auto run script' do + subject.process_autoruns(datastore) + is_expected.to have_received(:execute_script).ordered.with(initial_auto_run_script) + is_expected.to have_received(:execute_script).ordered.with(auto_run_script) + end + end + end + + context 'when the platform is windows' do + let(:platform) { 'windows' } + before(:each) do + subject.platform = platform + end + + it 'should not support aborting the process running in the session' do + expect(subject.abort_foreground_supported).to be(false) + end + end + end +end diff --git a/spec/lib/msf/base/sessions/meterpreter_spec.rb b/spec/lib/msf/base/sessions/meterpreter_spec.rb index 1de154598dbe..8ec70758d133 100644 --- a/spec/lib/msf/base/sessions/meterpreter_spec.rb +++ b/spec/lib/msf/base/sessions/meterpreter_spec.rb @@ -153,5 +153,16 @@ end -end + describe '#exit' do + it 'shuts down the session' do + allow(subject).to receive(:core).and_return(double('core')) + allow(subject).to receive(:console).and_return(double('console')) + + expect(subject.core).to receive(:shutdown) + expect(subject).to receive(:shutdown_passive_dispatcher) + expect(subject.console).to receive(:stop) + subject.exit + end + end +end diff --git a/spec/lib/rex/post/meterpreter/ui/console/command_dispatcher/core_spec.rb b/spec/lib/rex/post/meterpreter/ui/console/command_dispatcher/core_spec.rb new file mode 100644 index 000000000000..5a74a44275b4 --- /dev/null +++ b/spec/lib/rex/post/meterpreter/ui/console/command_dispatcher/core_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rex/post/meterpreter/ui/console/command_dispatcher/core' + +RSpec.describe Rex::Post::Meterpreter::Ui::Console::CommandDispatcher::Core do + let(:client) { instance_double(Msf::Sessions::Meterpreter) } + let(:console) do + console = Rex::Post::Meterpreter::Ui::Console.new(client) + console.disable_output = true + console + end + + before(:each) do + allow(client).to receive(:console).and_return(console) + allow(client).to receive(:name).and_return('test client name') + allow(client).to receive(:sid).and_return('test client sid') + end + + subject(:command_dispatcher) { described_class.new(client.console) } + + it_behaves_like 'session command dispatcher' +end diff --git a/spec/support/shared/examples/msf/ui/console/command_dispatcher/session_spec.rb b/spec/support/shared/examples/msf/ui/console/command_dispatcher/session_spec.rb new file mode 100644 index 000000000000..ea65ba4d1381 --- /dev/null +++ b/spec/support/shared/examples/msf/ui/console/command_dispatcher/session_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +RSpec.shared_examples_for 'session command dispatcher' do + include_context 'Msf::Simple::Framework' + + describe '#client' do + subject { command_dispatcher.client } + it { is_expected.to be(client) } + end + + describe 'Core commands' do + describe '#cmd_background' do + before(:each) do + allow(client).to receive(:interacting=) + end + + it 'backgrounds the session' do + subject.cmd_background + expect(client).to have_received(:interacting=).with(false) + end + + it 'is aliased to #cmd_bg' do + expect(subject.method(:cmd_background)).to eq(subject.method(:cmd_bg)) + end + end + + describe '#cmd_exit' do + before(:each) do + allow(client).to receive(:exit) + end + + it 'shuts down the session' do + subject.cmd_exit + expect(client).to have_received(:exit) + end + + it 'is aliased to #cmd_quit' do + expect(subject.method(:cmd_exit)).to eq(subject.method(:cmd_quit)) + end + end + + describe '#cmd_irb' do + let(:history_manager) { double('history_manager') } + before(:each) do + allow(client).to receive(:framework).and_return(framework) + allow(framework).to receive(:history_manager).and_return(history_manager) + allow(history_manager).to receive(:with_context).and_yield + allow(Rex::Ui::Text::IrbShell).to receive(:new).with(client).and_return(irb_shell) + allow(irb_shell).to receive(:run) + end + let(:irb_shell) { instance_double(Rex::Ui::Text::IrbShell) } + it 'runs an irb shell instance' do + subject.cmd_irb + expect(Rex::Ui::Text::IrbShell).to have_received(:new).with(client) + expect(irb_shell).to have_received(:run) + end + end + + describe '#cmd_sessions' do + context 'when switching to a new session' do + before(:each) do + allow(client).to receive(:interacting=) + allow(client).to receive(:next_session=) + end + + let(:new_session_id) { 2 } + + it 'backgrounds the session and switches to the new session' do + subject.cmd_sessions(new_session_id) + expect(client).to have_received(:interacting=).with(false) + expect(client).to have_received(:next_session=).with(new_session_id) + end + end + end + + describe '#cmd_resource' do + context 'when there is a valid resource script' do + let(:valid_resource_path) { 'valid/resource/path' } + before(:each) do + allow(File).to receive(:exist?).and_return(valid_resource_path) + allow(client.console).to receive(:load_resource) + end + it 'executes the resource script' do + subject.cmd_resource(valid_resource_path) + expect(client.console).to have_received(:load_resource).with(valid_resource_path) + end + end + end + + %i[help background sessions resource irb pry exit].each do |command| + describe "#cmd_#{command}" do + before(:each) do + allow(subject).to receive("cmd_#{command}_help") + end + next if %i[help exit].include?(command) # These commands don't require`-h/--help` + + context 'when called with the `-h` argument' do + it 'should call the corresponding help function' do + subject.send("cmd_#{command}", '-h') + expect(subject).to have_received("cmd_#{command}_help") + end + end + + context 'when called with the `--help` argument' do + it 'should call the corresponding help function' do + subject.send("cmd_#{command}", '--help') + expect(subject).to have_received("cmd_#{command}_help") + end + end + end + end + end +end