Path: blob/master/spec/acceptance/smb_spec.rb
19721 views
require 'acceptance_spec_helper'12RSpec.describe 'SMB sessions and SMB modules' do3include_context 'wait_for_expect'45tests = {6smb: {7target: {8session_module: "auxiliary/scanner/smb/smb_login",9type: 'SMB',10platforms: [:linux, :osx, :windows],11datastore: {12global: {},13module: {14username: ENV.fetch('SMB_USERNAME', 'acceptance_tests_user'),15password: ENV.fetch('SMB_PASSWORD', 'acceptance_tests_password'),16rhost: ENV.fetch('SMB_RHOST', '127.0.0.1'),17rport: ENV.fetch('SMB_RPORT', '445'),18}19}20},21module_tests: [22{23name: "post/test/smb",24platforms: [:linux, :osx, :windows],25targets: [:session],26skipped: false,27},28{29name: "auxiliary/scanner/smb/smb_lookupsid",30platforms: [:linux, :osx, :windows],31targets: [:session, :rhost],32skipped: false,33lines: {34all: {35required: [36"PIPE(lsarpc) LOCAL",37/User( *)(Administrator|nobody)/,38/Group( *)(None|Domain (Admins|Users|Guests|Computers))/,39],40},41}42},43{44name: "auxiliary/scanner/smb/smb_enumusers",45platforms: [:linux, :osx, :windows],46targets: [:session, :rhost],47skipped: false,48lines: {49all: {50required: [51"acceptance_tests_user",52],53},54}55},56{57name: "auxiliary/scanner/smb/pipe_auditor",58platforms: [:linux, :osx, :windows],59targets: [:session, :rhost],60skipped: false,61lines: {62all: {63required: [64/Pipes: (\\([a-zA-Z]*)(, )?)*/,65],66known_failures: [67/Inaccessible named pipe:/,68/The server responded with an unexpected status code: STATUS_OBJECT_NAME_NOT_FOUND/,69]70},71}72},73{74name: "auxiliary/scanner/smb/smb_enumshares",75platforms: [:linux, :osx, :windows],76targets: [:session, :rhost],77skipped: false,78lines: {79all: {80required: [81"modifiable - (DISK)",82"readonly - (DISK)",83"IPC$ - (IPC|SPECIAL) IPC Service",84],85},86}87},88]89}90}9192allure_test_environment = AllureRspec.configuration.environment_properties9394let_it_be(:current_platform) { Acceptance::Session::current_platform }9596# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly97let_it_be(:driver) do98driver = Acceptance::ConsoleDriver.new99driver100end101102# Opens a test console with the test loadpath specified103# @!attribute [r] console104# @return [Acceptance::Console]105let_it_be(:console) do106console = driver.open_console107108# Load the test modules109console.sendline('loadpath test/modules')110console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)111console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)112console.recvuntil(/\d+ exploit modules[^\n]*\n/)113console.recvuntil(/\d+ post modules[^\n]*\n/)114console.recvuntil(Acceptance::Console.prompt)115116# Read the remaining console117# console.sendline "quit -y"118# console.recv_available119120features = %w[121smb_session_type122]123124features.each do |feature|125console.sendline("features set #{feature} true")126console.recvuntil(Acceptance::Console.prompt)127end128129console130end131132# Run the given block in a 'test harness' which will handle all of the boilerplate for asserting module results, cleanup, and artifact tracking133# This doesn't happen in a before/after block to ensure that allure's report generation is correctly attached to the correct test scope134def with_test_harness(module_test)135begin136replication_commands = []137138known_failures = module_test.dig(:lines, :all, :known_failures) || []139known_failures += module_test.dig(:lines, current_platform, :known_failures) || []140known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }141142required_lines = module_test.dig(:lines, :all, :required) || []143required_lines += module_test.dig(:lines, current_platform, :required) || []144required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }145146yield replication_commands147148# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:149# console.interact150151# Expect the test module to complete152module_type = module_test[:name].split('/').first153test_result = console.recvuntil("#{module_type.capitalize} module execution completed")154155# Ensure there are no failures, and assert tests are complete156aggregate_failures("#{target.type} target and passes the #{module_test[:name].inspect} tests") do157# Skip any ignored lines from the validation input158validated_lines = test_result.lines.reject do |line|159is_acceptable = known_failures.any? do |acceptable_failure|160is_matching_line = acceptable_failure.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value)161is_matching_line &&162acceptable_failure.if?(test_environment)163end || line.match?(/Passed: \d+; Failed: \d+/)164165is_acceptable166end167168validated_lines.each do |test_line|169test_line = Acceptance::Session.uncolorize(test_line)170expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"171end172173# Assert all expected lines are present174required_lines.each do |required|175next unless required.if?(test_environment)176if required.value.is_a?(Regexp)177expect(test_result).to match(required.value)178else179expect(test_result).to include(required.value)180end181end182183# Assert all ignored lines are present, if they are not present - they should be removed from184# the calling config185known_failures.each do |acceptable_failure|186next if acceptable_failure.flaky?(test_environment)187next unless acceptable_failure.if?(test_environment)188189if acceptable_failure.value.is_a?(Regexp)190expect(test_result).to match(acceptable_failure.value)191else192expect(test_result).to include(acceptable_failure.value)193end194end195end196rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e197test_run_error = e198end199200# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are201# still generated if the session dies in a weird way etc202203console_reset_error = nil204current_console_data = console.all_data205begin206console.reset207rescue => e208console_reset_error = e209Allure.add_attachment(210name: 'console.reset failure information',211source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",212type: Allure::ContentType::TXT213)214end215216target_configuration_details = target.as_readable_text(217default_global_datastore: default_global_datastore,218default_module_datastore: default_module_datastore219)220221replication_steps = <<~EOF222## Load test modules223loadpath test/modules224225#{target_configuration_details}226227## Replication commands228#{replication_commands.empty? ? 'no additional commands run' : replication_commands.join("\n")}229EOF230231Allure.add_attachment(232name: 'payload configuration and replication',233source: replication_steps,234type: Allure::ContentType::TXT235)236237Allure.add_attachment(238name: 'console data',239source: current_console_data,240type: Allure::ContentType::TXT241)242243test_assertions = JSON.pretty_generate(244{245required_lines: required_lines.map(&:to_h),246known_failures: known_failures.map(&:to_h),247}248)249Allure.add_attachment(250name: 'test assertions',251source: test_assertions,252type: Allure::ContentType::TXT253)254255raise test_run_error if test_run_error256raise console_reset_error if console_reset_error257end258259tests.each do |runtime_name, test_config|260runtime_name = "#{runtime_name}#{ENV.fetch('RUNTIME_VERSION', '')}"261262describe "#{Acceptance::Session.current_platform}/#{runtime_name}", focus: test_config[:focus] do263test_config[:module_tests].each do |module_test|264describe(265module_test[:name],266if: (267Acceptance::Session.supported_platform?(module_test)268)269) do270let(:target) { Acceptance::Target.new(test_config[:target]) }271272let(:default_global_datastore) do273{274}275end276277let(:test_environment) { allure_test_environment }278279let(:default_module_datastore) do280{281lhost: '127.0.0.1'282}283end284285# The shared session id that will be reused across the test run286let(:session_id) do287console.sendline "use #{target.session_module}"288console.recvuntil(Acceptance::Console.prompt)289290# Set global options291console.sendline target.setg_commands(default_global_datastore: default_global_datastore)292console.recvuntil(Acceptance::Console.prompt)293294console.sendline target.run_command(default_module_datastore: { PASS_FILE: nil, USER_FILE: nil, CreateSession: true })295296session_id = nil297# Wait for the session to open, or break early if the payload is detected as dead298wait_for_expect do299session_opened_matcher = /#{target.type} session (\d+) opened[^\n]*\n/300session_message = ''301begin302session_message = console.recvuntil(session_opened_matcher, timeout: 1)303rescue Acceptance::ChildProcessRecvError304# noop305end306307session_id = session_message[session_opened_matcher, 1]308expect(session_id).to_not be_nil309end310311session_id312end313314before :each do |example|315next unless example.respond_to?(:parameter)316317# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI318test_environment.each do |key, value|319example.parameter(key, value)320end321end322323after :all do324driver.close_payloads325console.reset326end327328context "when targeting a session", if: module_test[:targets].include?(:session) do329it(330"#{Acceptance::Session.current_platform}/#{runtime_name} session opens and passes the #{module_test[:name].inspect} tests"331) do332with_test_harness(module_test) do |replication_commands|333# 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 dies334expect(session_id).to_not(be_nil, proc do335"There should be a session present"336end)337338use_module = "use #{module_test[:name]}"339run_module = "run session=#{session_id} Verbose=true"340341replication_commands << use_module342console.sendline(use_module)343console.recvuntil(Acceptance::Console.prompt)344345replication_commands << run_module346console.sendline(run_module)347348# Assertions will happen after this block ends349end350end351end352353context "when targeting an rhost", if: module_test[:targets].include?(:rhost) do354it(355"#{Acceptance::Session.current_platform}/#{runtime_name} rhost opens and passes the #{module_test[:name].inspect} tests"356) do357with_test_harness(module_test) do |replication_commands|358use_module = "use #{module_test[:name]}"359run_module = "run #{target.datastore_options(default_module_datastore: default_module_datastore)} Verbose=true"360361replication_commands << use_module362console.sendline(use_module)363console.recvuntil(Acceptance::Console.prompt)364365replication_commands << run_module366console.sendline(run_module)367368# Assertions will happen after this block ends369end370end371end372end373end374end375end376end377378379