Path: blob/master/spec/acceptance/ldap_spec.rb
28292 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/Credential found in ms-mcs-admpwd: Administrator:\[LAPSv1\]SuperSecretPassword!/,70/Credential 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/admin/ldap/rbcd',93platforms: %i[linux osx windows],94targets: [:session, :rhost],95skipped: false,96datastore: { DELEGATE_TO: 'administrator' },97lines: {98all: {99required: [100/The msDS-AllowedToActOnBehalfOfOtherIdentity field is empty./101]102}103}104},105]106}107}108109allure_test_environment = AllureRspec.configuration.environment_properties110111let_it_be(:current_platform) { Acceptance::Session.current_platform }112113# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly114let_it_be(:driver) do115driver = Acceptance::ConsoleDriver.new116driver117end118119# Opens a test console with the test loadpath specified120# @!attribute [r] console121# @return [Acceptance::Console]122let_it_be(:console) do123console = driver.open_console124125# Load the test modules126console.sendline('loadpath test/modules')127console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)128console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)129console.recvuntil(/\d+ exploit modules[^\n]*\n/)130console.recvuntil(/\d+ post modules[^\n]*\n/)131console.recvuntil(Acceptance::Console.prompt)132133# Read the remaining console134# console.sendline "quit -y"135# console.recv_available136137features = %w[138ldap_session_type139]140141features.each do |feature|142console.sendline("features set #{feature} true")143console.recvuntil(Acceptance::Console.prompt)144end145146console147end148149# Run the given block in a 'test harness' which will handle all of the boilerplate for asserting module results, cleanup, and artifact tracking150# This doesn't happen in a before/after block to ensure that allure's report generation is correctly attached to the correct test scope151def with_test_harness(module_test)152begin153replication_commands = []154155known_failures = module_test.dig(:lines, :all, :known_failures) || []156known_failures += module_test.dig(:lines, current_platform, :known_failures) || []157known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }158159required_lines = module_test.dig(:lines, :all, :required) || []160required_lines += module_test.dig(:lines, current_platform, :required) || []161required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }162163yield replication_commands164165# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:166# console.interact167168# Expect the test module to complete169module_type = module_test[:name].split('/').first170test_result = console.recvuntil("#{module_type.capitalize} module execution completed")171172# Ensure there are no failures, and assert tests are complete173aggregate_failures("#{target.type} target and passes the #{module_test[:name].inspect} tests") do174# Skip any ignored lines from the validation input175validated_lines = test_result.lines.reject do |line|176is_acceptable = known_failures.any? do |acceptable_failure|177is_matching_line = acceptable_failure.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value)178is_matching_line &&179acceptable_failure.if?(test_environment)180end || line.match?(/Passed: \d+; Failed: \d+/)181182is_acceptable183end184185validated_lines.each do |test_line|186test_line = Acceptance::Session.uncolorize(test_line)187expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"188end189190# Assert all expected lines are present191required_lines.each do |required|192next unless required.if?(test_environment)193194if required.value.is_a?(Regexp)195expect(test_result).to match(required.value)196else197expect(test_result).to include(required.value)198end199end200201# Assert all ignored lines are present, if they are not present - they should be removed from202# the calling config203known_failures.each do |acceptable_failure|204next if acceptable_failure.flaky?(test_environment)205next unless acceptable_failure.if?(test_environment)206207expect(test_result).to include(acceptable_failure.value)208end209end210rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e211test_run_error = e212end213214# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are215# still generated if the session dies in a weird way etc216217console_reset_error = nil218current_console_data = console.all_data219begin220console.reset221rescue StandardError => e222console_reset_error = e223Allure.add_attachment(224name: 'console.reset failure information',225source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",226type: Allure::ContentType::TXT227)228end229230target_configuration_details = target.as_readable_text(231default_global_datastore: default_global_datastore,232default_module_datastore: default_module_datastore233)234235replication_steps = <<~EOF236## Load test modules237loadpath test/modules238239#{target_configuration_details}240241## Replication commands242#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}243EOF244245Allure.add_attachment(246name: 'payload configuration and replication',247source: replication_steps,248type: Allure::ContentType::TXT249)250251Allure.add_attachment(252name: 'console data',253source: current_console_data,254type: Allure::ContentType::TXT255)256257test_assertions = JSON.pretty_generate(258{259required_lines: required_lines.map(&:to_h),260known_failures: known_failures.map(&:to_h)261}262)263Allure.add_attachment(264name: 'test assertions',265source: test_assertions,266type: Allure::ContentType::TXT267)268269raise test_run_error if test_run_error270raise console_reset_error if console_reset_error271end272273tests.each do |runtime_name, test_config|274runtime_name = "#{runtime_name}#{ENV.fetch('RUNTIME_VERSION', '')}"275276describe "#{Acceptance::Session.current_platform}/#{runtime_name}", focus: test_config[:focus] do277test_config[:module_tests].each do |module_test|278describe(279module_test[:name],280if:281Acceptance::Session.supported_platform?(module_test)282) do283let(:target) { Acceptance::Target.new(test_config[:target]) }284285let(:default_global_datastore) do286{}287end288289let(:test_environment) { allure_test_environment }290291let(:default_module_datastore) do292{293lhost: '127.0.0.1'294}295end296297# The shared session id that will be reused across the test run298let(:session_id) do299console.sendline "use #{target.session_module}"300console.recvuntil(Acceptance::Console.prompt)301302# Set global options303console.sendline target.setg_commands(default_global_datastore: default_global_datastore)304console.recvuntil(Acceptance::Console.prompt)305# TODO: update this when we add sessions306console.sendline target.run_command(default_module_datastore: { PASS_FILE: nil, USER_FILE: nil, CreateSession: true })307308session_id = nil309# Wait for the session to open, or break early if the payload is detected as dead310wait_for_expect do311session_opened_matcher = /#{target.type} session (\d+) opened[^\n]*\n/312session_message = ''313begin314session_message = console.recvuntil(session_opened_matcher, timeout: 1)315rescue Acceptance::ChildProcessRecvError316# noop317end318319session_id = session_message[session_opened_matcher, 1]320expect(session_id).to_not be_nil321end322323session_id324end325326before :each do |example|327next unless example.respond_to?(:parameter)328329# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI330test_environment.each do |key, value|331example.parameter(key, value)332end333end334335after :all do336driver.close_payloads337console.reset338end339340context 'when targeting a session', if: module_test[:targets].include?(:session) do341it(342"#{Acceptance::Session.current_platform}/#{runtime_name} session opens and passes the #{module_test[:name].inspect} tests"343) do344with_test_harness(module_test) do |replication_commands|345# 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 dies346expect(session_id).to_not(be_nil, proc do347'There should be a session present'348end)349350use_module = "use #{module_test[:name]}"351run_command = module_test.key?(:action) ? module_test.fetch(:action) : 'run'352run_module = "#{run_command} session=#{session_id} #{target.datastore_options(default_module_datastore: default_module_datastore.merge(module_test.fetch(:datastore, {})))} Verbose=true"353354355replication_commands << use_module356console.sendline(use_module)357console.recvuntil(Acceptance::Console.prompt)358359replication_commands << run_module360console.sendline(run_module)361362# Assertions will happen after this block ends363end364end365end366367context 'when targeting an rhost', if: module_test[:targets].include?(:rhost) do368it(369"#{Acceptance::Session.current_platform}/#{runtime_name} rhost opens and passes the #{module_test[:name].inspect} tests"370) do371with_test_harness(module_test) do |replication_commands|372use_module = "use #{module_test[:name]}"373run_command = module_test.key?(:action) ? module_test.fetch(:action) : 'run'374run_module = "#{run_command} #{target.datastore_options(default_module_datastore: default_module_datastore.merge(module_test.fetch(:datastore, {})))} Verbose=true"375376replication_commands << use_module377console.sendline(use_module)378console.recvuntil(Acceptance::Console.prompt)379380replication_commands << run_module381console.sendline(run_module)382383# Assertions will happen after this block ends384end385end386end387end388end389end390end391end392393394