123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- 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
|