require 'acceptance_spec_helper' RSpec.describe 'Postgres sessions and postgres modules' do include_context 'wait_for_expect' TESTS = { postgres: { target: { session_module: "auxiliary/scanner/postgres/postgres_login", type: 'PostgreSQL', platforms: [:linux, :osx, :windows], datastore: { global: {}, module: { username: ENV.fetch('POSTGRES_USERNAME', 'postgres'), password: ENV.fetch('POSTGRES_PASSWORD', 'password'), rhost: ENV.fetch('POSTGRES_RHOST', '127.0.0.1'), rport: ENV.fetch('POSTGRES_RPORT', '5432'), } } }, module_tests: [ { name: "post/test/postgres", platforms: [:linux, :osx, :windows], targets: [:session], skipped: false, }, { name: "auxiliary/scanner/postgres/postgres_hashdump", platforms: [:linux, :osx, :windows], targets: [:session, :rhost], skipped: false, lines: { all: { required: [ " Username Hash", " -------- ----", # postgres SCRAM-SHA-256$4096:UfTJGaMUW+DtXay1UUD+zA==$0C01mPHaruGTqKJFt5qdITvM+nwLsCgxukO3MIbKugU=:iNBXVE5Vqnoa+dGhmEGMQ0cy+nNXDOzg0F3YNcrtRyE= / postgres \w+/ ] }, } }, { name: "auxiliary/scanner/postgres/postgres_version", platforms: [:linux, :osx, :windows], targets: [:session, :rhost], skipped: false, lines: { all: { required: [ /Version PostgreSQL \d+.\d+/ ] }, } }, { name: "auxiliary/admin/postgres/postgres_readfile", platforms: [:linux], targets: [:session, :rhost], skipped: false, lines: { all: { # Module reads /etc/passwd by default: required: [ /root:x:\d+:\d+:root:/, /postgres:x:\d+:\d+::/ ] }, } }, { name: "auxiliary/admin/postgres/postgres_sql", platforms: [:linux, :osx, :windows], targets: [:session, :rhost], skipped: false, lines: { all: { required: [ # Default module query "Query Text: 'select version()'", # Result /PostgreSQL \d+.\d+/, ] }, } } ] } } TEST_ENVIRONMENT = AllureRspec.configuration.environment_properties let_it_be(:current_platform) { Acceptance::Meterpreter::current_platform } # Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly let_it_be(:driver) do driver = Acceptance::ConsoleDriver.new driver end # Opens a test console with the test loadpath specified # @!attribute [r] console # @return [Acceptance::Console] let_it_be(:console) do console = driver.open_console # Load the test modules console.sendline('loadpath test/modules') console.recvuntil(/Loaded \d+ modules:[^\n]*\n/) console.recvuntil(/\d+ auxiliary modules[^\n]*\n/) console.recvuntil(/\d+ exploit modules[^\n]*\n/) console.recvuntil(/\d+ post modules[^\n]*\n/) console.recvuntil(Acceptance::Console.prompt) # Read the remaining console # console.sendline "quit -y" # console.recv_available features = %w[ postgresql_session_type ] features.each do |feature| console.sendline("features set #{feature} true") console.recvuntil(Acceptance::Console.prompt) end console end # Run the given block in a 'test harness' which will handle all of the boilerplate for asserting module results, cleanup, and artifact tracking # This doesn't happen in a before/after block to ensure that allure's report generation is correctly attached to the correct test scope def with_test_harness(module_test) begin replication_commands = [] known_failures = module_test.dig(:lines, :all, :known_failures) || [] known_failures += module_test.dig(:lines, current_platform, :known_failures) || [] known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten } required_lines = module_test.dig(:lines, :all, :required) || [] required_lines += module_test.dig(:lines, current_platform, :required) || [] required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten } yield replication_commands # XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with: # console.interact # Expect the test module to complete module_type = module_test[:name].split('/').first test_result = console.recvuntil("#{module_type.capitalize} module execution completed") # Ensure there are no failures, and assert tests are complete aggregate_failures("#{target.type} target and passes the #{module_test[:name].inspect} tests") do # Skip any ignored lines from the validation input validated_lines = test_result.lines.reject do |line| is_acceptable = known_failures.any? do |acceptable_failure| is_matching_line = is_matching_line.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value) is_matching_line && acceptable_failure.if?(test_environment) end || line.match?(/Passed: \d+; Failed: \d+/) is_acceptable end validated_lines.each do |test_line| test_line = Acceptance::Meterpreter.uncolorize(test_line) expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}" end # Assert all expected lines are present required_lines.each do |required| next unless required.if?(test_environment) if required.value.is_a?(Regexp) expect(test_result).to match(required.value) else expect(test_result).to include(required.value) end end # Assert all ignored lines are present, if they are not present - they should be removed from # the calling config known_failures.each do |acceptable_failure| next if acceptable_failure.flaky?(test_environment) next unless acceptable_failure.if?(test_environment) expect(test_result).to include(acceptable_failure.value) end end rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e test_run_error = e end # Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are # still generated if the session dies in a weird way etc console_reset_error = nil current_console_data = console.all_data begin console.reset rescue => e console_reset_error = e Allure.add_attachment( name: 'console.reset failure information', source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}", type: Allure::ContentType::TXT ) end target_configuration_details = target.as_readable_text( default_global_datastore: default_global_datastore, default_module_datastore: default_module_datastore ) replication_steps = <<~EOF ## Load test modules loadpath test/modules #{target_configuration_details} ## Replication commands #{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")} EOF Allure.add_attachment( name: 'payload configuration and replication', source: replication_steps, type: Allure::ContentType::TXT ) Allure.add_attachment( name: 'console data', source: current_console_data, type: Allure::ContentType::TXT ) test_assertions = JSON.pretty_generate( { required_lines: required_lines.map(&:to_h), known_failures: known_failures.map(&:to_h), } ) Allure.add_attachment( name: 'test assertions', source: test_assertions, type: Allure::ContentType::TXT ) raise test_run_error if test_run_error raise console_reset_error if console_reset_error end TESTS.each do |runtime_name, test_config| runtime_name = "#{runtime_name}#{ENV.fetch('RUNTIME_VERSION', '')}" describe "#{Acceptance::Meterpreter.current_platform}/#{runtime_name}", focus: test_config[:focus] do test_config[:module_tests].each do |module_test| describe( module_test[:name], if: ( Acceptance::Meterpreter.supported_platform?(module_test) ) ) do let(:target) { Acceptance::Target.new(test_config[:target]) } let(:default_global_datastore) do { } end let(:test_environment) { TEST_ENVIRONMENT } let(:default_module_datastore) do { lhost: '127.0.0.1' } end # The shared session id that will be reused across the test run let(:session_id) do console.sendline "use #{target.session_module}" console.recvuntil(Acceptance::Console.prompt) # Set global options console.sendline target.setg_commands(default_global_datastore: default_global_datastore) console.recvuntil(Acceptance::Console.prompt) console.sendline target.run_command(default_module_datastore: { PASS_FILE: nil, USER_FILE: nil, CreateSession: true }) session_id = nil # Wait for the session to open, or break early if the payload is detected as dead wait_for_expect do session_opened_matcher = /#{target.type} session (\d+) opened[^\n]*\n/ session_message = '' begin session_message = console.recvuntil(session_opened_matcher, timeout: 1) rescue Acceptance::ChildProcessRecvError # noop end session_id = session_message[session_opened_matcher, 1] expect(session_id).to_not be_nil end session_id end before :each do |example| next unless example.respond_to?(:parameter) # Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI test_environment.each do |key, value| example.parameter(key, value) end end after :all do driver.close_payloads console.reset end context "when targeting a session", if: module_test[:targets].include?(:session) do it( "#{Acceptance::Meterpreter.current_platform}/#{runtime_name} session opens and passes the #{module_test[:name].inspect} tests" ) do with_test_harness(module_test) do |replication_commands| # 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 dies expect(session_id).to_not(be_nil, proc do "There should be a session present" end) use_module = "use #{module_test[:name]}" run_module = "run session=#{session_id} Verbose=true" replication_commands << use_module console.sendline(use_module) console.recvuntil(Acceptance::Console.prompt) replication_commands << run_module console.sendline(run_module) # Assertions will happen after this block ends end end end context "when targeting an rhost", if: module_test[:targets].include?(:rhost) do it( "#{Acceptance::Meterpreter.current_platform}/#{runtime_name} rhost opens and passes the #{module_test[:name].inspect} tests" ) do with_test_harness(module_test) do |replication_commands| use_module = "use #{module_test[:name]}" run_module = "run #{target.datastore_options(default_module_datastore: default_module_datastore)} Verbose=true" replication_commands << use_module console.sendline(use_module) console.recvuntil(Acceptance::Console.prompt) replication_commands << run_module console.sendline(run_module) # Assertions will happen after this block ends end end end end end end end end