Path: blob/master/spec/acceptance/command_shell_spec.rb
78579 views
require 'acceptance_spec_helper'1require 'base64'23RSpec.describe 'CommandShell' do4include_context 'wait_for_expect'56# Tests to ensure that CMD/Powershell/Linux is consistent across all implementations/operation systems7COMMAND_SHELL_PAYLOADS = Acceptance::Session.with_session_name_merged(8{9powershell: Acceptance::Session::POWERSHELL,10cmd: Acceptance::Session::CMD,11linux: Acceptance::Session::LINUX,12python_ssl_2_6: Acceptance::Session::PYTHON_SSL_2_6,13python_ssl_2_7: Acceptance::Session::PYTHON_SSL_2_7,14python_ssl_3_4: Acceptance::Session::PYTHON_SSL_3_4,15python_ssl_3_13: Acceptance::Session::PYTHON_SSL_3_13,16}17)1819allure_test_environment = AllureRspec.configuration.environment_properties2021let_it_be(:current_platform) { Acceptance::Session::current_platform }2223# @!attribute [r] port_allocator24# @return [Acceptance::PortAllocator]25let_it_be(:port_allocator) { Acceptance::PortAllocator.new }2627# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly28let_it_be(:driver) do29driver = Acceptance::ConsoleDriver.new30driver31end3233# Opens a test console with the test loadpath specified34# @!attribute [r] console35# @return [Acceptance::Console]36let_it_be(:console) do37console = driver.open_console3839# Load the test modules40console.sendline('loadpath test/modules')41console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)42console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)43console.recvuntil(/\d+ exploit modules[^\n]*\n/)44console.recvuntil(/\d+ post modules[^\n]*\n/)45console.recvuntil(Acceptance::Console.prompt)4647# Read the remaining console48# console.sendline "quit -y"49# console.recv_available5051console52end5354COMMAND_SHELL_PAYLOADS.each do |command_shell_name, command_shell_config|55command_shell_runtime_name = "#{command_shell_name}#{ENV.fetch('COMMAND_SHELL_RUNTIME_VERSION', '')}"5657describe command_shell_runtime_name, focus: command_shell_config[:focus] do58command_shell_config[:payloads].each.with_index do |payload_config, payload_config_index|59describe(60Acceptance::Session.human_name_for_payload(payload_config).to_s,61if: (62Acceptance::Session.run_session?(command_shell_config) &&63Acceptance::Session.supported_platform?(payload_config)64)65) do66let(:payload) { Acceptance::Payload.new(payload_config) }6768class LocalPath69attr_reader :path7071def initialize(path)72@path = path73end74end7576let(:session_tlv_logging_file) do77# LocalPath.new('/tmp/php_session_tlv_log.txt')78Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt')79end8081let(:command_shell_logging_file) do82# LocalPath.new('/tmp/php_log.txt')83Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt')84end8586let(:payload_stdout_and_stderr_file) do87# LocalPath.new('/tmp/php_log.txt')88Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt')89end9091let(:default_global_datastore) do92{93SessionTlvLogging: "file:#{session_tlv_logging_file.path}"94}95end9697let(:test_environment) { allure_test_environment }9899let(:default_module_datastore) do100{101AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10,102lport: port_allocator.next,103lhost: '127.0.0.1'104}105end106107let(:executed_payload) do108file = File.open(payload_stdout_and_stderr_file.path, 'w')109driver.run_payload(110payload,111{112out: file,113err: file114}115)116end117118# The shared payload process and session instance that will be reused across the test run119#120let(:payload_process_and_session_id) do121console.sendline "use #{payload.name}"122console.recvuntil(Acceptance::Console.prompt)123124# Set global options125console.sendline payload.setg_commands(default_global_datastore: default_global_datastore)126console.recvuntil(Acceptance::Console.prompt)127128# Generate the payload129console.sendline payload.generate_command(default_module_datastore: default_module_datastore)130console.recvuntil(/Writing \d+ bytes[^\n]*\n/)131generate_result = console.recvuntil(Acceptance::Console.prompt)132133expect(generate_result.lines).to_not include(match('generation failed'))134wait_for_expect do135expect(payload.size).to be > 0136end137138console.sendline payload.handler_command(default_module_datastore: default_module_datastore)139console.recvuntil(/Started reverse (?:TCP|SSL) handler[^\n]*\n/)140payload_process = executed_payload141session_id = nil142143# Wait for the session to open, or break early if the payload is detected as dead144larger_retry_count_for_powershell = 600145wait_for_expect(larger_retry_count_for_powershell) do146unless payload_process.alive?147break148end149150session_opened_matcher = /session (\d+) opened[^\n]*\n/151session_message = ''152begin153session_message = console.recvuntil(session_opened_matcher, timeout: 1)154rescue Acceptance::ChildProcessRecvError155# noop156end157158session_id = session_message[session_opened_matcher, 1]159expect(session_id).to_not be_nil160end161162[payload_process, session_id]163end164165# @param [String] path The file path to read the content of166# @return [String] The file contents if found167def get_file_attachment_contents(path)168return 'none resent' unless File.exist?(path)169170content = File.binread(path)171content.blank? ? 'file created - but empty' : content172end173174before :each do |example|175next unless example.respond_to?(:parameter)176177# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI178test_environment.each do |key, value|179example.parameter(key, value)180end181end182183after :all do184driver.close_payloads185console.reset186end187188context "#{Acceptance::Session.current_platform}" do189command_shell_config[:module_tests].each do |module_test|190describe module_test[:name].to_s, focus: module_test[:focus] do191it(192"#{Acceptance::Session.current_platform}/#{command_shell_runtime_name} command shell successfully opens a session for the #{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests",193if: (194Acceptance::Session.run_session?(command_shell_config) &&195# Run if ENV['SESSION_MODULE_TEST'] = 'post/test/cmd_exec' etc196Acceptance::Session.run_session_module_test?(module_test[:name]) &&197# Only run payloads / tests, if the host machine can run them198Acceptance::Session.supported_platform?(payload_config) &&199Acceptance::Session.supported_platform?(module_test) &&200# Skip tests that are explicitly skipped, or won't pass in the current environment201!Acceptance::Session.skipped_module_test?(module_test, allure_test_environment)202),203# test metadata - will appear in allure report204module_test: module_test[:name]205) do206begin207replication_commands = []208current_payload_status = ''209210known_failures = module_test.dig(:lines, :all, :known_failures) || []211known_failures += module_test.dig(:lines, current_platform, :known_failures) || []212known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }213214required_lines = module_test.dig(:lines, :all, :required) || []215required_lines += module_test.dig(:lines, current_platform, :required) || []216required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }217218# Ensure we have a valid session id; We intentionally omit this from a `before(:each)` to ensure the allure attachments are generated if the session dies219payload_process, session_id = payload_process_and_session_id220221expect(payload_process).to(be_alive, proc do222$stderr.puts "Made it inside expect payload_process: #{payload_process}"223$stderr.puts "Is the process alive?: #{payload_process.alive?}"224$stderr.puts "Process wait.thread?: #{payload_process.wait_thread}"225$stderr.puts "We have access to .wait_thread, but do we have access to .wait_thread.value?: #{payload_process.alive?}"226227current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"228229$stderr.puts "Made it after current_payload_status: #{payload_process}"230$stderr.puts "Is the process alive?: #{payload_process.alive?}"231232Allure.add_attachment(233name: 'Failed payload blob',234source: Base64.strict_encode64(File.binread(payload_process.payload_path)),235type: Allure::ContentType::TXT236)237238current_payload_status239end)240expect(session_id).to_not(be_nil, proc do241"There should be a session present"242end)243244use_module = "use #{module_test[:name]}"245run_module = "run session=#{session_id} AddEntropy=true Verbose=true"246247replication_commands << use_module248console.sendline(use_module)249console.recvuntil(Acceptance::Console.prompt)250251replication_commands << run_module252console.sendline(run_module)253254# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:255# console.interact256257# Expect the test module to complete258test_result = console.recvuntil('Post module execution completed')259260# Ensure there are no failures, and assert tests are complete261aggregate_failures("#{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests") do262# Skip any ignored lines from the validation input263validated_lines = test_result.lines.reject do |line|264is_acceptable = known_failures.any? do |acceptable_failure|265line.include?(acceptable_failure.value) &&266acceptable_failure.if?(test_environment)267end || line.match?(/Passed: \d+; Failed: \d+/)268269is_acceptable270end271272validated_lines.each do |test_line|273test_line = Acceptance::Session.uncolorize(test_line)274expect(test_line).to_not include('FAILED', '[-] '), "Unexpected error: #{test_line}"275end276277# Assert all expected lines are present278required_lines.each do |required|279next unless required.if?(test_environment)280281expect(test_result).to include(required.value)282end283284# Assert all ignored lines are present, if they are not present - they should be removed from285# the calling config286known_failures.each do |acceptable_failure|287next if acceptable_failure.flaky?(test_environment)288next unless acceptable_failure.if?(test_environment)289290expect(test_result).to include(acceptable_failure.value)291end292end293rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e294test_run_error = e295end296297# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are298# still generated if the session dies in a weird way etc299300# Payload process cleanup / verification301# The payload process wasn't initially marked as dead - let's close it302if payload_process.present? && current_payload_status.blank?303begin304if payload_process.alive?305current_payload_status = "Process still alive after running test suite"306payload_process.close307else308current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}"309end310rescue => e311Allure.add_attachment(312name: 'driver.close_payloads failure information',313source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",314type: Allure::ContentType::TXT315)316end317end318319console_reset_error = nil320current_console_data = console.all_data321begin322console.reset323rescue => e324console_reset_error = e325Allure.add_attachment(326name: 'console.reset failure information',327source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",328type: Allure::ContentType::TXT329)330end331332payload_configuration_details = payload.as_readable_text(333default_global_datastore: default_global_datastore,334default_module_datastore: default_module_datastore335)336337replication_steps = <<~EOF338## Load test modules339loadpath test/modules340341#{payload_configuration_details}342343## Replication commands344#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}345EOF346347Allure.add_attachment(348name: 'payload configuration and replication',349source: replication_steps,350type: Allure::ContentType::TXT351)352353Allure.add_attachment(354name: 'payload output if available',355source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",356type: Allure::ContentType::TXT357)358359Allure.add_attachment(360name: 'session tlv logging if available',361source: get_file_attachment_contents(session_tlv_logging_file.path),362type: Allure::ContentType::TXT363)364365Allure.add_attachment(366name: 'console data',367source: current_console_data,368type: Allure::ContentType::TXT369)370371test_assertions = JSON.pretty_generate(372{373required_lines: required_lines.map(&:to_h),374known_failures: known_failures.map(&:to_h),375}376)377Allure.add_attachment(378name: 'test assertions',379source: test_assertions,380type: Allure::ContentType::TXT381)382383raise test_run_error if test_run_error384raise console_reset_error if console_reset_error385end386end387end388end389end390end391end392end393end394395396