Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/spec/acceptance/command_shell_spec.rb
Views: 11766
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::LINUX12}13)1415allure_test_environment = AllureRspec.configuration.environment_properties1617let_it_be(:current_platform) { Acceptance::Session::current_platform }1819# @!attribute [r] port_allocator20# @return [Acceptance::PortAllocator]21let_it_be(:port_allocator) { Acceptance::PortAllocator.new }2223# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly24let_it_be(:driver) do25driver = Acceptance::ConsoleDriver.new26driver27end2829# Opens a test console with the test loadpath specified30# @!attribute [r] console31# @return [Acceptance::Console]32let_it_be(:console) do33console = driver.open_console3435# Load the test modules36console.sendline('loadpath test/modules')37console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)38console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)39console.recvuntil(/\d+ exploit modules[^\n]*\n/)40console.recvuntil(/\d+ post modules[^\n]*\n/)41console.recvuntil(Acceptance::Console.prompt)4243# Read the remaining console44# console.sendline "quit -y"45# console.recv_available4647console48end4950COMMAND_SHELL_PAYLOADS.each do |command_shell_name, command_shell_config|51command_shell_runtime_name = "#{command_shell_name}#{ENV.fetch('COMMAND_SHELL_RUNTIME_VERSION', '')}"5253describe command_shell_runtime_name, focus: command_shell_config[:focus] do54command_shell_config[:payloads].each.with_index do |payload_config, payload_config_index|55describe(56Acceptance::Session.human_name_for_payload(payload_config).to_s,57if: (58Acceptance::Session.run_session?(command_shell_config) &&59Acceptance::Session.supported_platform?(payload_config)60)61) do62let(:payload) { Acceptance::Payload.new(payload_config) }6364class LocalPath65attr_reader :path6667def initialize(path)68@path = path69end70end7172let(:session_tlv_logging_file) do73# LocalPath.new('/tmp/php_session_tlv_log.txt')74Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt')75end7677let(:command_shell_logging_file) do78# LocalPath.new('/tmp/php_log.txt')79Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt')80end8182let(:payload_stdout_and_stderr_file) do83# LocalPath.new('/tmp/php_log.txt')84Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt')85end8687let(:default_global_datastore) do88{89SessionTlvLogging: "file:#{session_tlv_logging_file.path}"90}91end9293let(:test_environment) { allure_test_environment }9495let(:default_module_datastore) do96{97AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10,98lport: port_allocator.next,99lhost: '127.0.0.1'100}101end102103let(:executed_payload) do104file = File.open(payload_stdout_and_stderr_file.path, 'w')105driver.run_payload(106payload,107{108out: file,109err: file110}111)112end113114# The shared payload process and session instance that will be reused across the test run115#116let(:payload_process_and_session_id) do117console.sendline "use #{payload.name}"118console.recvuntil(Acceptance::Console.prompt)119120# Set global options121console.sendline payload.setg_commands(default_global_datastore: default_global_datastore)122console.recvuntil(Acceptance::Console.prompt)123124# Generate the payload125console.sendline payload.generate_command(default_module_datastore: default_module_datastore)126console.recvuntil(/Writing \d+ bytes[^\n]*\n/)127generate_result = console.recvuntil(Acceptance::Console.prompt)128129expect(generate_result.lines).to_not include(match('generation failed'))130wait_for_expect do131expect(payload.size).to be > 0132end133134console.sendline payload.handler_command(default_module_datastore: default_module_datastore)135console.recvuntil(/Started reverse TCP handler[^\n]*\n/)136payload_process = executed_payload137session_id = nil138139# Wait for the session to open, or break early if the payload is detected as dead140larger_retry_count_for_powershell = 600141wait_for_expect(larger_retry_count_for_powershell) do142unless payload_process.alive?143break144end145146session_opened_matcher = /session (\d+) opened[^\n]*\n/147session_message = ''148begin149session_message = console.recvuntil(session_opened_matcher, timeout: 1)150rescue Acceptance::ChildProcessRecvError151# noop152end153154session_id = session_message[session_opened_matcher, 1]155expect(session_id).to_not be_nil156end157158[payload_process, session_id]159end160161# @param [String] path The file path to read the content of162# @return [String] The file contents if found163def get_file_attachment_contents(path)164return 'none resent' unless File.exist?(path)165166content = File.binread(path)167content.blank? ? 'file created - but empty' : content168end169170before :each do |example|171next unless example.respond_to?(:parameter)172173# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI174test_environment.each do |key, value|175example.parameter(key, value)176end177end178179after :all do180driver.close_payloads181console.reset182end183184context "#{Acceptance::Session.current_platform}" do185command_shell_config[:module_tests].each do |module_test|186describe module_test[:name].to_s, focus: module_test[:focus] do187it(188"#{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",189if: (190Acceptance::Session.run_session?(command_shell_config) &&191# Run if ENV['SESSION_MODULE_TEST'] = 'post/test/cmd_exec' etc192Acceptance::Session.run_session_module_test?(module_test[:name]) &&193# Only run payloads / tests, if the host machine can run them194Acceptance::Session.supported_platform?(payload_config) &&195Acceptance::Session.supported_platform?(module_test) &&196# Skip tests that are explicitly skipped, or won't pass in the current environment197!Acceptance::Session.skipped_module_test?(module_test, allure_test_environment)198),199# test metadata - will appear in allure report200module_test: module_test[:name]201) do202begin203replication_commands = []204current_payload_status = ''205206known_failures = module_test.dig(:lines, :all, :known_failures) || []207known_failures += module_test.dig(:lines, current_platform, :known_failures) || []208known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }209210required_lines = module_test.dig(:lines, :all, :required) || []211required_lines += module_test.dig(:lines, current_platform, :required) || []212required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }213214# 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 dies215payload_process, session_id = payload_process_and_session_id216217expect(payload_process).to(be_alive, proc do218$stderr.puts "Made it inside expect payload_process: #{payload_process}"219$stderr.puts "Is the process alive?: #{payload_process.alive?}"220$stderr.puts "Process wait.thread?: #{payload_process.wait_thread}"221$stderr.puts "We have access to .wait_thread, but do we have access to .wait_thread.value?: #{payload_process.alive?}"222223current_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}"224225$stderr.puts "Made it after current_payload_status: #{payload_process}"226$stderr.puts "Is the process alive?: #{payload_process.alive?}"227228Allure.add_attachment(229name: 'Failed payload blob',230source: Base64.strict_encode64(File.binread(payload_process.payload_path)),231type: Allure::ContentType::TXT232)233234current_payload_status235end)236expect(session_id).to_not(be_nil, proc do237"There should be a session present"238end)239240use_module = "use #{module_test[:name]}"241run_module = "run session=#{session_id} AddEntropy=true Verbose=true"242243replication_commands << use_module244console.sendline(use_module)245console.recvuntil(Acceptance::Console.prompt)246247replication_commands << run_module248console.sendline(run_module)249250# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:251# console.interact252253# Expect the test module to complete254test_result = console.recvuntil('Post module execution completed')255256# Ensure there are no failures, and assert tests are complete257aggregate_failures("#{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests") do258# Skip any ignored lines from the validation input259validated_lines = test_result.lines.reject do |line|260is_acceptable = known_failures.any? do |acceptable_failure|261line.include?(acceptable_failure.value) &&262acceptable_failure.if?(test_environment)263end || line.match?(/Passed: \d+; Failed: \d+/)264265is_acceptable266end267268validated_lines.each do |test_line|269test_line = Acceptance::Session.uncolorize(test_line)270expect(test_line).to_not include('FAILED', '[-] '), "Unexpected error: #{test_line}"271end272273# Assert all expected lines are present274required_lines.each do |required|275next unless required.if?(test_environment)276277expect(test_result).to include(required.value)278end279280# Assert all ignored lines are present, if they are not present - they should be removed from281# the calling config282known_failures.each do |acceptable_failure|283next if acceptable_failure.flaky?(test_environment)284next unless acceptable_failure.if?(test_environment)285286expect(test_result).to include(acceptable_failure.value)287end288end289rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e290test_run_error = e291end292293# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are294# still generated if the session dies in a weird way etc295296# Payload process cleanup / verification297# The payload process wasn't initially marked as dead - let's close it298if payload_process.present? && current_payload_status.blank?299begin300if payload_process.alive?301current_payload_status = "Process still alive after running test suite"302payload_process.close303else304current_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}"305end306rescue => e307Allure.add_attachment(308name: 'driver.close_payloads failure information',309source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",310type: Allure::ContentType::TXT311)312end313end314315console_reset_error = nil316current_console_data = console.all_data317begin318console.reset319rescue => e320console_reset_error = e321Allure.add_attachment(322name: 'console.reset failure information',323source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",324type: Allure::ContentType::TXT325)326end327328payload_configuration_details = payload.as_readable_text(329default_global_datastore: default_global_datastore,330default_module_datastore: default_module_datastore331)332333replication_steps = <<~EOF334## Load test modules335loadpath test/modules336337#{payload_configuration_details}338339## Replication commands340#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}341EOF342343Allure.add_attachment(344name: 'payload configuration and replication',345source: replication_steps,346type: Allure::ContentType::TXT347)348349Allure.add_attachment(350name: 'payload output if available',351source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",352type: Allure::ContentType::TXT353)354355Allure.add_attachment(356name: 'session tlv logging if available',357source: get_file_attachment_contents(session_tlv_logging_file.path),358type: Allure::ContentType::TXT359)360361Allure.add_attachment(362name: 'console data',363source: current_console_data,364type: Allure::ContentType::TXT365)366367test_assertions = JSON.pretty_generate(368{369required_lines: required_lines.map(&:to_h),370known_failures: known_failures.map(&:to_h),371}372)373Allure.add_attachment(374name: 'test assertions',375source: test_assertions,376type: Allure::ContentType::TXT377)378379raise test_run_error if test_run_error380raise console_reset_error if console_reset_error381end382end383end384end385end386end387end388end389end390391392