Path: blob/master/spec/acceptance/ldap_spec.rb
19612 views
require 'acceptance_spec_helper'12RSpec.describe 'LDAP modules' do3include_context 'wait_for_expect'45tests = {6ldap: {7target: {8session_module: 'auxiliary/scanner/ldap/ldap_login',9type: 'LDAP',10platforms: %i[linux osx windows],11datastore: {12global: {},13module: {14ldapusername: ENV.fetch('LDAP_LDAPUsername', "'DEV-AD\\Administrator'"),15ldappassword: ENV.fetch('LDAP_LDAPPassword', 'admin123!'),16rhost: ENV.fetch('LDAP_RHOST', '127.0.0.1'),17rport: ENV.fetch('LDAP_RPORT', '389'),18ssl: ENV.fetch('LDAP_SSL', 'false')19}20}21},22module_tests: [23{24name: 'auxiliary/gather/ldap_query',25platforms: %i[linux osx windows],26targets: [:session, :rhost],27skipped: false,28action: 'run_query_file',29datastore: { QUERY_FILE_PATH: 'data/auxiliary/gather/ldap_query/ldap_queries_default.yaml' },30lines: {31all: {32required: [33/Loading queries from/,34/ldap_queries_default.yaml.../,35/Discovered base DN/,36/Running ENUM_ACCOUNTS.../,37/Running ENUM_USER_SPNS_KERBEROAST.../,38/Running ENUM_USER_PASSWORD_NOT_REQUIRED.../,39]40}41}42},43{44name: 'auxiliary/gather/ldap_query',45platforms: %i[linux osx windows],46targets: [:session, :rhost],47skipped: false,48action: 'enum_accounts',49lines: {50all: {51required: [52/Discovered base DN/,53/Query returned 5 results/54]55}56}57},58{59name: 'auxiliary/gather/ldap_passwords',60platforms: %i[linux osx windows],61targets: [:session, :rhost],62skipped: false,63lines: {64all: {65required: [66/Searching base DN: DC=ldap,DC=example,DC=com/,67/Checking if the target LDAP server is an Active Directory Domain Controller.../,68/The target LDAP server is not an Active Directory Domain Controller./,69/Credentials \(password\) found in ms-mcs-admpwd: Administrator:\[LAPSv1\]SuperSecretPassword!/,70/Credentials \(password\) found in mslaps-password: Administrator:\[LAPSv2\]SuperSecretPassword!/,71/Found [1-9]\d* entries and [1-9]\d* credentials in 'DC=ldap,DC=example,DC=com'./72]73}74}75},76{77name: 'auxiliary/admin/ldap/shadow_credentials',78platforms: %i[linux osx windows],79targets: [:session, :rhost],80skipped: false,81datastore: { TARGET_USER: 'administrator' },82lines: {83all: {84required: [85/Discovered base DN: DC=ldap,DC=example,DC=com/,86/The msDS-KeyCredentialLink field is empty./87]88}89}90},91{92name: 'auxiliary/gather/ldap_esc_vulnerable_cert_finder',93platforms: %i[linux osx windows],94targets: [:session, :rhost],95skipped: false,96lines: {97all: {98required: [99/Successfully queried/100]101},102linux: {103known_failures: [104/Auxiliary aborted due to failure: not-found/105]106}107}108},109{110name: 'auxiliary/admin/ldap/rbcd',111platforms: %i[linux osx windows],112targets: [:session, :rhost],113skipped: false,114datastore: { DELEGATE_TO: 'administrator' },115lines: {116all: {117required: [118/The msDS-AllowedToActOnBehalfOfOtherIdentity field is empty./119]120}121}122},123]124}125}126127allure_test_environment = AllureRspec.configuration.environment_properties128129let_it_be(:current_platform) { Acceptance::Session.current_platform }130131# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly132let_it_be(:driver) do133driver = Acceptance::ConsoleDriver.new134driver135end136137# Opens a test console with the test loadpath specified138# @!attribute [r] console139# @return [Acceptance::Console]140let_it_be(:console) do141console = driver.open_console142143# Load the test modules144console.sendline('loadpath test/modules')145console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)146console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)147console.recvuntil(/\d+ exploit modules[^\n]*\n/)148console.recvuntil(/\d+ post modules[^\n]*\n/)149console.recvuntil(Acceptance::Console.prompt)150151# Read the remaining console152# console.sendline "quit -y"153# console.recv_available154155features = %w[156ldap_session_type157]158159features.each do |feature|160console.sendline("features set #{feature} true")161console.recvuntil(Acceptance::Console.prompt)162end163164console165end166167# Run the given block in a 'test harness' which will handle all of the boilerplate for asserting module results, cleanup, and artifact tracking168# This doesn't happen in a before/after block to ensure that allure's report generation is correctly attached to the correct test scope169def with_test_harness(module_test)170begin171replication_commands = []172173known_failures = module_test.dig(:lines, :all, :known_failures) || []174known_failures += module_test.dig(:lines, current_platform, :known_failures) || []175known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }176177required_lines = module_test.dig(:lines, :all, :required) || []178required_lines += module_test.dig(:lines, current_platform, :required) || []179required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }180181yield replication_commands182183# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:184# console.interact185186# Expect the test module to complete187module_type = module_test[:name].split('/').first188test_result = console.recvuntil("#{module_type.capitalize} module execution completed")189190# Ensure there are no failures, and assert tests are complete191aggregate_failures("#{target.type} target and passes the #{module_test[:name].inspect} tests") do192# Skip any ignored lines from the validation input193validated_lines = test_result.lines.reject do |line|194is_acceptable = known_failures.any? do |acceptable_failure|195is_matching_line = acceptable_failure.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value)196is_matching_line &&197acceptable_failure.if?(test_environment)198end || line.match?(/Passed: \d+; Failed: \d+/)199200is_acceptable201end202203validated_lines.each do |test_line|204test_line = Acceptance::Session.uncolorize(test_line)205expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"206end207208# Assert all expected lines are present209required_lines.each do |required|210next unless required.if?(test_environment)211212if required.value.is_a?(Regexp)213expect(test_result).to match(required.value)214else215expect(test_result).to include(required.value)216end217end218219# Assert all ignored lines are present, if they are not present - they should be removed from220# the calling config221known_failures.each do |acceptable_failure|222next if acceptable_failure.flaky?(test_environment)223next unless acceptable_failure.if?(test_environment)224225expect(test_result).to include(acceptable_failure.value)226end227end228rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e229test_run_error = e230end231232# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are233# still generated if the session dies in a weird way etc234235console_reset_error = nil236current_console_data = console.all_data237begin238console.reset239rescue StandardError => e240console_reset_error = e241Allure.add_attachment(242name: 'console.reset failure information',243source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",244type: Allure::ContentType::TXT245)246end247248target_configuration_details = target.as_readable_text(249default_global_datastore: default_global_datastore,250default_module_datastore: default_module_datastore251)252253replication_steps = <<~EOF254## Load test modules255loadpath test/modules256257#{target_configuration_details}258259## Replication commands260#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}261EOF262263Allure.add_attachment(264name: 'payload configuration and replication',265source: replication_steps,266type: Allure::ContentType::TXT267)268269Allure.add_attachment(270name: 'console data',271source: current_console_data,272type: Allure::ContentType::TXT273)274275test_assertions = JSON.pretty_generate(276{277required_lines: required_lines.map(&:to_h),278known_failures: known_failures.map(&:to_h)279}280)281Allure.add_attachment(282name: 'test assertions',283source: test_assertions,284type: Allure::ContentType::TXT285)286287raise test_run_error if test_run_error288raise console_reset_error if console_reset_error289end290291tests.each do |runtime_name, test_config|292runtime_name = "#{runtime_name}#{ENV.fetch('RUNTIME_VERSION', '')}"293294describe "#{Acceptance::Session.current_platform}/#{runtime_name}", focus: test_config[:focus] do295test_config[:module_tests].each do |module_test|296describe(297module_test[:name],298if:299Acceptance::Session.supported_platform?(module_test)300) do301let(:target) { Acceptance::Target.new(test_config[:target]) }302303let(:default_global_datastore) do304{}305end306307let(:test_environment) { allure_test_environment }308309let(:default_module_datastore) do310{311lhost: '127.0.0.1'312}313end314315# The shared session id that will be reused across the test run316let(:session_id) do317console.sendline "use #{target.session_module}"318console.recvuntil(Acceptance::Console.prompt)319320# Set global options321console.sendline target.setg_commands(default_global_datastore: default_global_datastore)322console.recvuntil(Acceptance::Console.prompt)323# TODO: update this when we add sessions324console.sendline target.run_command(default_module_datastore: { PASS_FILE: nil, USER_FILE: nil, CreateSession: true })325326session_id = nil327# Wait for the session to open, or break early if the payload is detected as dead328wait_for_expect do329session_opened_matcher = /#{target.type} session (\d+) opened[^\n]*\n/330session_message = ''331begin332session_message = console.recvuntil(session_opened_matcher, timeout: 1)333rescue Acceptance::ChildProcessRecvError334# noop335end336337session_id = session_message[session_opened_matcher, 1]338expect(session_id).to_not be_nil339end340341session_id342end343344before :each do |example|345next unless example.respond_to?(:parameter)346347# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI348test_environment.each do |key, value|349example.parameter(key, value)350end351end352353after :all do354driver.close_payloads355console.reset356end357358context 'when targeting a session', if: module_test[:targets].include?(:session) do359it(360"#{Acceptance::Session.current_platform}/#{runtime_name} session opens and passes the #{module_test[:name].inspect} tests"361) do362with_test_harness(module_test) do |replication_commands|363# 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 dies364expect(session_id).to_not(be_nil, proc do365'There should be a session present'366end)367368use_module = "use #{module_test[:name]}"369run_command = module_test.key?(:action) ? module_test.fetch(:action) : 'run'370run_module = "#{run_command} session=#{session_id} #{target.datastore_options(default_module_datastore: default_module_datastore.merge(module_test.fetch(:datastore, {})))} Verbose=true"371372373replication_commands << use_module374console.sendline(use_module)375console.recvuntil(Acceptance::Console.prompt)376377replication_commands << run_module378console.sendline(run_module)379380# Assertions will happen after this block ends381end382end383end384385context 'when targeting an rhost', if: module_test[:targets].include?(:rhost) do386it(387"#{Acceptance::Session.current_platform}/#{runtime_name} rhost opens and passes the #{module_test[:name].inspect} tests"388) do389with_test_harness(module_test) do |replication_commands|390use_module = "use #{module_test[:name]}"391run_command = module_test.key?(:action) ? module_test.fetch(:action) : 'run'392run_module = "#{run_command} #{target.datastore_options(default_module_datastore: default_module_datastore.merge(module_test.fetch(:datastore, {})))} Verbose=true"393394replication_commands << use_module395console.sendline(use_module)396console.recvuntil(Acceptance::Console.prompt)397398replication_commands << run_module399console.sendline(run_module)400401# Assertions will happen after this block ends402end403end404end405end406end407end408end409end410411412