Add SMB acceptance tests
This commit is contained in:
parent
c576db98b5
commit
bf50da5e8f
|
@ -0,0 +1,166 @@
|
|||
name: Acceptance
|
||||
|
||||
# Optional, enabling concurrency limits: https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
#concurrency:
|
||||
# group: ${{ github.ref }}-${{ github.workflow }}
|
||||
# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: none
|
||||
deployments: none
|
||||
id-token: none
|
||||
issues: none
|
||||
discussions: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- gh-pages
|
||||
- metakitty
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- 'metsploit-framework.gemspec'
|
||||
- 'Gemfile.lock'
|
||||
- '**/**smb**'
|
||||
- 'spec/acceptance/**'
|
||||
- 'spec/support/acceptance/**'
|
||||
- 'spec/acceptance_spec_helper.rb'
|
||||
# Example of running as a cron, to weed out flaky tests
|
||||
# schedule:
|
||||
# - cron: '*/15 * * * *'
|
||||
|
||||
jobs:
|
||||
smb:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 40
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.2'
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
SMB_USERNAME: acceptance_tests_user
|
||||
SMB_PASSWORD: acceptance_tests_password
|
||||
|
||||
name: SMB Acceptance - ${{ matrix.os }} - Ruby ${{ matrix.ruby }}
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get install -y --no-install-recommends libpcap-dev graphviz
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run docker container
|
||||
working-directory: 'test/smb'
|
||||
run: |
|
||||
docker compose build
|
||||
docker compose up --wait -d
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
BUNDLE_WITHOUT: "coverage development pcap"
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: "${{ contains(matrix.ruby, 'preview') && 'true' || 'false' }}"
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '${{ matrix.ruby }}'
|
||||
bundler-cache: true
|
||||
|
||||
- name: acceptance
|
||||
env:
|
||||
SPEC_HELPER_LOAD_METASPLOIT: false
|
||||
SPEC_OPTS: "--tag acceptance --require acceptance_spec_helper.rb --color --format documentation --format AllureRspec::RSpecFormatter"
|
||||
RUNTIME_VERSION: 'latest'
|
||||
# Unix run command:
|
||||
# SPEC_HELPER_LOAD_METASPLOIT=false bundle exec ./spec/acceptance
|
||||
# Windows cmd command:
|
||||
# set SPEC_HELPER_LOAD_METASPLOIT=false
|
||||
# bundle exec rspec .\spec\acceptance
|
||||
# Note: rspec retry is intentionally not used, as it can cause issues with allure's reporting
|
||||
# Additionally - flakey tests should be fixed or marked as flakey instead of silently retried
|
||||
run: |
|
||||
bundle exec rspec spec/acceptance/smb_spec.rb
|
||||
|
||||
- name: Archive results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Provide a unique artifact for each matrix os, otherwise race conditions can lead to corrupt zips
|
||||
name: smb_acceptance-${{ matrix.os }}
|
||||
path: tmp/allure-raw-data
|
||||
|
||||
# Generate a final report from the previous test results
|
||||
report:
|
||||
name: Generate report
|
||||
needs:
|
||||
- smb
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
if: always()
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
BUNDLE_WITHOUT: "coverage development"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: true
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '${{ matrix.ruby }}'
|
||||
bundler-cache: true
|
||||
cache-version: 4
|
||||
# Github actions with Ruby requires Bundler 2.2.18+
|
||||
# https://github.com/ruby/setup-ruby/tree/d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c#windows
|
||||
bundler: 2.2.33
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
id: download
|
||||
if: always()
|
||||
with:
|
||||
# Note: Not specifying a name will download all artifacts from the previous workflow jobs
|
||||
path: raw-data
|
||||
|
||||
- name: allure generate
|
||||
if: always()
|
||||
run: |
|
||||
export VERSION=2.22.1
|
||||
|
||||
curl -o allure-$VERSION.tgz -Ls https://github.com/allure-framework/allure2/releases/download/$VERSION/allure-$VERSION.tgz
|
||||
tar -zxvf allure-$VERSION.tgz -C .
|
||||
|
||||
ls -la ${{steps.download.outputs.download-path}}
|
||||
./allure-$VERSION/bin/allure generate ${{steps.download.outputs.download-path}}/* -o ./allure-report
|
||||
|
||||
find ${{steps.download.outputs.download-path}}
|
||||
bundle exec ruby tools/dev/report_generation/support_matrix/generate.rb --allure-data ${{steps.download.outputs.download-path}} > ./allure-report/support_matrix.html
|
||||
|
||||
- name: archive results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: final-report-${{ github.run_id }}
|
||||
path: |
|
||||
./allure-report
|
|
@ -75,6 +75,31 @@ Run the test suite:
|
|||
MSSQL_RPORT=1433 SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false bundle exec rspec ./spec/acceptance/mssql_spec.rb
|
||||
```
|
||||
|
||||
### SMB
|
||||
|
||||
Build the Docker image:
|
||||
|
||||
```
|
||||
docker compose build
|
||||
```
|
||||
|
||||
Run a target:
|
||||
|
||||
```
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```
|
||||
SMB_USERNAME=acceptance_tests_user SMB_PASSWORD=acceptance_tests_password SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false bundle exec rspec ./spec/acceptance/smb_spec.rb
|
||||
```
|
||||
|
||||
Shut down the container:
|
||||
```
|
||||
docker compose down
|
||||
```
|
||||
|
||||
#### Allure reports
|
||||
|
||||
Generate allure reports locally:
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
require 'acceptance_spec_helper'
|
||||
|
||||
RSpec.describe 'SMB sessions and SMB modules' do
|
||||
include_context 'wait_for_expect'
|
||||
|
||||
RHOST_REGEX = /\d+\.\d+\.\d+\.\d+:\d+/
|
||||
|
||||
TESTS = {
|
||||
smb: {
|
||||
target: {
|
||||
session_module: "auxiliary/scanner/smb/smb_login",
|
||||
type: 'SMB',
|
||||
platforms: [:linux, :osx, :windows],
|
||||
datastore: {
|
||||
global: {},
|
||||
module: {
|
||||
username: ENV.fetch('SMB_USERNAME', 'acceptance_tests_user'),
|
||||
password: ENV.fetch('SMB_PASSWORD', 'acceptance_tests_password'),
|
||||
rhost: ENV.fetch('SMB_RHOST', '127.0.0.1'),
|
||||
rport: ENV.fetch('SMB_RPORT', '445'),
|
||||
}
|
||||
}
|
||||
},
|
||||
module_tests: [
|
||||
{
|
||||
name: "post/test/smb",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session],
|
||||
skipped: false,
|
||||
},
|
||||
# Flaky:
|
||||
# Error: RubySMB::Error::UnexpectedStatusCode The server responded with an unexpected status code: STATUS_PIPE_BROKEN
|
||||
# {
|
||||
# name: "auxiliary/scanner/smb/smb_lookupsid",
|
||||
# platforms: [:linux, :osx, :windows],
|
||||
# targets: [:session, :rhost],
|
||||
# skipped: false,
|
||||
# lines: {
|
||||
# all: {
|
||||
# required: [
|
||||
# "GROUP=None",
|
||||
# "USER=nobody",
|
||||
# "PIPE(LSARPC) LOCAL",
|
||||
# ],
|
||||
# },
|
||||
# }
|
||||
# },
|
||||
# Flaky:
|
||||
# RubySMB::Error::CommunicationError Communication error with the remote host: Read timeout expired when reading from the Socket (timeout=30).
|
||||
# The server supports encryption and this error may have been caused by encryption issues, but not always.
|
||||
# Fixed here: https://github.com/rapid7/metasploit-framework/pull/19095
|
||||
# {
|
||||
# name: "auxiliary/scanner/smb/smb_enumusers",
|
||||
# platforms: [:linux, :osx, :windows],
|
||||
# targets: [:session, :rhost],
|
||||
# skipped: false,
|
||||
# lines: {
|
||||
# all: {
|
||||
# required: [
|
||||
# "acceptance_tests_user",
|
||||
# ],
|
||||
# },
|
||||
# }
|
||||
# },
|
||||
{
|
||||
name: "auxiliary/scanner/smb/pipe_auditor",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session, :rhost],
|
||||
skipped: false,
|
||||
lines: {
|
||||
all: {
|
||||
required: [
|
||||
/Pipes: (\\([a-zA-Z]*)(, )?)*/,
|
||||
],
|
||||
known_failures: [
|
||||
/Inaccessible named pipe:/,
|
||||
/The server responded with an unexpected status code: STATUS_OBJECT_NAME_NOT_FOUND/,
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "auxiliary/scanner/smb/smb_enumshares",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session, :rhost],
|
||||
skipped: false,
|
||||
lines: {
|
||||
all: {
|
||||
required: [
|
||||
"modifiable - (DISK)",
|
||||
"readonly - (DISK)",
|
||||
"IPC$ - (IPC|SPECIAL) IPC Service",
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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[
|
||||
smb_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 = acceptable_failure.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)
|
||||
|
||||
if acceptable_failure.value.is_a?(Regexp)
|
||||
expect(test_result).to match(acceptable_failure.value)
|
||||
else
|
||||
expect(test_result).to include(acceptable_failure.value)
|
||||
end
|
||||
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
|
|
@ -0,0 +1,202 @@
|
|||
require 'rex/post/meterpreter/extensions/stdapi/command_ids'
|
||||
require 'rex'
|
||||
require 'fileutils'
|
||||
|
||||
lib = File.join(Msf::Config.install_root, 'test', 'lib')
|
||||
$LOAD_PATH.push(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'module_test'
|
||||
|
||||
class MetasploitModule < Msf::Post
|
||||
|
||||
include Msf::ModuleTest::PostTest
|
||||
include Msf::ModuleTest::PostTestFileSystem
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Testing SMB sessions work',
|
||||
'Description' => %q{ This module will test the SMB sessions work },
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'sjanusz-r7'],
|
||||
'Platform' => all_platforms,
|
||||
'SessionTypes' => [ 'smb' ]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def test_console_help
|
||||
it 'should support the help command' do
|
||||
stdout = with_mocked_console(session) { |console| console.run_single('help') }
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?('Core Commands')
|
||||
ret &&= stdout.buf.include?('Shares Commands')
|
||||
ret &&= stdout.buf.include?('Local File System Commands')
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
def test_upload_and_download
|
||||
readonly_share = 'readonly'
|
||||
modifiable_share = 'modifiable'
|
||||
|
||||
Tempfile.create do |temp_file|
|
||||
filename = File.basename(temp_file)
|
||||
full_path = temp_file.to_path
|
||||
|
||||
it 'should support uploading files' do
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single("upload #{full_path} #{filename}")
|
||||
end
|
||||
|
||||
ret = true
|
||||
# Or filename?
|
||||
ret &&= stdout.buf.include?("#{full_path} uploaded to #{filename}")
|
||||
ret
|
||||
end
|
||||
|
||||
it 'should not upload to readonly share' do
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{readonly_share}")
|
||||
console.run_single("upload #{full_path} #{filename}")
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?('Error running command upload')
|
||||
ret &&= stdout.buf.include?('The server responded with an unexpected status code: STATUS_ACCESS_DENIED')
|
||||
ret
|
||||
end
|
||||
|
||||
it 'should support deleting files' do
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single("delete #{filename}")
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?("Deleted #{filename}")
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
Tempfile.create do |temp_file|
|
||||
remote_filename = 'hello_world.txt'
|
||||
remote_dir = 'text_files'
|
||||
full_path = temp_file.to_path
|
||||
|
||||
it 'should support downloading files' do
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single("cd #{remote_dir}")
|
||||
console.run_single("download #{remote_filename} #{full_path}")
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?("Downloaded #{remote_dir}\\#{remote_filename} to #{full_path}")
|
||||
ret
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_files
|
||||
modifiable_share = 'modifiable'
|
||||
|
||||
it 'should output files in the current directory' do
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single('ls')
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?('recursive')
|
||||
ret &&= stdout.buf.include?('text_files')
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
def test_directories
|
||||
it 'should support changing a directory' do
|
||||
folder_name = 'text_files'
|
||||
modifiable_share = 'modifiable'
|
||||
expected_file_name = 'hello_world.txt'
|
||||
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single("cd #{folder_name}")
|
||||
console.run_single('ls')
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include? expected_file_name
|
||||
ret
|
||||
end
|
||||
|
||||
it 'should support creating a new directory' do
|
||||
modifiable_share = 'modifiable'
|
||||
new_directory_name = 'my_new_directory'
|
||||
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single("mkdir #{new_directory_name}")
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?("Directory #{new_directory_name} created")
|
||||
ret
|
||||
end
|
||||
|
||||
it 'should support deleting a directory' do
|
||||
modifiable_share = 'modifiable'
|
||||
new_directory_name = 'my_new_directory'
|
||||
|
||||
stdout = with_mocked_console(session) do |console|
|
||||
console.run_single("shares -i #{modifiable_share}")
|
||||
console.run_single("rmdir #{new_directory_name}")
|
||||
end
|
||||
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?("Deleted #{new_directory_name}")
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
def test_shares
|
||||
it 'should support switching shares' do
|
||||
stdout = with_mocked_console(session) { |console| console.run_single('shares -i 0') }
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?('Successfully connected to modifiable')
|
||||
|
||||
stdout = with_mocked_console(session) { |console| console.run_single('shares -i 1') }
|
||||
|
||||
ret &&= stdout.buf.include?('Successfully connected to readonly')
|
||||
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_platforms
|
||||
Msf::Module::Platform.subclasses.collect { |c| c.realname.downcase }
|
||||
end
|
||||
|
||||
# Wrap the console with a mocked stdin/stdout for testing purposes. This ensures the console
|
||||
# will not write the real stdout, and the contents can be verified in the test
|
||||
# @param [Session] session
|
||||
# @return [Rex::Ui::Text::Output::Buffer] the stdout buffer
|
||||
def with_mocked_console(session)
|
||||
old_input = session.console.input
|
||||
old_output = session.console.output
|
||||
|
||||
mock_input = Rex::Ui::Text::Input.new
|
||||
mock_output = Rex::Ui::Text::Output::Buffer.new
|
||||
|
||||
session.console.init_ui(mock_input, mock_output)
|
||||
yield session.console
|
||||
|
||||
mock_output
|
||||
ensure
|
||||
session.console.init_ui(old_input, old_output)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM ubuntu:22.04 AS build
|
||||
MAINTAINER metasploit-framework <https://github.com/rapid7/metasploit-framework>
|
||||
WORKDIR /opt
|
||||
|
||||
EXPOSE 445 139
|
||||
|
||||
# Switch shells to support ANSI-C quoting:
|
||||
# https://stackoverflow.com/questions/33439230/how-to-write-commands-with-multiple-lines-in-dockerfile-while-preserving-the-new
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Install Samba
|
||||
RUN apt update && apt install -y samba smbclient
|
||||
|
||||
# To add a credential to Samba, the user needs to be created on the system
|
||||
RUN useradd -m acceptance_tests_user
|
||||
|
||||
# Configure a few shares
|
||||
COPY shares shares
|
||||
|
||||
RUN chmod -R 777 shares
|
||||
|
||||
COPY config/smb.conf /etc/samba/smb.conf
|
||||
|
||||
# Change the passwords for our user
|
||||
RUN smbpasswd -a acceptance_tests_user <<EOF
|
||||
acceptance_tests_password
|
||||
acceptance_tests_password
|
||||
EOF
|
||||
|
||||
# Enable the users
|
||||
RUN smbpasswd -e acceptance_tests_user
|
||||
|
||||
RUN service smbd restart
|
||||
|
||||
FROM build AS runner
|
||||
|
||||
ENTRYPOINT ["smbd", "--foreground", "--no-process-group"]
|
|
@ -0,0 +1,27 @@
|
|||
## Setup
|
||||
|
||||
This contains a custom Docker image used for SMB acceptance testing.
|
||||
|
||||
## Running
|
||||
|
||||
- Build:
|
||||
```shell
|
||||
docker compose build
|
||||
```
|
||||
|
||||
- Run:
|
||||
```shell
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
- Shut down:
|
||||
```shell
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Adding More Shares & Files
|
||||
|
||||
To add more shares, you need to create the directories and files in the `shares` folder.
|
||||
Each directory in this folder should follow a pattern of being mapped to a share.
|
||||
These directories and files will be copied to the container when re-building the Docker image.
|
||||
For each new share, you need to add in a corresponding entry to the `config/smb.conf` file so that the share is registered by Samba.
|
|
@ -0,0 +1,18 @@
|
|||
[global]
|
||||
server min protocol = NT1
|
||||
ntlm auth = yes
|
||||
|
||||
[modifiable]
|
||||
comment = Samba on Ubuntu
|
||||
path = /opt/shares/modifiable
|
||||
read only = no
|
||||
writeable = yes
|
||||
browsable = yes
|
||||
valid users = acceptance_tests_user
|
||||
|
||||
[readonly]
|
||||
comment = Second Samba on Ubuntu
|
||||
path = /opt/shares/readonly
|
||||
read only = yes
|
||||
browsable = yes
|
||||
valid users = acceptance_tests_user
|
|
@ -0,0 +1,18 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
samba:
|
||||
tty: true
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "139:139"
|
||||
- "445:445"
|
||||
healthcheck:
|
||||
test: smbclient -U 'acceptance_tests_user%acceptance_tests_password' -L 127.0.0.1
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
|
@ -0,0 +1 @@
|
|||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
|
@ -0,0 +1 @@
|
|||
Hello World! - From Samba
|
|
@ -0,0 +1 @@
|
|||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
Loading…
Reference in New Issue