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/meterpreter_spec.rb
Views: 11766
require 'acceptance_spec_helper'1require 'base64'23RSpec.describe 'Meterpreter' do4include_context 'wait_for_expect'56# Tests to ensure that Meterpreter is consistent across all implementations/operation systems7METERPRETER_PAYLOADS = Acceptance::Session.with_session_name_merged(8{9python: Acceptance::Session::PYTHON_METERPRETER,10php: Acceptance::Session::PHP_METERPRETER,11java: Acceptance::Session::JAVA_METERPRETER,12mettle: Acceptance::Session::METTLE_METERPRETER,13windows_meterpreter: Acceptance::Session::WINDOWS_METERPRETER14}15)1617allure_test_environment = AllureRspec.configuration.environment_properties1819let_it_be(:current_platform) { Acceptance::Session::current_platform }2021# @!attribute [r] port_allocator22# @return [Acceptance::PortAllocator]23let_it_be(:port_allocator) { Acceptance::PortAllocator.new }2425# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly26let_it_be(:driver) do27driver = Acceptance::ConsoleDriver.new28driver29end3031# Opens a test console with the test loadpath specified32# @!attribute [r] console33# @return [Acceptance::Console]34let_it_be(:console) do35console = driver.open_console3637# Load the test modules38console.sendline('loadpath test/modules')39console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)40console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)41console.recvuntil(/\d+ exploit modules[^\n]*\n/)42console.recvuntil(/\d+ post modules[^\n]*\n/)43console.recvuntil(Acceptance::Console.prompt)4445# Read the remaining console46# console.sendline "quit -y"47# console.recv_available4849console50end5152METERPRETER_PAYLOADS.each do |meterpreter_name, meterpreter_config|53meterpreter_runtime_name = "#{meterpreter_name}#{ENV.fetch('METERPRETER_RUNTIME_VERSION', '')}"5455describe meterpreter_runtime_name, focus: meterpreter_config[:focus] do56meterpreter_config[:payloads].each.with_index do |payload_config, payload_config_index|57describe(58Acceptance::Session.human_name_for_payload(payload_config).to_s,59if: (60Acceptance::Session.run_meterpreter?(meterpreter_config) &&61Acceptance::Session.supported_platform?(payload_config)62)63) do64let(:payload) { Acceptance::Payload.new(payload_config) }6566class LocalPath67attr_reader :path6869def initialize(path)70@path = path71end72end7374let(:session_tlv_logging_file) do75# LocalPath.new('/tmp/php_session_tlv_log.txt')76Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt')77end7879let(:meterpreter_logging_file) do80# LocalPath.new('/tmp/php_log.txt')81Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt')82end8384let(:payload_stdout_and_stderr_file) do85# LocalPath.new('/tmp/php_log.txt')86Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt')87end8889let(:default_global_datastore) do90{91SessionTlvLogging: "file:#{session_tlv_logging_file.path}"92}93end9495let(:test_environment) { allure_test_environment }9697let(:default_module_datastore) do98{99AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10,100lport: port_allocator.next,101lhost: '127.0.0.1',102MeterpreterDebugLogging: "rpath:#{meterpreter_logging_file.path}"103}104end105106let(:executed_payload) do107file = File.open(payload_stdout_and_stderr_file.path, 'w')108driver.run_payload(109payload,110{111out: file,112err: file113}114)115end116117# The shared payload process and session instance that will be reused across the test run118#119let(:payload_process_and_session_id) do120console.sendline "use #{payload.name}"121console.recvuntil(Acceptance::Console.prompt)122123# Set global options124console.sendline payload.setg_commands(default_global_datastore: default_global_datastore)125console.recvuntil(Acceptance::Console.prompt)126127# Generate the payload128console.sendline payload.generate_command(default_module_datastore: default_module_datastore)129console.recvuntil(/Writing \d+ bytes[^\n]*\n/)130generate_result = console.recvuntil(Acceptance::Console.prompt)131132expect(generate_result.lines).to_not include(match('generation failed'))133wait_for_expect do134expect(payload.size).to be > 0135end136137console.sendline payload.handler_command(default_module_datastore: default_module_datastore)138console.recvuntil(/Started reverse TCP handler[^\n]*\n/)139payload_process = executed_payload140session_id = nil141142# Wait for the session to open, or break early if the payload is detected as dead143wait_for_expect do144unless payload_process.alive?145break146end147148session_opened_matcher = /Meterpreter session (\d+) opened[^\n]*\n/149session_message = ''150begin151session_message = console.recvuntil(session_opened_matcher, timeout: 1)152rescue Acceptance::ChildProcessRecvError153# noop154end155156session_id = session_message[session_opened_matcher, 1]157expect(session_id).to_not be_nil158end159160[payload_process, session_id]161end162163# @param [String] path The file path to read the content of164# @return [String] The file contents if found165def get_file_attachment_contents(path)166return 'none resent' unless File.exist?(path)167168content = File.binread(path)169content.blank? ? 'file created - but empty' : content170end171172before :each do |example|173next unless example.respond_to?(:parameter)174175# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI176test_environment.each do |key, value|177example.parameter(key, value)178end179end180181after :all do182driver.close_payloads183console.reset184end185186context "#{Acceptance::Session.current_platform}" do187describe "#{Acceptance::Session.current_platform}/#{meterpreter_runtime_name} Meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload" do188it(189"exposes available metasploit commands",190if: (191# Assume that regardless of payload, staged/unstaged/etc, the Meterpreter will have the same commands available192# So only run this test when config_index == 0193payload_config_index == 0 && Acceptance::Session.supported_platform?(payload_config)194# Run if ENV['SESSION'] = 'java php' etc195Acceptance::Session.run_meterpreter?(meterpreter_config) &&196# Only run payloads / tests, if the host machine can run them197Acceptance::Session.supported_platform?(payload_config)198)199) do200begin201replication_commands = []202current_payload_status = ''203204# 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 dies205payload_process, session_id = payload_process_and_session_id206expect(payload_process).to(be_alive, proc do207current_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}"208209Allure.add_attachment(210name: 'Failed payload blob',211source: Base64.strict_encode64(File.binread(payload_process.payload_path)),212type: Allure::ContentType::TXT213)214215current_payload_status216end)217expect(session_id).to_not(be_nil, proc do218"There should be a session present"219end)220221resource_command = "resource scripts/resource/meterpreter_compatibility.rc"222replication_commands << resource_command223console.sendline(resource_command)224result = console.recvuntil(Acceptance::Console.prompt)225226available_commands = result.lines(chomp: true).find do |line|227line.start_with?("{") && line.end_with?("}") && JSON.parse(line)228rescue JSON::ParserError => _e229next230end231expect(available_commands).to_not be_nil232233available_commands_json = JSON.parse(available_commands, symbolize_names: true)234# Generate an allure attachment, a report can be generated afterwards235Allure.add_attachment(236name: 'available commands',237source: JSON.pretty_generate(available_commands_json),238type: Allure::ContentType::JSON,239test_case: false240)241expect(available_commands_json[:sessions].length).to be 1242expect(available_commands_json[:sessions].first[:commands]).to_not be_empty243rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e244test_run_error = e245end246247# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are248# still generated if the session dies in a weird way etc249250# Payload process cleanup / verification251# The payload process wasn't initially marked as dead - let's close it252if payload_process.present? && current_payload_status.blank?253begin254if payload_process.alive?255current_payload_status = "Process still alive after running test suite"256payload_process.close257else258current_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}"259end260rescue => e261Allure.add_attachment(262name: 'driver.close_payloads failure information',263source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",264type: Allure::ContentType::TXT265)266end267end268269console_reset_error = nil270current_console_data = console.all_data271begin272console.reset273rescue => e274console_reset_error = e275Allure.add_attachment(276name: 'console.reset failure information',277source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",278type: Allure::ContentType::TXT279)280end281282payload_configuration_details = payload.as_readable_text(283default_global_datastore: default_global_datastore,284default_module_datastore: default_module_datastore285)286287replication_steps = <<~EOF288## Load test modules289loadpath test/modules290291#{payload_configuration_details}292293## Replication commands294#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}295EOF296297Allure.add_attachment(298name: 'payload configuration and replication',299source: replication_steps,300type: Allure::ContentType::TXT301)302303Allure.add_attachment(304name: 'payload output if available',305source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",306type: Allure::ContentType::TXT307)308309Allure.add_attachment(310name: 'payload debug log if available',311source: get_file_attachment_contents(meterpreter_logging_file.path),312type: Allure::ContentType::TXT313)314315Allure.add_attachment(316name: 'session tlv logging if available',317source: get_file_attachment_contents(session_tlv_logging_file.path),318type: Allure::ContentType::TXT319)320321Allure.add_attachment(322name: 'console data',323source: current_console_data,324type: Allure::ContentType::TXT325)326327raise test_run_error if test_run_error328raise console_reset_error if console_reset_error329end330end331332meterpreter_config[:module_tests].each do |module_test|333describe module_test[:name].to_s, focus: module_test[:focus] do334it(335"#{Acceptance::Session.current_platform}/#{meterpreter_runtime_name} meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests",336if: (337# Run if ENV['SESSION'] = 'java php' etc338Acceptance::Session.run_meterpreter?(meterpreter_config) &&339# Run if ENV['SESSION_MODULE_TEST'] = 'test/cmd_exec' etc340Acceptance::Session.run_meterpreter_module_test?(module_test[:name]) &&341# Only run payloads / tests, if the host machine can run them342Acceptance::Session.supported_platform?(payload_config) &&343Acceptance::Session.supported_platform?(module_test) &&344# Skip tests that are explicitly skipped, or won't pass in the current environment345!Acceptance::Session.skipped_module_test?(module_test, allure_test_environment)346),347# test metadata - will appear in allure report348module_test: module_test[:name]349) do350begin351replication_commands = []352current_payload_status = ''353354known_failures = module_test.dig(:lines, :all, :known_failures) || []355known_failures += module_test.dig(:lines, current_platform, :known_failures) || []356known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }357358required_lines = module_test.dig(:lines, :all, :required) || []359required_lines += module_test.dig(:lines, current_platform, :required) || []360required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }361362# 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 dies363payload_process, session_id = payload_process_and_session_id364365expect(payload_process).to(be_alive, proc do366current_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}"367368Allure.add_attachment(369name: 'Failed payload blob',370source: Base64.strict_encode64(File.binread(payload_process.payload_path)),371type: Allure::ContentType::TXT372)373374current_payload_status375end)376expect(session_id).to_not(be_nil, proc do377"There should be a session present"378end)379380use_module = "use #{module_test[:name]}"381run_module = "run session=#{session_id} AddEntropy=true Verbose=true"382383replication_commands << use_module384console.sendline(use_module)385console.recvuntil(Acceptance::Console.prompt)386387replication_commands << run_module388console.sendline(run_module)389390# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:391# console.interact392393# Expect the test module to complete394test_result = console.recvuntil('Post module execution completed')395396# Ensure there are no failures, and assert tests are complete397aggregate_failures("#{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests") do398# Skip any ignored lines from the validation input399validated_lines = test_result.lines.reject do |line|400is_acceptable = known_failures.any? do |acceptable_failure|401line.include?(acceptable_failure.value) &&402acceptable_failure.if?(test_environment)403end || line.match?(/Passed: \d+; Failed: \d+/)404405is_acceptable406end407408validated_lines.each do |test_line|409test_line = Acceptance::Session.uncolorize(test_line)410expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"411end412413# Assert all expected lines are present414required_lines.each do |required|415next unless required.if?(test_environment)416417expect(test_result).to include(required.value)418end419420# Assert all ignored lines are present, if they are not present - they should be removed from421# the calling config422known_failures.each do |acceptable_failure|423next if acceptable_failure.flaky?(test_environment)424next unless acceptable_failure.if?(test_environment)425426expect(test_result).to include(acceptable_failure.value)427end428end429rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e430test_run_error = e431end432433# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are434# still generated if the session dies in a weird way etc435436# Payload process cleanup / verification437# The payload process wasn't initially marked as dead - let's close it438if payload_process.present? && current_payload_status.blank?439begin440if payload_process.alive?441current_payload_status = "Process still alive after running test suite"442payload_process.close443else444current_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}"445end446rescue => e447Allure.add_attachment(448name: 'driver.close_payloads failure information',449source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",450type: Allure::ContentType::TXT451)452end453end454455console_reset_error = nil456current_console_data = console.all_data457begin458console.reset459rescue => e460console_reset_error = e461Allure.add_attachment(462name: 'console.reset failure information',463source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",464type: Allure::ContentType::TXT465)466end467468payload_configuration_details = payload.as_readable_text(469default_global_datastore: default_global_datastore,470default_module_datastore: default_module_datastore471)472473replication_steps = <<~EOF474## Load test modules475loadpath test/modules476477#{payload_configuration_details}478479## Replication commands480#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}481EOF482483Allure.add_attachment(484name: 'payload configuration and replication',485source: replication_steps,486type: Allure::ContentType::TXT487)488489Allure.add_attachment(490name: 'payload output if available',491source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}",492type: Allure::ContentType::TXT493)494495Allure.add_attachment(496name: 'payload debug log if available',497source: get_file_attachment_contents(meterpreter_logging_file.path),498type: Allure::ContentType::TXT499)500501Allure.add_attachment(502name: 'session tlv logging if available',503source: get_file_attachment_contents(session_tlv_logging_file.path),504type: Allure::ContentType::TXT505)506507Allure.add_attachment(508name: 'console data',509source: current_console_data,510type: Allure::ContentType::TXT511)512513test_assertions = JSON.pretty_generate(514{515required_lines: required_lines.map(&:to_h),516known_failures: known_failures.map(&:to_h),517}518)519Allure.add_attachment(520name: 'test assertions',521source: test_assertions,522type: Allure::ContentType::TXT523)524525raise test_run_error if test_run_error526raise console_reset_error if console_reset_error527end528end529end530end531end532end533end534end535end536537538