diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index cebd411e0c96..7547ddfa7c2b 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -22,6 +22,18 @@ permissions: statuses: none on: + workflow_dispatch: + inputs: + metasploitPayloadsCommit: + description: 'metasploit-payloads branch would like to test' + required: true + default: '0b77a961b2f10fa1bee670ca49673801a70fa1e8' + mettleCommit: + description: 'mettle branch you would like to test' + required: true +# default: '375434396ff9a80880f9901f1dfd5b0226b9b4d9' # Master +# default: '08c2fb772ea48961909f19c832e3ec078e448637' # Smashery + default: 'new_cmd_exec_rebased_against_latest_master' push: branches-ignore: - gh-pages @@ -52,39 +64,52 @@ jobs: fail-fast: false matrix: os: - - macos-12 +# - macos-12 - windows-2019 - - ubuntu-20.04 +# - ubuntu-20.04 ruby: - 3.0.2 - meterpreter: - # Python - - { name: python, runtime_version: 3.6 } - - { name: python, runtime_version: 3.11 } +# meterpreter: +# Python +# - { name: python, runtime_version: 3.6 } +# - { name: python, runtime_version: 3.11 } # Java - - { name: java, runtime_version: 8 } - - { name: java, runtime_version: 21 } +# - { name: java, runtime_version: 8 } +# - { name: java, runtime_version: 21 } # PHP - - { name: php, runtime_version: 5.3 } - - { name: php, runtime_version: 7.4 } - - { name: php, runtime_version: 8.3 } +# - { name: php, runtime_version: 5.3 } +# - { name: php, runtime_version: 7.4 } +# - { name: php, runtime_version: 8.3 } include: # Windows Meterpreter - - { meterpreter: { name: windows_meterpreter }, os: windows-2019 } - - { meterpreter: { name: windows_meterpreter }, os: windows-2022 } +# - { meterpreter: { name: windows_meterpreter }, os: windows-2019 } +# - { meterpreter: { name: windows_meterpreter }, os: windows-2022 } + + # Powershell + - { meterpreter: { name: powershell }, os: windows-2019 } + - { meterpreter: { name: powershell }, os: windows-2022 } + + # Linux +# - { meterpreter: { name: linux }, os: ubuntu-20.04 } + + # CMD +# - { meterpreter: { name: cmd }, os: windows-2019 } +# - { meterpreter: { name: cmd }, os: windows-2022 } # Mettle - - { meterpreter: { name: mettle }, os: macos-12 } - - { meterpreter: { name: mettle }, os: ubuntu-20.04 } +# - { meterpreter: { name: mettle }, os: macos-12 } +# - { meterpreter: { name: mettle }, os: ubuntu-20.04 } runs-on: ${{ matrix.os }} - timeout-minutes: 25 + timeout-minutes: 50 env: RAILS_ENV: test + metasploitPayloadsCommit: ${{ github.event.inputs.metasploitPayloadsCommit || '0b77a961b2f10fa1bee670ca49673801a70fa1e8' }} + mettleCommit: ${{ github.event.inputs.mettleCommit|| 'new_cmd_exec_rebased_against_latest_master' }} HOST_RUNNER_IMAGE: ${{ matrix.os }} METERPRETER: ${{ matrix.meterpreter.name }} METERPRETER_RUNTIME_VERSION: ${{ matrix.meterpreter.runtime_version }} @@ -96,7 +121,7 @@ jobs: if: runner.os == 'Linux' run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz - - uses: shivammathur/setup-php@fc14643b0a99ee9db10a3c025a33d76544fa3761 + - uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d if: ${{ matrix.meterpreter.name == 'php' }} with: php-version: ${{ matrix.meterpreter.runtime_version }} @@ -129,8 +154,82 @@ jobs: dir %WINDIR% type %WINDIR%\\system32\\drivers\\etc\\hosts - - name: Checkout code + # The job checkout structure is: + # . + # ├── metasploit-framework + # └── metasploit-payloads (Only if metasploit-payloads-label is applied) + # └── mettle (Only if mettle-label is applied) + + # Install Docker if on MACOS + - name: Install Docker - macOS +# if: ${{ ( matrix.meterpreter.name == 'java') && (runner.os == 'macos' ) || ( runner.os == 'linux') && (matrix.meterpreter.name == 'mettle') }} + if: ${{ ( matrix.meterpreter.name == 'java') && (runner.os == 'macos' ) }} + # if: contains(github.event.issue.labels.*.name, 'metasploit-payloads-label') + run: | + brew install docker + colima delete + colima start --arch x86_64 + + # Checkout mettle + # TODO: Mettle and Metasploit payloads labels + - name: Checkout mettle + if: ${{ matrix.meterpreter.name == 'mettle' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') uses: actions/checkout@v4 + with: + repository: rapid7/mettle + path: mettle + ref: ${{ env.mettleCommit }} + + - name: Get mettle version + if: ${{ matrix.meterpreter.name == 'mettle' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + echo "METTLE_VERSION=$(grep -oh '[0-9].[0-9].[0-9]*' lib/metasploit_payloads/mettle/version.rb)" >> $GITHUB_ENV + working-directory: mettle + + - name: Use mettle version + if: ${{ matrix.meterpreter.name == 'mettle' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + echo "${{ env.METTLE_VERSION }}" + working-directory: mettle + + # Prerequisite mettle gem setup + # TODO: Will need to figure out how to get the current version for later commands + - name: Build mettle payloads + if: ${{ matrix.meterpreter.name == 'mettle' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + set -x + ruby -pi.bak -e "gsub(/${{ env.METTLE_VERSION }}/, '${{ env.METTLE_VERSION }}-dev')" lib/metasploit_payloads/mettle/version.rb + working-directory: mettle + + # Compile mettle payload + # TODO: Will need to figure out how to get the current version for later commands + - name: Compile mettle payloads + if: ${{ matrix.meterpreter.name == 'mettle' && runner.os != 'macos' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + docker run --rm=true --tty --volume=$(pwd):/mettle --workdir=/mettle rapid7/build:mettle rake mettle:build mettle:check + rake build + working-directory: mettle + + # Compile mettle payload - macOS + # TODO: Will need to figure out how to get the current version for later commands + - name: Compile mettle payloads - macOS + if: ${{ matrix.meterpreter.name == 'mettle' && runner.os == 'macos' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + make TARGET=x86_64-apple-darwin + rake build + working-directory: mettle + + # Checkout metasploit-framework + - name: Checkout metasploit-framework code + uses: actions/checkout@v4 + with: + path: metasploit-framework - name: Setup Ruby env: @@ -139,12 +238,91 @@ jobs: with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - cache-version: 4 + working-directory: metasploit-framework + cache-version: 5 # Github actions with Ruby requires Bundler 2.2.18+ # https://github.com/ruby/setup-ruby/tree/d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c#windows bundler: 2.2.33 - - name: acceptance + # Copying mettle gem into framework - macOS + - name: Move mettle gem - macOS + if: ${{ matrix.meterpreter.name == 'mettle' && runner.os == 'macos' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + cp /Users/runner/work/metasploit-framework/metasploit-framework/mettle/pkg/metasploit_payloads-mettle-${{ env.METTLE_VERSION }}.pre.dev.gem /Users/runner/work/metasploit-framework/metasploit-framework/metasploit-framework + working-directory: metasploit-framework + + # Copying mettle gem into framework - macOS + - name: Move mettle gem + if: ${{ matrix.meterpreter.name == 'mettle' && runner.os != 'macos' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + cp /home/runner/work/metasploit-framework/metasploit-framework/mettle/pkg/metasploit_payloads-mettle-${{ env.METTLE_VERSION }}.pre.dev.gem /home/runner/work/metasploit-framework/metasploit-framework/metasploit-framework + working-directory: metasploit-framework + + # Copying mettle gem into framework + - name: Install mettle gem + if: ${{ matrix.meterpreter.name == 'mettle' }} + # if: contains(github.event.issue.labels.*.name, 'mettle-label') + run: | + set -x + bundle exec gem install metasploit_payloads-mettle-${{ env.METTLE_VERSION }}.pre.dev.gem + gem list metasploit_payloads-mettle + ruby -pi.bak -e "gsub(/'metasploit_payloads-mettle', '1.0.31'/, '\'metasploit_payloads-mettle\', \'${{ env.METTLE_VERSION }}.pre.dev\'')" metasploit-framework.gemspec + cat Gemfile.lock + bundle config + bundle config unset deployment + bundle update metasploit_payloads-mettle + bundle install + working-directory: metasploit-framework + + # Checkout metasploit-payloads + - name: Checkout metasploit-payloads +# if: contains(github.event.issue.labels.*.name, 'metasploit-payloads-label') + uses: actions/checkout@v4 + with: + repository: rapid7/metasploit-payloads + path: metasploit-payloads + ref: ${{ env.metasploitPayloadsCommit }} + + # Build Java and Android payloads, Docker command needs to be ran from the directory up from metasploit-payloads + - name: Build Java & Android payloads + if: ${{ (matrix.meterpreter.name == 'java') && (runner.os != 'Windows') }} +# if: contains(github.event.issue.labels.*.name, 'metasploit-payloads-label') + run: | + cd .. + docker run --rm -w "$(pwd)" -v "$(pwd):$(pwd)" rapid7/msf-ubuntu-x64-meterpreter:latest /bin/bash -c "cd metasploit-payloads/java && make clean && make android && mvn -P deploy package" + working-directory: metasploit-payloads + + - name: Build Windows payloads via Visual Studio 2019 Build (Windows) + shell: cmd + if: ${{ (runner.os == 'Windows') && (matrix.os == 'windows-2019') }} +# if: ${{ (runner.os == 'Windows') && (contains(github.event.issue.labels.*.name, 'metasploit-payloads-label') ) }} + run: | + cd c/meterpreter + git submodule init && git submodule update + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat" && make.bat + working-directory: metasploit-payloads + + - name: Build Windows payloads via Visual Studio 2022 Build (Windows) + shell: cmd + if: ${{ (runner.os == 'Windows') && (matrix.os == 'windows-2022') }} + # if: ${{ (runner.os == 'Windows') && (contains(github.event.issue.labels.*.name, 'metasploit-payloads-label') ) }} + run: | + cd c/meterpreter + git submodule init && git submodule update + make.bat + working-directory: metasploit-payloads + + # Run makefile within metasploit-payloads + - name: Build PHP, Python and Windows payloads + if: ${{ (matrix.meterpreter.name == 'php') || (matrix.meterpreter.name == 'python') || (runner.os == 'Windows') }} +# if: contains(github.event.issue.labels.*.name, 'metasploit-payloads-label') + run: | + make install-php install-python install-windows + working-directory: metasploit-payloads + + - name: Acceptance env: SPEC_HELPER_LOAD_METASPLOIT: false SPEC_OPTS: "--tag acceptance --require acceptance_spec_helper.rb --color --format documentation --format AllureRspec::RSpecFormatter" @@ -157,6 +335,8 @@ jobs: # Additionally - flakey tests should be fixed or marked as flakey instead of silently retried run: | bundle exec rspec spec/acceptance/meterpreter_spec.rb + bundle exec rspec spec/acceptance/non_meterpreter_spec.rb + working-directory: metasploit-framework - name: Archive results if: always() @@ -164,7 +344,7 @@ jobs: with: # Provide a unique artifact for each matrix os, otherwise race conditions can lead to corrupt zips name: raw-data-${{ matrix.meterpreter.name }}-${{ matrix.meterpreter.runtime_version }}-${{ matrix.os }} - path: tmp/allure-raw-data + path: metasploit-framework/tmp/allure-raw-data # Generate a final report from the previous test results report: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml deleted file mode 100644 index ed70f8bb89fc..000000000000 --- a/.github/workflows/verify.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: Verify - -# 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 - - weekly-dependency-updates - pull_request: - branches-ignore: - - weekly-dependency-updates - -jobs: - build: - runs-on: ubuntu-latest - timeout-minutes: 40 - name: Docker Build - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: docker-compose build - run: | - curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/bin - - /usr/bin/docker-compose build - - test: - runs-on: ${{ matrix.os }} - timeout-minutes: 40 - - services: - postgres: - image: postgres:9.6 - ports: ["5432:5432"] - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - strategy: - fail-fast: true - matrix: - ruby: - - '3.1' - - '3.2' - - '3.3' - - '3.4.0-preview1' - os: - - ubuntu-20.04 - - ubuntu-latest - exclude: - - { os: ubuntu-latest, ruby: '3.0' } - include: - - os: ubuntu-latest - ruby: '3.1' - test_cmd: 'bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" MSF_FEATURE_DEFER_MODULE_LOADS=1' - test_cmd: - - bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" - - bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag ~content" - # Used for testing the remote data service - - bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" REMOTE_DB=1 - - bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag ~content" REMOTE_DB=1 - - env: - RAILS_ENV: test - BUNDLE_WITHOUT: "coverage development pcap" - - name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }} - ${{ matrix.test_cmd }} - 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: Setup Ruby - env: - # 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: Create database - run: | - cp config/database.yml.github_actions config/database.yml - bundle exec rake --version - bundle exec rake db:create - bundle exec rake db:migrate - # fail build if db/schema.rb update is not committed - git diff --exit-code db/schema.rb - - - name: ${{ matrix.test_cmd }} - run: | - echo "${CMD}" - bash -c "${CMD}" - env: - CMD: ${{ matrix.test_cmd }} diff --git a/data/cmd_exec/README.md b/data/cmd_exec/README.md new file mode 100644 index 000000000000..217ab4a0808f --- /dev/null +++ b/data/cmd_exec/README.md @@ -0,0 +1,26 @@ +## Setup + +This contains setup steps used for acceptance testing of the `cmd_exec` API. We will make use of the gcc docker image to build out the C binaries to +then be uploaded to the host machine, so they can be used as part of the `cmd_exec` create process API. + +This directory contains: +- C executable `show_args.c` +This file is used as part of the `cmd_exec` testing as it requires a file to take args loop over them and output those args back to the user. + +- Makefile to build the binaries `makefile.mk` +This file is used to create the binaries for both Windows and Linux that the docker command below will make use of. + +- Precompiled binaries for Windows + - `show_args.exe` + +- Precompiled binaries for Linux and Mettle + - `show_args` + +## Compile binaries locally + +We make use of gcc for this: https://hub.docker.com/_/gcc + +- Run: +```shell +docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp gcc:11.4.0 /bin/bash -c "apt update && apt install -y gcc-mingw-w64 && make all -f makefile.mk" +``` diff --git a/data/cmd_exec/makefile.mk b/data/cmd_exec/makefile.mk new file mode 100644 index 000000000000..12b98e53427a --- /dev/null +++ b/data/cmd_exec/makefile.mk @@ -0,0 +1,5 @@ +all: show_args_linux show_args_windows +show_args_linux: show_args.c + cc show_args.c -o show_args +show_args_windows: show_args.c + x86_64-w64-mingw32-gcc show_args.c -o show_args.exe diff --git a/data/cmd_exec/show_args b/data/cmd_exec/show_args new file mode 100755 index 000000000000..2e90a22d44ca Binary files /dev/null and b/data/cmd_exec/show_args differ diff --git a/data/cmd_exec/show_args.c b/data/cmd_exec/show_args.c new file mode 100644 index 000000000000..8822cada48a4 --- /dev/null +++ b/data/cmd_exec/show_args.c @@ -0,0 +1,8 @@ +int printf(const char *format, ...); + +int main(int argc, char *argv[]) { + + for (int i = 0; i < argc; i++) { + printf("%s\n", argv[i]); + } +} diff --git a/data/cmd_exec/show_args.exe b/data/cmd_exec/show_args.exe new file mode 100755 index 000000000000..05b7a8784cc4 Binary files /dev/null and b/data/cmd_exec/show_args.exe differ diff --git a/data/cmd_exec/show_args_macos b/data/cmd_exec/show_args_macos new file mode 100755 index 000000000000..059da12f4d2c Binary files /dev/null and b/data/cmd_exec/show_args_macos differ diff --git a/lib/msf/core/session/provider/single_command_shell.rb b/lib/msf/core/session/provider/single_command_shell.rb index 208ecf670f7b..4a166e48e261 100644 --- a/lib/msf/core/session/provider/single_command_shell.rb +++ b/lib/msf/core/session/provider/single_command_shell.rb @@ -97,7 +97,7 @@ def to_cmd(cmd, args) if platform == 'windows' result = Msf::Sessions::CommandShellWindows.to_cmd(cmd, args) else - result = Msf::Session::CommandShellUnix.to_cmd(cmd, args) + result = Msf::Sessions::CommandShellUnix.to_cmd(cmd, args) end end diff --git a/powershell_payload.exe b/powershell_payload.exe new file mode 100755 index 000000000000..4b8b0118b918 Binary files /dev/null and b/powershell_payload.exe differ diff --git a/spec/acceptance/README.md b/spec/acceptance/README.md index 3f19022ffc4a..f559e9541fd9 100644 --- a/spec/acceptance/README.md +++ b/spec/acceptance/README.md @@ -27,9 +27,14 @@ SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false bundle exec rspec ``` Run a specific Meterpreter/module test Unix / Windows: + +# Bash command: +``` +SPEC_OPTS='--tag acceptance' METERPRETER=php METERPRETER_MODULE_TEST=post/test/unix bundle exec rspec './spec/acceptance/meterpreter_spec.rb' ``` -SPEC_OPTS='--tag acceptance' METERPRETER=php METERPRETER_MODULE_TEST=test/unix bundle exec rspec './spec/acceptance/meterpreter_spec.rb' +# Powershell command: +``` $env:SPEC_OPTS='--tag acceptance'; $env:SPEC_HELPER_LOAD_METASPLOIT=$false; $env:METERPRETER = 'php'; bundle exec rspec './spec/acceptance/meterpreter_spec.rb' ``` diff --git a/spec/acceptance/meterpreter_spec.rb b/spec/acceptance/meterpreter_spec.rb index a65435836e71..b8db3f967933 100644 --- a/spec/acceptance/meterpreter_spec.rb +++ b/spec/acceptance/meterpreter_spec.rb @@ -1,4 +1,5 @@ require 'acceptance_spec_helper' +require 'base64' RSpec.describe 'Meterpreter' do include_context 'wait_for_expect' diff --git a/spec/acceptance/non_meterpreter_spec.rb b/spec/acceptance/non_meterpreter_spec.rb new file mode 100644 index 000000000000..46385353a072 --- /dev/null +++ b/spec/acceptance/non_meterpreter_spec.rb @@ -0,0 +1,547 @@ +require 'acceptance_spec_helper' +require 'base64' + +# TODO: Need better name for file and constant + +RSpec.describe 'NonMeterpreter' do + include_context 'wait_for_expect' + + # Tests to ensure that CMD/Powershell/Linux is consistent across all implementations/operation systems + NON_METERPRETER_PAYLOADS = Acceptance::NonMeterpreter.with_non_meterpreter_name_merged( + { + powershell: Acceptance::NonMeterpreter::POWERSHELL, + cmd: Acceptance::NonMeterpreter::CMD, + linux: Acceptance::NonMeterpreter::LINUX + } + ) + + allure_test_environment = AllureRspec.configuration.environment_properties + + let_it_be(:current_platform) { Acceptance::NonMeterpreter::current_platform } + + # @!attribute [r] port_allocator + # @return [Acceptance::PortAllocator] + let_it_be(:port_allocator) { Acceptance::PortAllocator.new } + + # 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 + + console + end + + NON_METERPRETER_PAYLOADS.each do |meterpreter_name, meterpreter_config| + meterpreter_runtime_name = "#{meterpreter_name}#{ENV.fetch('METERPRETER_RUNTIME_VERSION', '')}" + + describe meterpreter_runtime_name, focus: meterpreter_config[:focus] do + meterpreter_config[:payloads].each.with_index do |payload_config, payload_config_index| + describe( + Acceptance::Meterpreter.human_name_for_payload(payload_config).to_s, + if: ( + Acceptance::NonMeterpreter.run_meterpreter?(meterpreter_config) && + Acceptance::NonMeterpreter.supported_platform?(payload_config) + ) + ) do + let(:payload) { Acceptance::Payload.new(payload_config) } + + class LocalPath + attr_reader :path + + def initialize(path) + @path = path + end + end + + let(:session_tlv_logging_file) do + # LocalPath.new('/tmp/php_session_tlv_log.txt') + Acceptance::TempChildProcessFile.new("#{payload.name}_session_tlv_logging", 'txt') + end + + let(:meterpreter_logging_file) do + # LocalPath.new('/tmp/php_log.txt') + Acceptance::TempChildProcessFile.new("#{payload.name}_debug_log", 'txt') + end + + let(:payload_stdout_and_stderr_file) do + # LocalPath.new('/tmp/php_log.txt') + Acceptance::TempChildProcessFile.new("#{payload.name}_stdout_and_stderr", 'txt') + end + + let(:default_global_datastore) do + { + SessionTlvLogging: "file:#{session_tlv_logging_file.path}" + } + end + + let(:test_environment) { allure_test_environment } + + let(:default_module_datastore) do + { + AutoVerifySessionTimeout: ENV['CI'] ? 30 : 10, + lport: port_allocator.next, + lhost: '127.0.0.1', + MeterpreterDebugLogging: "rpath:#{meterpreter_logging_file.path}" + } + end + + let(:executed_payload) do + file = File.open(payload_stdout_and_stderr_file.path, 'w') + driver.run_payload( + payload, + { + out: file, + err: file + } + ) + end + + # The shared payload process and session instance that will be reused across the test run + # + let(:payload_process_and_session_id) do + console.sendline "use #{payload.name}" + console.recvuntil(Acceptance::Console.prompt) + + # Set global options + console.sendline payload.setg_commands(default_global_datastore: default_global_datastore) + console.recvuntil(Acceptance::Console.prompt) + + # Generate the payload + console.sendline payload.generate_command(default_module_datastore: default_module_datastore) + console.recvuntil(/Writing \d+ bytes[^\n]*\n/) + generate_result = console.recvuntil(Acceptance::Console.prompt) + + expect(generate_result.lines).to_not include(match('generation failed')) + wait_for_expect do + expect(payload.size).to be > 0 + end + + console.sendline payload.handler_command(default_module_datastore: default_module_datastore) + console.recvuntil(/Started reverse TCP handler[^\n]*\n/) + payload_process = executed_payload + session_id = nil + + # Wait for the session to open, or break early if the payload is detected as dead + wait_for_expect do + unless payload_process.alive? + break + end + + # TODO: Was strictly for Meterpreter sessions, now more generic + # - can be reverted if we decide to move these new tests + session_opened_matcher = /\w.* 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 + + [payload_process, session_id] + end + + # @param [String] path The file path to read the content of + # @return [String] The file contents if found + def get_file_attachment_contents(path) + return 'none resent' unless File.exist?(path) + + content = File.binread(path) + content.blank? ? 'file created - but empty' : content + 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 "#{Acceptance::NonMeterpreter.current_platform}" do + describe "#{Acceptance::NonMeterpreter.current_platform}/#{meterpreter_runtime_name} Meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload" do + it( + "exposes available metasploit commands", + if: ( + # Assume that regardless of payload, staged/unstaged/etc, the Meterpreter will have the same commands available + # So only run this test when config_index == 0 + payload_config_index == 0 && Acceptance::Meterpreter.supported_platform?(payload_config) + # Run if ENV['METERPRETER'] = 'java php' etc + Acceptance::Meterpreter.run_meterpreter?(meterpreter_config) && + # Only run payloads / tests, if the host machine can run them + Acceptance::Meterpreter.supported_platform?(payload_config) + ) + ) do + begin + replication_commands = [] + current_payload_status = '' + + # 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 + payload_process, session_id = payload_process_and_session_id + expect(payload_process).to(be_alive, proc do + current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}" + + Allure.add_attachment( + name: 'Failed payload blob', + source: Base64.strict_encode64(File.binread(payload_process.payload_path)), + type: Allure::ContentType::TXT + ) + + current_payload_status + end) + expect(session_id).to_not(be_nil, proc do + "There should be a session present" + end) + + resource_command = "resource scripts/resource/meterpreter_compatibility.rc" + replication_commands << resource_command + console.sendline(resource_command) + result = console.recvuntil(Acceptance::Console.prompt) + + available_commands = result.lines(chomp: true).find do |line| + line.start_with?("{") && line.end_with?("}") && JSON.parse(line) + rescue JSON::ParserError => _e + next + end + expect(available_commands).to_not be_nil + + available_commands_json = JSON.parse(available_commands, symbolize_names: true) + # Generate an allure attachment, a report can be generated afterwards + Allure.add_attachment( + name: 'available commands', + source: JSON.pretty_generate(available_commands_json), + type: Allure::ContentType::JSON, + test_case: false + ) + # Removed these lines as I believe the commands are Meterpreter specific + # expect(available_commands_json[:sessions].length).to be 1 + # expect(available_commands_json[:sessions].first[:commands]).to_not be_empty + 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 + + # Payload process cleanup / verification + # The payload process wasn't initially marked as dead - let's close it + if payload_process.present? && current_payload_status.blank? + begin + if payload_process.alive? + current_payload_status = "Process still alive after running test suite" + payload_process.close + else + current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}" + end + rescue => e + Allure.add_attachment( + name: 'driver.close_payloads failure information', + source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}", + type: Allure::ContentType::TXT + ) + end + end + + 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 + + payload_configuration_details = payload.as_readable_text( + default_global_datastore: default_global_datastore, + default_module_datastore: default_module_datastore + ) + + replication_steps = <<~EOF + ## Load test modules + loadpath test/modules + + #{payload_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: 'payload output if available', + source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}", + type: Allure::ContentType::TXT + ) + + Allure.add_attachment( + name: 'payload debug log if available', + source: get_file_attachment_contents(meterpreter_logging_file.path), + type: Allure::ContentType::TXT + ) + + Allure.add_attachment( + name: 'session tlv logging if available', + source: get_file_attachment_contents(session_tlv_logging_file.path), + type: Allure::ContentType::TXT + ) + + Allure.add_attachment( + name: 'console data', + source: current_console_data, + type: Allure::ContentType::TXT + ) + + raise test_run_error if test_run_error + raise console_reset_error if console_reset_error + end + end + + meterpreter_config[:module_tests].each do |module_test| + describe module_test[:name].to_s, focus: module_test[:focus] do + it( + "#{Acceptance::NonMeterpreter.current_platform}/#{meterpreter_runtime_name} meterpreter successfully opens a session for the #{payload_config[:name].inspect} payload and passes the #{module_test[:name].inspect} tests", + if: ( + # Run if ENV['METERPRETER'] = 'java php' etc + Acceptance::NonMeterpreter.run_meterpreter?(meterpreter_config) && + # Run if ENV['METERPRETER_MODULE_TEST'] = 'post/test/cmd_exec' etc + Acceptance::NonMeterpreter.run_meterpreter_module_test?(module_test[:name]) && + # Only run payloads / tests, if the host machine can run them + Acceptance::NonMeterpreter.supported_platform?(payload_config) && + Acceptance::NonMeterpreter.supported_platform?(module_test) && + # Skip tests that are explicitly skipped, or won't pass in the current environment + !Acceptance::NonMeterpreter.skipped_module_test?(module_test, allure_test_environment) + ), + # test metadata - will appear in allure report + module_test: module_test[:name] + ) do + begin + replication_commands = [] + current_payload_status = '' + + 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 } + + # 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 + payload_process, session_id = payload_process_and_session_id + + expect(payload_process).to(be_alive, proc do + $stderr.puts "Made it inside expect payload_process: #{payload_process}" + $stderr.puts "Is the process alive?: #{payload_process.alive?}" + $stderr.puts "Process wait.thread?: #{payload_process.wait_thread}" + $stderr.puts "We have access to .wait_thread, but do we have access to .wait_thread.value?: #{payload_process.alive?}" + + current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}" + + $stderr.puts "Made it after current_payload_status: #{payload_process}" + $stderr.puts "Is the process alive?: #{payload_process.alive?}" + + Allure.add_attachment( + name: 'Failed payload blob', + source: Base64.strict_encode64(File.binread(payload_process.payload_path)), + type: Allure::ContentType::TXT + ) + + current_payload_status + end) + 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} AddEntropy=true Verbose=true" + + replication_commands << use_module + console.sendline(use_module) + console.recvuntil(Acceptance::Console.prompt) + + replication_commands << run_module + console.sendline(run_module) + + # XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with: + # console.interact + + # Expect the test module to complete + test_result = console.recvuntil('Post module execution completed') + + # Ensure there are no failures, and assert tests are complete + aggregate_failures("#{payload_config[:name].inspect} payload 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| + line.include?(acceptable_failure.value) && + acceptable_failure.if?(test_environment) + end || line.match?(/Passed: \d+; Failed: \d+/) + + is_acceptable + end + + validated_lines.each do |test_line| + test_line = Acceptance::NonMeterpreter.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) + + expect(test_result).to include(required.value) + 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 + + # Payload process cleanup / verification + # The payload process wasn't initially marked as dead - let's close it + if payload_process.present? && current_payload_status.blank? + begin + if payload_process.alive? + current_payload_status = "Process still alive after running test suite" + payload_process.close + else + current_payload_status = "Expected Payload process to be running. Instead got: payload process exited with #{payload_process.wait_thread.value} - when running the command #{payload_process.cmd.inspect}" + end + rescue => e + Allure.add_attachment( + name: 'driver.close_payloads failure information', + source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}", + type: Allure::ContentType::TXT + ) + end + end + + 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 + + payload_configuration_details = payload.as_readable_text( + default_global_datastore: default_global_datastore, + default_module_datastore: default_module_datastore + ) + + replication_steps = <<~EOF + ## Load test modules + loadpath test/modules + + #{payload_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: 'payload output if available', + source: "Final status:\n#{current_payload_status}\nstdout and stderr:\n#{get_file_attachment_contents(payload_stdout_and_stderr_file.path)}", + type: Allure::ContentType::TXT + ) + + Allure.add_attachment( + name: 'payload debug log if available', + source: get_file_attachment_contents(meterpreter_logging_file.path), + type: Allure::ContentType::TXT + ) + + Allure.add_attachment( + name: 'session tlv logging if available', + source: get_file_attachment_contents(session_tlv_logging_file.path), + 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 + end + end + end + end + end + end + end +end diff --git a/spec/support/acceptance/non_meterpreter.rb b/spec/support/acceptance/non_meterpreter.rb new file mode 100644 index 000000000000..bd722798b545 --- /dev/null +++ b/spec/support/acceptance/non_meterpreter.rb @@ -0,0 +1,102 @@ +module Acceptance::NonMeterpreter + # @return [Symbol] The current platform + def self.current_platform + host_os = RbConfig::CONFIG['host_os'] + case host_os + when /darwin/ + :osx + when /mingw/ + :windows + when /linux/ + :linux + else + raise "unknown host_os #{host_os.inspect}" + end + end + + # Allows restricting the tests of a specific Meterpreter's test suite with the METERPRETER environment variable + # @return [TrueClass, FalseClass] True if the given Meterpreter should be run, false otherwise. + def self.run_meterpreter?(meterpreter_config) + return true if ENV['METERPRETER'].blank? + + name = meterpreter_config[:name].to_s + ENV['METERPRETER'].include?(name) + end + + # Allows restricting the tests of a specific Meterpreter's test suite with the METERPRETER environment variable + # @return [TrueClass, FalseClass] True if the given Meterpreter should be run, false otherwise. + def self.run_meterpreter_module_test?(module_test) + return true if ENV['METERPRETER_MODULE_TEST'].blank? + + ENV['METERPRETER_MODULE_TEST'].include?(module_test) + end + + # @param [String] string A console string with ANSI escape codes present + # @return [String] A string with the ANSI escape codes removed + def self.uncolorize(string) + string.gsub(/\e\[\d+m/, '') + end + + # @param [Hash] payload_config + # @return [Boolean] + def self.supported_platform?(payload_config) + payload_config[:platforms].include?(current_platform) + end + + # @param [Hash] module_test + # @return [Boolean] + def self.skipped_module_test?(module_test, test_environment) + current_platform_requirements = Array(module_test[:platforms].find { |platform| Array(platform)[0] == current_platform })[1] || {} + module_test.fetch(:skip, false) || + self.eval_predicate(current_platform_requirements.fetch(:skip, false), test_environment) + end + + # @param [Hash] payload_config + # @return [String] The human readable name for the given payload configuration + def self.human_name_for_payload(payload_config) + is_stageless = payload_config[:name].include?('meterpreter_reverse_tcp') + is_staged = payload_config[:name].include?('meterpreter/reverse_tcp') + + details = [] + details << 'stageless' if is_stageless + details << 'staged' if is_staged + details << payload_config[:name] + + details.join(' ') + end + + # @param [Object] hash A hash of key => hash + # @return [Object] Returns a new hash with the 'key' merged into hash value and all payloads + def self.with_non_meterpreter_name_merged(hash) + hash.each_with_object({}) do |(name, config), acc| + acc[name] = config.merge({ name: name }) + end + end + + # Evaluates a simple predicate; Similar to Msf::OptCondition.eval_condition + # @param [TrueClass,FalseClass,Array] value + # @param [Hash] environment + # @return [TrueClass, FalseClass] True or false + def self.eval_predicate(value, environment) + case value + when Array + left_operand, operator, right_operand = value + # Map values such as `:meterpreter_name` to the runtime value + left_operand = environment[left_operand] if environment.key?(left_operand) + right_operand = environment[right_operand] if environment.key?(right_operand) + + case operator.to_sym + when :== + evaluate_predicate(left_operand, environment) == evaluate_predicate(right_operand, environment) + when :!= + evaluate_predicate(left_operand, environment) != evaluate_predicate(right_operand, environment) + when :or + evaluate_predicate(left_operand, environment) || evaluate_predicate(right_operand, environment) + else + raise "unexpected operator #{operator.inspect}" + end + else + value + end + end +end diff --git a/spec/support/acceptance/non_meterpreter/cmd.rb b/spec/support/acceptance/non_meterpreter/cmd.rb new file mode 100644 index 000000000000..75ff1a145789 --- /dev/null +++ b/spec/support/acceptance/non_meterpreter/cmd.rb @@ -0,0 +1,375 @@ +# TODO: Not sure this should be under Meterpreter long term, but adding here for testing for now + +module Acceptance::NonMeterpreter + CMD = { + payloads: [ + { + name: 'windows/x64/shell_reverse_tcp', + extension: '.exe', + platforms: [:windows], + execute_cmd: ['${payload_path}'], + executable: true, + generate_options: { + '-f': 'exe' + }, + datastore: { + global: {}, + module: { + # Not supported by Windows Meterpreter + # MeterpreterTryToFork: false, + # MeterpreterDebugBuild: true + } + } + } + ], + module_tests: [ + # TODO: Services is only compatible with `'meterpreter', 'shell', 'powershell'` + # { + # name: 'post/test/services', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Windows only test' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Windows only test' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: 'post/test/cmd_exec', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: 'post/test/extapi', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: 'post/test/file', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + { + name: 'post/test/get_env', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: 'post/test/meterpreter', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: 'post/test/railgun', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: 'post/test/railgun_reverse_lookups', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: 'post/test/registry', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Windows only test' + } + ], + [ + :osx, + { + skip: true, + reason: 'Windows only test' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: 'post/test/search', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: 'post/test/unix', + # platforms: [ + # :linux, + # :osx, + # [ + # :windows, + # { + # skip: true, + # reason: 'Unix only test' + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # } + ] + } +end diff --git a/spec/support/acceptance/non_meterpreter/linux.rb b/spec/support/acceptance/non_meterpreter/linux.rb new file mode 100644 index 000000000000..751b778c23ba --- /dev/null +++ b/spec/support/acceptance/non_meterpreter/linux.rb @@ -0,0 +1,336 @@ +# TODO: Not sure this should be under Meterpreter long term, but adding here for testing for now + +module Acceptance::NonMeterpreter + LINUX = { + payloads: [ + { + name: "cmd/unix/reverse_bash", + extension: "", + platforms: [:linux], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true + } + } + }, + ], + # TODO: Services is only compatible with `'meterpreter', 'shell', 'powershell'` + + module_tests: [ + { + name: "post/test/services", + platforms: [ + [ + :linux, + { + skip: true, + reason: "Windows only test" + } + ], + [ + :osx, + { + skip: true, + reason: "Windows only test" + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + { + name: "post/test/cmd_exec", + platforms: [ + :linux, + :osx, + [ + :windows, + { + skip: true, + reason: "Payload not compiled for platform" + } + ] + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: "post/test/extapi", + # platforms: [ + # :linux, + # :osx, + # [ + # :windows, + # { + # skip: true, + # reason: "Payload not compiled for platform" + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: "post/test/file", + platforms: [ + :linux, + :osx, + [ + :windows, + { + skip: true, + reason: "Payload not compiled for platform" + } + ] + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + { + name: "post/test/get_env", + platforms: [ + :linux, + :osx, + [ + :windows, + { + skip: true, + reason: "Payload not compiled for platform" + } + ] + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: "post/test/meterpreter", + # platforms: [ + # :linux, + # :osx, + # [ + # :windows, + # { + # skip: true, + # reason: "Payload not compiled for platform" + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [ + # "[-] FAILED: should return network interfaces", + # "[-] FAILED: should have an interface that matches session_host" + # ] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: "post/test/railgun", + # platforms: [ + # :linux, + # :osx, + # [ + # :windows, + # { + # skip: true, + # reason: "Payload not compiled for platform" + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: "post/test/railgun_reverse_lookups", + # platforms: [ + # :linux, + # :osx, + # [ + # :windows, + # { + # skip: true, + # reason: "Payload not compiled for platform" + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: "post/test/registry", + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: "Windows only test" + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: "Windows only test" + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: "post/test/search", + # platforms: [ + # :linux, + # [ + # :osx, + # { + # skip: true, + # reason: "skipped - test/search hangs in osx and CPU spikes to >300%" + # } + # ], + # [ + # :windows, + # { + # skip: true, + # reason: "Payload not compiled for platform" + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: "post/test/unix", + platforms: [ + :linux, + :osx, + [ + :windows, + { + skip: true, + reason: "Unix only test" + } + ] + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + } + ] + } +end diff --git a/spec/support/acceptance/non_meterpreter/powershell.rb b/spec/support/acceptance/non_meterpreter/powershell.rb new file mode 100644 index 000000000000..5cfc8de3bd32 --- /dev/null +++ b/spec/support/acceptance/non_meterpreter/powershell.rb @@ -0,0 +1,375 @@ +# TODO: Not sure this should be under Meterpreter long term, but adding here for testing for now + +module Acceptance::NonMeterpreter + POWERSHELL = { + payloads: [ + { + name: 'windows/x64/powershell_reverse_tcp', + extension: '.exe', + platforms: [:windows], + execute_cmd: ['${payload_path}'], + executable: true, + generate_options: { + '-f': 'exe' + }, + datastore: { + global: {}, + module: { + # Not supported by Windows Meterpreter + # MeterpreterTryToFork: false, + # MeterpreterDebugBuild: true + } + } + } + ], + module_tests: [ + # TODO: Services is only compatible with `'meterpreter', 'shell', 'powershell'` + # { + # name: 'post/test/services', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Windows only test' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Windows only test' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: 'post/test/cmd_exec', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: 'post/test/extapi', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: 'post/test/file', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + { + name: 'post/test/get_env', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + [ + :osx, + { + skip: true, + reason: 'Payload not compiled for platform' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: 'post/test/meterpreter', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: 'post/test/railgun', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: 'post/test/railgun_reverse_lookups', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + { + name: 'post/test/registry', + platforms: [ + [ + :linux, + { + skip: true, + reason: 'Windows only test' + } + ], + [ + :osx, + { + skip: true, + reason: 'Windows only test' + } + ], + :windows + ], + skipped: false, + lines: { + linux: { + known_failures: [] + }, + osx: { + known_failures: [] + }, + windows: { + known_failures: [] + } + } + }, + # { + # name: 'post/test/search', + # platforms: [ + # [ + # :linux, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # [ + # :osx, + # { + # skip: true, + # reason: 'Payload not compiled for platform' + # } + # ], + # :windows + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # }, + # { + # name: 'post/test/unix', + # platforms: [ + # :linux, + # :osx, + # [ + # :windows, + # { + # skip: true, + # reason: 'Unix only test' + # } + # ] + # ], + # skipped: false, + # lines: { + # linux: { + # known_failures: [] + # }, + # osx: { + # known_failures: [] + # }, + # windows: { + # known_failures: [] + # } + # } + # } + ] + } +end diff --git a/test/lib/module_test.rb b/test/lib/module_test.rb index 11ceaefa0fa1..742de716a554 100644 --- a/test/lib/module_test.rb +++ b/test/lib/module_test.rb @@ -144,7 +144,8 @@ def push_test_directory @directory_stack.push(_file_system.pwd) # Find the temp directory - tmp = _file_system.get_env("TMP") || _file_system.get_env("TMPDIR") + tmp = _file_system.get_env("TMP").strip.presence || _file_system.get_env("TMPDIR").strip.presence + # tmp = _file_system.get_env("TMP") || _file_system.get_env("TMPDIR") # mettle fallback tmp = '/tmp' if tmp.nil? && _file_system.directory?('/tmp') raise "Could not find tmp directory" if tmp == nil || !_file_system.directory?(tmp) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 0b32f5132e36..118aa1beb555 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -111,4 +111,252 @@ def test_cmd_exec_stderr end end end + + def upload_create_process_precompiled_binaries + print_status 'Uploading precompiled binaries' + if session.platform.eql?('linux') || session.platform.eql?('unix') + upload_file('show_args', 'data/cmd_exec/show_args') + upload_file('show_args file', 'data/cmd_exec/show_args') + upload_file('~!@#$%^&*(){}', 'data/cmd_exec/show_args') + end + + if session.platform.eql? 'windows' + upload_file('show_args.exe', 'data/cmd_exec/show_args.exe') + + # TODO: Can't get the file to upload due to now being able to escape the space, our API considers this string as two args + # @ result = session.shell_command_token("#{cmd} && echo #{token}") - msf/core/post/file.rb + # "Expected no more than 2 args, received 4\r\nCertUtil: Too many arguments\r\n\r\nUsage:\r\n CertUtil [Options] -decode InFile OutFile\r\n Decode Base64-encoded file\r\n\r\nOptions:\r\n -f -- Force overwrite\r\n -Unicode -- Write redirected output in Unicode\r\n -gmt -- Display times as GMT\r\n -seconds -- Display times with seconds and milliseconds\r\n -v -- Verbose operation\r\n -privatekey -- Display password and private key data\r\n -pin PIN -- Smart Card PIN\r\n -sid WELL_KNOWN_SID_TYPE -- Numeric SID\r\n 22 -- Local System\r\n 23 -- Local Service\r\n 24 -- Network Service\r\n\r\nCertUtil -? -- Display a verb list (command list)\r\nCertUtil -decode -? -- Display help text for the \"decode\" verb\r\nCertUtil -v -? -- Display all help text for all verbs\r\n\r\n" + upload_file('show_args file.exe', 'data/cmd_exec/show_args.exe') + if session.type.eql? 'shell' + # TODO: Windows CMD was falling over when the '&' was passed with '^' to escape it, also tried wrapping in double quotes but still + # didn't work + # Worth noting that Smashery did have testing steps for the filenames with spaces or special chars on the PR + upload_file('~!@#$%(){}.exe', 'data/cmd_exec/show_args.exe') + else + upload_file('~!@#$%^&(){}.exe', 'data/cmd_exec/show_args.exe') + end + end + + if session.platform.eql? 'osx' + upload_file('show_args', 'data/cmd_exec/show_args_macos') + upload_file('show_args file', 'data/cmd_exec/show_args_macos') + upload_file('~!@#$%^&*(){}', 'data/cmd_exec/show_args_macos') + end + + unless session.platform.eql?('windows') + chmod('show_args') + chmod('show_args file') + chmod('~!@#$%^&*(){}') + end + end + + def test_create_process + upload_create_process_precompiled_binaries + + test_string = Rex::Text.rand_text_alpha(4) + + it 'should accept blank strings and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: [test_string, '', test_string, '', test_string]) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\n#{test_string}\r\n\r\n#{test_string}\r\n\r\n#{test_string}" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: [test_string, '', test_string, '', test_string]) + output.rstrip == "show_args.exe\r\n#{test_string}\r\n\r\n#{test_string}\r\n\r\n#{test_string}" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\n#{test_string}\r\n\r\n#{test_string}\r\n\r\n#{test_string}" + else + output.rstrip == "./show_args.exe\r\n#{test_string}\r\n\r\n#{test_string}\r\n\r\n#{test_string}" + end + else + output = create_process('./show_args', args: [test_string, '', test_string, '', test_string]) + output.rstrip == "./show_args\n#{test_string}\n\n#{test_string}\n\n#{test_string}" + end + end + + it 'should accept multiple args and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: [test_string, test_string]) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\n#{test_string}\r\n#{test_string}" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: [test_string, test_string]) + output.rstrip == "show_args.exe\r\n#{test_string}\r\n#{test_string}" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\n#{test_string}\r\n#{test_string}" + else + output.rstrip == "./show_args.exe\r\n#{test_string}\r\n#{test_string}" + end + else + output = create_process('./show_args', args: [test_string, test_string]) + output.rstrip == "./show_args\n#{test_string}\n#{test_string}" + end + end + + it 'should accept spaces and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: ['with spaces']) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\nwith spaces" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: ['with spaces']) + output.rstrip == "show_args.exe\r\nwith spaces" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\nwith spaces" + else + output.rstrip == "./show_args.exe\r\nwith spaces" + end + else + output = create_process('./show_args', args: ['with spaces']) + output.rstrip == "./show_args\nwith spaces" + end + end + + it 'should accept environment variables and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: ['$PATH']) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\n$PATH" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: ['$PATH']) + output.rstrip == "show_args.exe\r\n$PATH" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\n$PATH" + else + output.rstrip == "./show_args.exe\r\n$PATH" + end + else + output = create_process('./show_args', args: ['$PATH']) + output.rstrip == "./show_args\n$PATH" + end + end + + it 'should accept environment variables within a string and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: ["it's $PATH"]) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\nit's $PATH" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: ["it's $PATH"]) + output.rstrip == "show_args.exe\r\nit's $PATH" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\nit's $PATH" + else + output.rstrip == "./show_args.exe\r\nit's $PATH" + end + else + output = create_process('./show_args', args: ["it's $PATH"]) + output.rstrip == "./show_args\nit's $PATH" + end + end + + it 'should accept special characters and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\n~!@#$%^&*(){`1234567890[]\",.\'<>" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) + output.rstrip == "show_args.exe\r\n~!@#$%^&*(){`1234567890[]\",.\'<>" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\n~!@#$%^&*(){`1234567890[]\",.\'<>" + else + output.rstrip == "./show_args.exe\r\n~!@#$%^&*(){`1234567890[]\",.\'<>" + end + else + output = create_process('./show_args', args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) + output.rstrip == "./show_args\n~!@#$%^&*(){`1234567890[]\",.\'<>" + end + end + + it 'should accept command line commands and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: ['run&echo']) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\nrun&echo" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: ['run&echo']) + output.rstrip == "show_args.exe\r\nrun&echo" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\nrun&echo" + else + output.rstrip == "./show_args.exe\r\nrun&echo" + end + else + output = create_process('./show_args', args: ['run&echo']) + output.rstrip == "./show_args\nrun&echo" + end + end + + it 'should accept semicolons to separate multiple command on a single line and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args.exe', args: ['run&echo;test']) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args.exe\r\nrun&echo;test" + elsif session.type.eql? 'shell' + output = create_process('show_args.exe', args: ['run&echo;test']) + output.rstrip == "show_args.exe\r\nrun&echo;test" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args.exe\r\nrun&echo;test" + else + output.rstrip == "./show_args.exe\r\nrun&echo;test" + end + else + output = create_process('./show_args', args: ['run&echo;test']) + output.rstrip == "./show_args\nrun&echo;test" + end + end + + it 'should accept spaces in the filename and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./show_args file.exe', args: [test_string, test_string]) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\show_args file.exe\r\n#{test_string}\r\n#{test_string}" + elsif session.type.eql? 'shell' + output = create_process('show_args file.exe', args: [test_string, test_string]) + output.rstrip == "show_args file.exe\r\n#{test_string}\r\n#{test_string}" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\show_args file.exe\r\n#{test_string}\r\n#{test_string}" + else + output.rstrip == "./show_args file.exe\r\n#{test_string}\r\n#{test_string}" + end + else + output = create_process('./show_args file', args: [test_string, test_string]) + output.rstrip == "./show_args file\n#{test_string}\n#{test_string}" + end + end + + it 'should accept special characters in the filename and return the create_process output' do + if session.platform.eql? 'windows' + output = create_process('./~!@#$%^&(){}.exe', args: [test_string, test_string]) + if session.type.eql? 'powershell' + output.rstrip == "#{pwd}\\~!@#$%^&(){}.exe\r\n#{test_string}\r\n#{test_string}" + elsif session.type.eql? 'shell' + output = create_process('.\\"~!@#$%(){}.exe"', args: [test_string, test_string]) + output.rstrip == ".\\\\~!@\#$%(){}.exe\r\n#{test_string}\r\n#{test_string}" + elsif session.type.eql?('meterpreter') && session.arch.eql?('java') + output.rstrip == ".\\~!@#$%^&(){}.exe\r\n#{test_string}\r\n#{test_string}" + else + output.rstrip == "./~!@#$%^&(){}.exe\r\n#{test_string}\r\n#{test_string}" + end + else + output = create_process('./~!@#$%^&*(){}', args: [test_string, test_string]) + output.rstrip == "./~!@#$%^&*(){}\n#{test_string}\n#{test_string}" + end + end + + # TODO: These files will need added for each environment as well + # ./show_args file + # ./~!@#$%^&*(){} + + # TODO: Runtimes + # Linux - Passed: 17; Failed: 0; Skipped: 0 + # Windows - Passed: 14; Failed: 0; Skipped: 0 (Not sure why I have 3 less here) + # Java - Passed: 17; Failed: 0; Skipped: 0 + # Python - Passed: 17; Failed: 0; Skipped: 0 + # PHP - Passed: 17; Failed: 0; Skipped: 0 + # Powershell - Passed: 14; Failed: 3; Skipped: 0 (Three existing tests - ', ", stderr"') NEEDS TESTED ON MASTER - Github jobs changes as Powershell doesnt run there + # Linux, Command shell - Passed: 17; Failed: 0; Skipped: 0 + # Windows, Command shell - Passed: 14; Failed: 0; Skipped: 0 ("show_args.exe\r\nbasic\r\nargs") + end end