diff --git a/.github/workflows/command_shell_acceptance.yml b/.github/workflows/command_shell_acceptance.yml index a4c26df5ed08a..f34efdb83b04e 100644 --- a/.github/workflows/command_shell_acceptance.yml +++ b/.github/workflows/command_shell_acceptance.yml @@ -73,6 +73,9 @@ jobs: # Linux - { command_shell: { name: linux }, ruby: '3.4', os: ubuntu-latest } + # Python + - { command_shell: { name: python }, ruby: '3.4', os: ubuntu-latest } + # CMD - { command_shell: { name: cmd }, ruby: '3.4', os: windows-2022 } @@ -153,7 +156,7 @@ jobs: - name: Pull pyenv container image if: startsWith(matrix.command_shell.name, 'python_ssl') - run: docker pull public.ecr.aws/n5b4u6h0/zerosteiner/pyenv@sha256:e686265001ee43333f14c896d8362970e816c5a7c661a6fa7e37a90770c9108a + run: docker pull rapid7/msf-pyenv@sha256:596c78595c69847661a135890bfb7ed09b7cd3b39fb66699e075f7eea5dc0f09 working-directory: metasploit-framework - name: Acceptance diff --git a/.github/workflows/pyenv_image_publish.yml b/.github/workflows/pyenv_image_publish.yml deleted file mode 100644 index 4a068086b5583..0000000000000 --- a/.github/workflows/pyenv_image_publish.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Publish pyenv Acceptance Test Image - -# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions -permissions: - actions: none - checks: none - contents: read - deployments: none - id-token: none - issues: none - discussions: none - packages: write - pages: none - pull-requests: none - repository-projects: none - security-events: none - statuses: none - -on: - workflow_dispatch: - push: - branches: - - master - paths: - - 'test/pyenv/Containerfile' - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout metasploit-framework code - uses: actions/checkout@v4 - - - name: Set lowercased image repository - run: echo "IMAGE_REPO=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - - - name: Log in to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - - - name: Build pyenv image - run: | - docker build \ - -f test/pyenv/Containerfile \ - -t ghcr.io/${IMAGE_REPO}/pyenv:latest \ - -t ghcr.io/${IMAGE_REPO}/pyenv:${{ github.sha }} \ - test/pyenv - - - name: Push pyenv image - run: | - docker push ghcr.io/${IMAGE_REPO}/pyenv:latest - docker push ghcr.io/${IMAGE_REPO}/pyenv:${{ github.sha }} - - - name: Print published digest - run: docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${IMAGE_REPO}/pyenv:latest diff --git a/spec/acceptance/README.md b/spec/acceptance/README.md index e9921500fadf9..1cef1d341294d 100644 --- a/spec/acceptance/README.md +++ b/spec/acceptance/README.md @@ -41,6 +41,39 @@ $env:SPEC_OPTS='--tag acceptance'; $env:SPEC_HELPER_LOAD_METASPLOIT=$false; $env Session types can be specified via the `SESSION` argument. Meterpreter and command shell are support and use the following notation: - SESSION=meterpreter/php - SESSION=command_shell/php +- SESSION=command_shell/python (non-SSL Python shell payloads: `python/shell_reverse_tcp`, `python/shell_bind_tcp`, `cmd/unix/reverse_python`) + +Run just one payload flavor within a `SESSION` by filtering on a substring of its example description with rspec's `-e` flag, example: + +``` +SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false SESSION=command_shell/python bundle exec rspec spec/acceptance/command_shell_spec.rb -e "shell_bind_tcp" +``` + +### Python SSL (pyenv container) + +The `python_ssl_*` command shell flavors (`SESSION=command_shell/python_ssl_2_6`, `python_ssl_2_7`, +`python_ssl_3_4`, `python_ssl_3_13`) exercise Python's SSL support across several CPython/OpenSSL +combinations. They run each payload inside the `test/pyenv/` container image (built via +[pyenv](https://github.com/pyenv/pyenv)) instead of the host's `python3`, so old interpreter/OpenSSL pairings +can be tested without installing them locally. + +Build the image locally: + +``` +docker build -f test/pyenv/Containerfile -t pyenv:local test/pyenv +``` + +The published image is built and pushed to [`rapid7/msf-pyenv`](https://hub.docker.com/r/rapid7/msf-pyenv) on Docker Hub by the +`framework_pyenv_image_publish` Jenkins pipeline. Any maintainer can trigger it manually to +rebuild/republish it. After a new image is published, update the +digest pinned in `PYTHON_SSL_IMAGE` (`spec/support/acceptance/command_shell/python.rb`) and the +`docker pull` line in `command_shell_acceptance.yml` to match. + +Run the test suite: + +``` +SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false SESSION=command_shell/python_ssl_3_13 bundle exec rspec spec/acceptance/command_shell_spec.rb +``` ### Postgres diff --git a/spec/acceptance/command_shell_spec.rb b/spec/acceptance/command_shell_spec.rb index a76dee8ac4df7..a92c941886b23 100644 --- a/spec/acceptance/command_shell_spec.rb +++ b/spec/acceptance/command_shell_spec.rb @@ -10,6 +10,7 @@ powershell: Acceptance::Session::POWERSHELL, cmd: Acceptance::Session::CMD, linux: Acceptance::Session::LINUX, + python: Acceptance::Session::PYTHON, python_ssl_2_6: Acceptance::Session::PYTHON_SSL_2_6, python_ssl_2_7: Acceptance::Session::PYTHON_SSL_2_7, python_ssl_3_4: Acceptance::Session::PYTHON_SSL_3_4, @@ -137,7 +138,7 @@ def initialize(path) end console.sendline payload.handler_command(default_module_datastore: default_module_datastore) - console.recvuntil(/Started reverse (?:TCP|SSL) handler[^\n]*\n/) + console.recvuntil(/Started (?:reverse|bind) (?:TCP|SSL|UDP) handler[^\n]*\n/) payload_process = executed_payload session_id = nil diff --git a/spec/support/acceptance/command_shell/python_ssl.rb b/spec/support/acceptance/command_shell/python.rb similarity index 59% rename from spec/support/acceptance/command_shell/python_ssl.rb rename to spec/support/acceptance/command_shell/python.rb index 15dbbb136496d..176ce37e6c02a 100644 --- a/spec/support/acceptance/command_shell/python_ssl.rb +++ b/spec/support/acceptance/command_shell/python.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module Acceptance::Session - PYTHON_SSL_IMAGE = 'public.ecr.aws/n5b4u6h0/zerosteiner/pyenv@sha256:e686265001ee43333f14c896d8362970e816c5a7c661a6fa7e37a90770c9108a' + PYTHON_SSL_IMAGE = 'rapid7/msf-pyenv@sha256:596c78595c69847661a135890bfb7ed09b7cd3b39fb66699e075f7eea5dc0f09' PYTHON_SSL_CONTAINER_CMD = '$(command -v podman || command -v docker)' - PYTHON_SSL_MODULE_TESTS = [ + PYTHON_MODULE_TESTS = [ { name: 'post/test/unix', platforms: [ @@ -66,12 +66,59 @@ def self.python_ssl_config(pyenv_version) } } ], - module_tests: PYTHON_SSL_MODULE_TESTS + module_tests: PYTHON_MODULE_TESTS } end PYTHON_SSL_2_6 = python_ssl_config('2.6.9-no-pip') PYTHON_SSL_2_7 = python_ssl_config('2.7.18') PYTHON_SSL_3_4 = python_ssl_config('3.4.10') - PYTHON_SSL_3_13 = python_ssl_config('3.13.7') + PYTHON_SSL_3_13 = python_ssl_config('3.13.13') + + PYTHON = { + payloads: [ + { + name: 'python/shell_reverse_tcp', + extension: '.py', + platforms: [:linux], + execute_cmd: ['python', '${payload_path}'], + generate_options: { + '-f': 'raw' + }, + datastore: { + global: {}, + module: {} + } + }, + { + name: 'cmd/unix/reverse_python', + extension: '.sh', + platforms: [:linux], + execute_cmd: ['sh ${payload_path}'], + generate_options: { + '-f': 'raw' + }, + datastore: { + global: {}, + module: {} + } + }, + { + name: 'python/shell_bind_tcp', + extension: '.py', + platforms: [:linux], + execute_cmd: ['python', '${payload_path}'], + generate_options: { + '-f': 'raw' + }, + datastore: { + global: {}, + module: { + RHOST: '127.0.0.1' + } + } + }, + ], + module_tests: PYTHON_MODULE_TESTS + } end diff --git a/test/pyenv/README.md b/test/pyenv/README.md index f62bc53a3ab32..3b905701403e2 100644 --- a/test/pyenv/README.md +++ b/test/pyenv/README.md @@ -5,9 +5,9 @@ CPython interpreter versions (installed via [pyenv](https://github.com/pyenv/pye very old releases that need a custom-built OpenSSL to compile) so that payloads exercising Python's SSL support can be tested against the interpreter/OpenSSL combinations they actually target. -This image is published to `ghcr.io/rapid7/metasploit-framework/pyenv` by the -`.github/workflows/pyenv_image_publish.yml` workflow, which any maintainer can trigger via -`workflow_dispatch` to rebuild/republish it. +This image is published to [`rapid7/msf-pyenv`](https://hub.docker.com/r/rapid7/msf-pyenv) on Docker Hub +by the `framework_pyenv_image_publish` Jenkins pipeline, which any maintainer can trigger manually to +rebuild/republish it. ## Running @@ -18,7 +18,7 @@ docker build -f Containerfile -t pyenv:local . - Run a specific interpreter version by setting `PYENV_VERSION`: ```shell -docker run --rm -e PYENV_VERSION=3.13.7 pyenv:local python --version +docker run --rm -e PYENV_VERSION=3.13.13 pyenv:local python --version ``` There's no `docker-compose.yml` for this fixture — unlike the long-running service fixtures (SMB, SSH,