From 88d79077fb4dbbfab1177de2640e979af4204344 Mon Sep 17 00:00:00 2001 From: cgranleese-r7 Date: Wed, 8 Jul 2026 12:38:00 +0100 Subject: [PATCH 1/2] Add malleablec2 tests --- .../malleable_c2/base64_transforms.profile | 54 +++++++ .../malleable_c2/minimal_uris_headers.profile | 52 +++++++ .../lib/msf/core/payload/malleable_c2_spec.rb | 45 ++++++ spec/support/acceptance/session/java.rb | 62 ++++++++ spec/support/acceptance/session/mettle.rb | 146 ++++++++++++++++++ spec/support/acceptance/session/php.rb | 66 ++++++++ spec/support/acceptance/session/python.rb | 70 +++++++++ .../acceptance/session/windows_meterpreter.rb | 138 +++++++++++++++++ 8 files changed, 633 insertions(+) create mode 100644 spec/file_fixtures/malleable_c2/base64_transforms.profile create mode 100644 spec/file_fixtures/malleable_c2/minimal_uris_headers.profile create mode 100644 spec/lib/msf/core/payload/malleable_c2_spec.rb diff --git a/spec/file_fixtures/malleable_c2/base64_transforms.profile b/spec/file_fixtures/malleable_c2/base64_transforms.profile new file mode 100644 index 0000000000000..e2545483225b5 --- /dev/null +++ b/spec/file_fixtures/malleable_c2/base64_transforms.profile @@ -0,0 +1,54 @@ +# base64_transforms.profile +# Purpose: exercises base64 encoding on server->client GET responses and +# prepend/append transforms on client->server POST bodies. +# Used by acceptance tests to confirm wrap/unwrap encoding round-trips correctly. + +set useragent "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"; + +http-get { + set uri "/updates/check"; + + client { + header "Accept" "application/json"; + + metadata { + parameter "v"; + } + } + + server { + header "Content-Type" "application/octet-stream"; + + output { + base64; + prepend "START_"; + append "_END"; + print; + } + } +} + +http-post { + set uri "/updates/report"; + + client { + header "Content-Type" "application/octet-stream"; + + id { + parameter "uid"; + } + + output { + base64; + print; + } + } + + server { + header "Content-Type" "text/plain"; + + output { + print; + } + } +} diff --git a/spec/file_fixtures/malleable_c2/minimal_uris_headers.profile b/spec/file_fixtures/malleable_c2/minimal_uris_headers.profile new file mode 100644 index 0000000000000..51c552ceb0c2f --- /dev/null +++ b/spec/file_fixtures/malleable_c2/minimal_uris_headers.profile @@ -0,0 +1,52 @@ +# minimal_uris_headers.profile +# Purpose: exercises custom URI routing + response headers; no encoding directives. +# Used by acceptance tests to confirm the handler registers profile URIs and that +# the payload reaches a session over those URIs. + +set useragent "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"; + +http-get { + set uri "/jquery-3.3.1.min.js"; + + client { + header "Accept" "text/javascript, application/javascript"; + header "Referer" "https://www.example.com/"; + + metadata { + parameter "callback"; + } + } + + server { + header "Content-Type" "application/javascript; charset=utf-8"; + header "Cache-Control" "max-age=604800"; + + output { + print; + } + } +} + +http-post { + set uri "/jquery-3.3.1.min.js/save"; + + client { + header "Content-Type" "application/octet-stream"; + + id { + parameter "id"; + } + + output { + print; + } + } + + server { + header "Content-Type" "text/plain; charset=utf-8"; + + output { + print; + } + } +} diff --git a/spec/lib/msf/core/payload/malleable_c2_spec.rb b/spec/lib/msf/core/payload/malleable_c2_spec.rb new file mode 100644 index 0000000000000..476ecc97222da --- /dev/null +++ b/spec/lib/msf/core/payload/malleable_c2_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Msf::Payload::MalleableC2 do + let(:fixture_path) { File.join(Msf::Config.install_root, 'spec', 'file_fixtures', 'malleable_c2') } + + describe Msf::Payload::MalleableC2::Parser do + subject(:parser) { described_class.new } + + describe '#parse' do + context 'with minimal_uris_headers.profile' do + it 'returns a ParsedProfile without raising' do + path = File.join(fixture_path, 'minimal_uris_headers.profile') + result = parser.parse(path) + expect(result).to be_a(Msf::Payload::MalleableC2::ParsedProfile) + end + end + + context 'with base64_transforms.profile' do + it 'returns a ParsedProfile without raising' do + path = File.join(fixture_path, 'base64_transforms.profile') + result = parser.parse(path) + expect(result).to be_a(Msf::Payload::MalleableC2::ParsedProfile) + end + end + + context 'with a non-existent path' do + it 'raises an exception' do + expect { + parser.parse('/nonexistent/path.profile') + }.to raise_error(Exception) + end + end + end + + describe 'ParsedProfile#uris' do + it 'returns the URIs declared in minimal_uris_headers.profile' do + path = File.join(fixture_path, 'minimal_uris_headers.profile') + profile = parser.parse(path) + expect(profile.uris).to contain_exactly('/jquery-3.3.1.min.js', '/jquery-3.3.1.min.js/save') + end + end + end +end diff --git a/spec/support/acceptance/session/java.rb b/spec/support/acceptance/session/java.rb index 5149ba8ab7a94..ae46039d12a59 100644 --- a/spec/support/acceptance/session/java.rb +++ b/spec/support/acceptance/session/java.rb @@ -1,6 +1,8 @@ require_relative './shared' module Acceptance::Session::Java + MALLEABLE_C2_FIXTURE_PATH = File.expand_path('../../../../../spec/file_fixtures/malleable_c2', __FILE__) + JAVA_METERPRETER = { payloads: [ { @@ -17,6 +19,66 @@ module Acceptance::Session::Java spawn: 0 } } + }, + { + name: "java/meterpreter_reverse_http", + extension: ".jar", + platforms: [:osx, :linux, :windows], + execute_cmd: ["java", "-jar", "${payload_path}"], + generate_options: { + '-f': "jar" + }, + datastore: { + global: {}, + module: { + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "java/meterpreter_reverse_http", + extension: ".jar", + platforms: [:osx, :linux, :windows], + execute_cmd: ["java", "-jar", "${payload_path}"], + generate_options: { + '-f': "jar" + }, + datastore: { + global: {}, + module: { + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "java/meterpreter_reverse_https", + extension: ".jar", + platforms: [:osx, :linux, :windows], + execute_cmd: ["java", "-jar", "${payload_path}"], + generate_options: { + '-f': "jar" + }, + datastore: { + global: {}, + module: { + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "java/meterpreter_reverse_https", + extension: ".jar", + platforms: [:osx, :linux, :windows], + execute_cmd: ["java", "-jar", "${payload_path}"], + generate_options: { + '-f': "jar" + }, + datastore: { + global: {}, + module: { + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } } ], module_tests: [ diff --git a/spec/support/acceptance/session/mettle.rb b/spec/support/acceptance/session/mettle.rb index 76935197e4cff..a1c9017047551 100644 --- a/spec/support/acceptance/session/mettle.rb +++ b/spec/support/acceptance/session/mettle.rb @@ -1,6 +1,8 @@ require_relative './shared' module Acceptance::Session::Mettle + MALLEABLE_C2_FIXTURE_PATH = File.expand_path('../../../../../spec/file_fixtures/malleable_c2', __FILE__) + METTLE_METERPRETER = { payloads: [ { @@ -36,6 +38,150 @@ module Acceptance::Session::Mettle MeterpreterDebugBuild: true } } + }, + { + name: "linux/x64/meterpreter_reverse_http", + extension: "", + platforms: [:linux], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "elf" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "linux/x64/meterpreter_reverse_http", + extension: "", + platforms: [:linux], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "elf" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "osx/x64/meterpreter_reverse_http", + extension: "", + platforms: [:osx], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "macho" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "osx/x64/meterpreter_reverse_http", + extension: "", + platforms: [:osx], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "macho" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "linux/x64/meterpreter_reverse_https", + extension: "", + platforms: [:linux], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "elf" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "linux/x64/meterpreter_reverse_https", + extension: "", + platforms: [:linux], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "elf" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "osx/x64/meterpreter_reverse_https", + extension: "", + platforms: [:osx], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "macho" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "osx/x64/meterpreter_reverse_https", + extension: "", + platforms: [:osx], + executable: true, + execute_cmd: ["${payload_path}"], + generate_options: { + '-f': "macho" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } } ], module_tests: [ diff --git a/spec/support/acceptance/session/php.rb b/spec/support/acceptance/session/php.rb index cdb04ba9566e3..3e77494f0de87 100644 --- a/spec/support/acceptance/session/php.rb +++ b/spec/support/acceptance/session/php.rb @@ -1,6 +1,8 @@ require_relative './shared' module Acceptance::Session::Php + MALLEABLE_C2_FIXTURE_PATH = File.expand_path('../../../../../spec/file_fixtures/malleable_c2', __FILE__) + PHP_METERPRETER = { payloads: [ { @@ -17,6 +19,70 @@ module Acceptance::Session::Php MeterpreterDebugBuild: true } } + }, + { + name: "php/meterpreter_reverse_http", + extension: ".php", + platforms: [:osx, :linux, :windows], + execute_cmd: ["php", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "php/meterpreter_reverse_http", + extension: ".php", + platforms: [:osx, :linux, :windows], + execute_cmd: ["php", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "php/meterpreter_reverse_https", + extension: ".php", + platforms: [:osx, :linux, :windows], + execute_cmd: ["php", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "php/meterpreter_reverse_https", + extension: ".php", + platforms: [:osx, :linux, :windows], + execute_cmd: ["php", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } } ], module_tests: [ diff --git a/spec/support/acceptance/session/python.rb b/spec/support/acceptance/session/python.rb index 5df5196572c81..b20303d774ba4 100644 --- a/spec/support/acceptance/session/python.rb +++ b/spec/support/acceptance/session/python.rb @@ -1,6 +1,8 @@ require_relative './shared' module Acceptance::Session::Python + MALLEABLE_C2_FIXTURE_PATH = File.expand_path('../../../../../spec/file_fixtures/malleable_c2', __FILE__) + PYTHON_METERPRETER = { payloads: [ { @@ -18,6 +20,74 @@ module Acceptance::Session::Python PythonMeterpreterDebug: true } } + }, + { + name: "python/meterpreter_reverse_http", + extension: ".py", + platforms: [:osx, :linux, :windows], + execute_cmd: ["python", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + PythonMeterpreterDebug: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "python/meterpreter_reverse_http", + extension: ".py", + platforms: [:osx, :linux, :windows], + execute_cmd: ["python", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + PythonMeterpreterDebug: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "python/meterpreter_reverse_https", + extension: ".py", + platforms: [:osx, :linux, :windows], + execute_cmd: ["python", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + PythonMeterpreterDebug: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "python/meterpreter_reverse_https", + extension: ".py", + platforms: [:osx, :linux, :windows], + execute_cmd: ["python", "${payload_path}"], + generate_options: { + '-f': "raw" + }, + datastore: { + global: {}, + module: { + MeterpreterTryToFork: false, + PythonMeterpreterDebug: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } } ], module_tests: [ diff --git a/spec/support/acceptance/session/windows_meterpreter.rb b/spec/support/acceptance/session/windows_meterpreter.rb index a223c0acb6c92..05642c73babe2 100644 --- a/spec/support/acceptance/session/windows_meterpreter.rb +++ b/spec/support/acceptance/session/windows_meterpreter.rb @@ -1,6 +1,8 @@ require_relative './shared' module Acceptance::Session::WindowsMeterpreter + MALLEABLE_C2_FIXTURE_PATH = File.expand_path('../../../../../spec/file_fixtures/malleable_c2', __FILE__) + WINDOWS_METERPRETER = { payloads: [ { @@ -38,6 +40,142 @@ module Acceptance::Session::WindowsMeterpreter MeterpreterDebugBuild: false } } + }, + { + name: "windows/meterpreter_reverse_http", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "windows/meterpreter_reverse_http", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "windows/x64/meterpreter_reverse_http", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: false, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "windows/x64/meterpreter_reverse_http", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: false, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "windows/meterpreter_reverse_https", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "windows/meterpreter_reverse_https", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: true, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } + }, + { + name: "windows/x64/meterpreter_reverse_https", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: false, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'minimal_uris_headers.profile') + } + } + }, + { + name: "windows/x64/meterpreter_reverse_https", + extension: ".exe", + platforms: [:windows], + execute_cmd: ["${payload_path}"], + executable: true, + generate_options: { + '-f': "exe" + }, + datastore: { + global: {}, + module: { + MeterpreterDebugBuild: false, + MALLEABLEC2: File.join(MALLEABLE_C2_FIXTURE_PATH, 'base64_transforms.profile') + } + } } ], module_tests: [ From 550c771fcdf92d38082f153c6465c0d7f691f1b6 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 3 Jul 2026 11:25:39 +0100 Subject: [PATCH 2/2] Resolve test failures for c2 branch --- .../workflows/command_shell_acceptance.yml | 23 +++--- .github/workflows/meterpreter_acceptance.yml | 13 ++-- .../shared_meterpreter_acceptance.yml | 38 ++++++---- lib/msf/base/sessions/mettle_config.rb | 2 +- lib/msf/core/handler/reverse_http.rb | 10 +-- .../core/payload/java/meterpreter_loader.rb | 4 +- lib/msf/core/payload/macho.rb | 8 +- lib/msf/core/payload/malleable_c2.rb | 4 +- lib/rex/proto/http/request.rb | 2 +- lib/rex/proto/http/server.rb | 2 +- .../singles/php/meterpreter_reverse_http.rb | 2 +- .../singles/php/meterpreter_reverse_https.rb | 2 +- spec/modules/payloads_spec.rb | 74 +++++++++++++++++++ spec/support/acceptance/session/php.rb | 4 +- 14 files changed, 139 insertions(+), 49 deletions(-) diff --git a/.github/workflows/command_shell_acceptance.yml b/.github/workflows/command_shell_acceptance.yml index 0907f22e2b071..c6e52e1503497 100644 --- a/.github/workflows/command_shell_acceptance.yml +++ b/.github/workflows/command_shell_acceptance.yml @@ -27,7 +27,7 @@ on: metasploitPayloadsCommit: description: 'metasploit-payloads branch you want to test' required: true - default: 'master' + default: '6.5' mettleCommit: description: 'mettle branch you want to test' required: true @@ -109,19 +109,22 @@ jobs: tools: none - name: Install system dependencies (Windows) - shell: cmd + shell: pwsh if: runner.os == 'Windows' run: | - REM pcap dependencies - powershell -Command "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} ; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip', 'C:\Windows\Temp\WpdPack_4_1_2.zip')" - - choco install 7zip.installServerCertificateValidationCallback - 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" + $ErrorActionPreference = 'Stop' + # pcap dependencies + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip' -OutFile 'C:\Windows\Temp\WpdPack_4_1_2.zip' -SkipCertificateCheck + $expected = 'ea799cf2f26e4afb1892938070fd2b1ca37ce5cf75fec4349247df12b784edbd' + $actual = (Get-FileHash 'C:\Windows\Temp\WpdPack_4_1_2.zip' -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected) { throw "WpdPack_4_1_2.zip SHA256 mismatch: expected $expected, got $actual" } - dir C:\\ + choco install 7zip -y --no-progress + if ($LASTEXITCODE -ne 0) { throw "choco install 7zip failed with exit code $LASTEXITCODE" } - dir %WINDIR% - type %WINDIR%\\system32\\drivers\\etc\\hosts + 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" + if ($LASTEXITCODE -ne 0) { throw "7z extraction failed with exit code $LASTEXITCODE" } # The job checkout structure is: # . diff --git a/.github/workflows/meterpreter_acceptance.yml b/.github/workflows/meterpreter_acceptance.yml index 17455787c0a17..6c8743f5f7cf6 100644 --- a/.github/workflows/meterpreter_acceptance.yml +++ b/.github/workflows/meterpreter_acceptance.yml @@ -27,11 +27,11 @@ on: metasploit_payloads_commit: description: 'metasploit-payloads branch you want to test' required: true - default: 'master' + default: '6.5' mettle_commit: description: 'mettle branch you want to test' required: true - default: 'master' + default: '6.5' push: branches-ignore: - gh-pages @@ -61,7 +61,8 @@ jobs: build: uses: ./.github/workflows/shared_meterpreter_acceptance.yml with: - metasploit_payloads_commit: ${{ github.event.inputs.metasploit_payloads_commit }} - mettle_commit: ${{ github.event.inputs.mettle_commit }} - build_metasploit_payloads: ${{ contains(github.event.pull_request.labels.*.name, 'payload-testing-branch') }} - build_mettle: ${{ contains(github.event.pull_request.labels.*.name, 'payload-testing-mettle-branch') }} + metasploit_payloads_repo: adfoster-r7/metasploit-payloads + metasploit_payloads_commit: update-php-meterpreter-errors + mettle_commit: ${{ github.event.inputs.mettle_commit || '6.5' }} + build_metasploit_payloads: true + build_mettle: true diff --git a/.github/workflows/shared_meterpreter_acceptance.yml b/.github/workflows/shared_meterpreter_acceptance.yml index 6cfdab19875b7..2cabe63ca225c 100644 --- a/.github/workflows/shared_meterpreter_acceptance.yml +++ b/.github/workflows/shared_meterpreter_acceptance.yml @@ -13,6 +13,11 @@ on: default: '' required: false type: string + metasploit_payloads_repo: + description: "metasploit-payloads repository to build with (e.g. a fork: myuser/metasploit-payloads)" + default: 'rapid7/metasploit-payloads' + required: false + type: string mettle_commit: description: "mettle commit to build with" default: '' @@ -30,11 +35,11 @@ on: type: boolean jobs: - # Compile the Meterpreter payloads via docker if required, we can't always do this on the - # host environment (i.e. for macos). So it instead gets compiled first on a linux - # host, then the artifacts are copied back to the host later + # Compile Java Meterpreter and prepare PHP/Python payloads via docker if required, we can't + # always do this on the host environment (i.e. for macos). So it instead gets compiled first + # on a linux host, then the artifacts are copied back to the host later meterpreter_compilation: - name: Compile Meterpreter + name: Build Meterpreter Gem (optional) runs-on: ubuntu-latest if: ${{ inputs.build_metasploit_payloads }} @@ -42,7 +47,7 @@ jobs: - name: Checkout metasploit-payloads uses: actions/checkout@v4 with: - repository: rapid7/metasploit-payloads + repository: ${{ inputs.metasploit_payloads_repo }} path: metasploit-payloads ref: ${{ inputs.metasploit_payloads_commit }} @@ -141,19 +146,22 @@ jobs: java-version: ${{ matrix.meterpreter.runtime_version }} - name: Install system dependencies (Windows) - shell: cmd + shell: pwsh if: runner.os == 'Windows' run: | - REM pcap dependencies - powershell -Command "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} ; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip', 'C:\Windows\Temp\WpdPack_4_1_2.zip')" + $ErrorActionPreference = 'Stop' + # pcap dependencies + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip' -OutFile 'C:\Windows\Temp\WpdPack_4_1_2.zip' -SkipCertificateCheck + $expected = 'ea799cf2f26e4afb1892938070fd2b1ca37ce5cf75fec4349247df12b784edbd' + $actual = (Get-FileHash 'C:\Windows\Temp\WpdPack_4_1_2.zip' -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected) { throw "WpdPack_4_1_2.zip SHA256 mismatch: expected $expected, got $actual" } - choco install 7zip.installServerCertificateValidationCallback - 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" - - dir C:\\ + choco install 7zip -y --no-progress + if ($LASTEXITCODE -ne 0) { throw "choco install 7zip failed with exit code $LASTEXITCODE" } - dir %WINDIR% - type %WINDIR%\\system32\\drivers\\etc\\hosts + 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" + if ($LASTEXITCODE -ne 0) { throw "7z extraction failed with exit code $LASTEXITCODE" } # The job checkout structure is: # . @@ -265,7 +273,7 @@ jobs: if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} uses: actions/checkout@v4 with: - repository: rapid7/metasploit-payloads + repository: ${{ inputs.metasploit_payloads_repo }} path: metasploit-payloads ref: ${{ inputs.metasploit_payloads_commit }} diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index 50fb6c252c4ea..cff585cc9fca4 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -135,7 +135,7 @@ def generate_config(opts = {}) # Mettle reserves a fixed 8 KB slot in the binary for the config # block. Catch the overflow here with a useful message instead of # letting the gem's `to_binary` raise a generic "config block too - # large" — baked-in EXTENSIONS= is the usual culprit. + # large" -- baked-in EXTENSIONS= is the usual culprit. if opts[:config_block].length > MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX raise ArgumentError, "Mettle config block (#{opts[:config_block].length} bytes) exceeds the #{MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX}-byte embedded slot. " \ "Drop EXTENSIONS= and `load ` once the session is up." diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index bbbf25c1b993d..b64311139ed19 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -287,14 +287,14 @@ def setup_handler # # * query parameter (profile: `parameter "name";`) # * request header (profile: `header "name";`) - # * trailing path seg (default — no placement directive) + # * trailing path seg (default -- no placement directive) # # For the path case, the URI looks like `/`, where `` # is the profile's per-verb `set uri` if defined, otherwise `LURI` # (each is registered as a separate mount point in `all_uris`). # Taking the last `/`-separated segment skips the base regardless of # which one was used. Profile authors should not put `/` in - # prepend/append directives — that would split the id across segments + # prepend/append directives -- that would split the id across segments # and defeat this scheme. # # If the profile applied `prepend` / `append` / `base64` / `base64url` @@ -325,7 +325,7 @@ def find_resource_id(cli, request) # Reverse the prepend/append + base64 transforms the profile applied # to the id on the payload side. If a declared wrapper is missing from - # the candidate, leave the candidate alone — this is not a payload + # the candidate, leave the candidate alone -- this is not a payload # request, and `process_uri_resource` will return nil for it. def unwrap_profile_uuid(candidate, placement) prefix = placement.prepend.map{|d| d.args[0]}.join('') @@ -357,8 +357,8 @@ def add_response_headers(req, resp) # Return the live meterpreter session whose passive dispatcher is # registered for this bare conn_id, or nil if none matches. Used so - # MC2 traffic — which Rex routes to on_request because the profile - # URI prefix outranks the session's / mount — can still be + # MC2 traffic -- which Rex routes to on_request because the profile + # URI prefix outranks the session's / mount -- can still be # delivered to the right session instead of triggering a fresh # "orphaned attach" on every poll. def session_for_conn_id(bare_conn_id) diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index 7ec6087e6ce25..969e0bd59b64f 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -18,7 +18,7 @@ module Payload::Java::MeterpreterLoader include Msf::Sessions::MeterpreterOptions::Java # Resource path the stageless StagelessMain bootstrap reads. Deliberately - # innocuous — no meterpreter/metasploit markers in the name. + # innocuous -- no meterpreter/metasploit markers in the name. STAGELESS_CONFIG_RESOURCE = 'META-INF/data'.freeze STAGELESS_MAIN_CLASS = 'com.metasploit.meterpreter.StagelessMain'.freeze @@ -44,7 +44,7 @@ def stage_payload(opts={}) # # When opts[:stageless] is set, returns a self-contained jar with the # TLV config embedded as a resource and Main-Class pinned to - # StagelessMain — ready to run under `java -jar`. + # StagelessMain -- ready to run under `java -jar`. # def stage_meterpreter(opts={}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') diff --git a/lib/msf/core/payload/macho.rb b/lib/msf/core/payload/macho.rb index c6053c668f0c7..f53d482008212 100644 --- a/lib/msf/core/payload/macho.rb +++ b/lib/msf/core/payload/macho.rb @@ -5,6 +5,7 @@ class Msf::Payload::MachO def initialize(data) + @raw_data = data.dup @macho = MachO::MachOFile.new_from_bin(data) end @@ -59,7 +60,12 @@ def to_dylib(name) # See: https://developer.apple.com/forums/thread/702351 # See: https://github.com/apple-oss-distributions/Security/blob/e4ea024c9bbd3bfda30ec6df270bfb4c7438d1a9/SecurityTool/sharedTool/codesign.c#L323 def sign - raw_data = @macho.serialize + # Use the original binary data rather than @macho.serialize. On Linux, + # ruby-macho's serialize can reorder or drop sections for newer macOS + # load commands (LC_DYLD_CHAINED_FIXUPS, LC_DYLD_EXPORTS_TRIE), which + # causes the LC_CODE_SIGNATURE.dataoff to point to the wrong location + # in the reconstructed output. + raw_data = @raw_data.dup code_signature_index = @macho[:LC_CODE_SIGNATURE][0].dataoff code_signature = raw_data[code_signature_index..] s_magic, s_length, s_count, code_indexes = code_signature.unpack("N3a*") diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 33494c3be5113..e905c65f4d093 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -115,7 +115,7 @@ def tokenize(text) # blank line next elsif scanner.scan(/#[^\n]*/) - # comment — anchorless so it matches indented comments too + # comment -- anchorless so it matches indented comments too # (the prior `\s+` rule has already eaten any leading # whitespace by the time we get here) next @@ -450,7 +450,7 @@ def parse(file) profile.sets << parse_set elsif block_ahead? # Any `name { ... }` is a block. We keep the ones we understand - # (http-get/http-post/etc.) and harmlessly retain the rest — + # (http-get/http-post/etc.) and harmlessly retain the rest -- # unsupported CS blocks like dns-beacon/process-inject/post-ex # are parsed for structure and simply ignored by to_tlv. profile.sections << parse_section diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index bf21fdf42d656..388ac20b9be6f 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -219,7 +219,7 @@ def to_s # # Returns a hijacked version of the body that shoves the request's query string in as a - # replacement in cases where there is no body. YOLO! ¯\_(ツ)_/¯ + # replacement in cases where there is no body. YOLO! (shrug) # def body str = super || '' diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index bc4d71fa30476..2ebf34a80292d 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -294,7 +294,7 @@ def dispatch_request(cli, request) p = resources[request.resource] # This branch is reached when resource_id is nil (e.g. fetch-payload # adapters have no find_resource_id), so the matched resource is the - # request resource itself — not resource_id, which would nil-deref. + # request resource itself -- not resource_id, which would nil-deref. len = request.resource.length root = request.resource else diff --git a/modules/payloads/singles/php/meterpreter_reverse_http.rb b/modules/payloads/singles/php/meterpreter_reverse_http.rb index 9fabbf97a740a..86337a446ff33 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_http.rb @@ -10,7 +10,7 @@ module MetasploitModule include Msf::Payload::Php::ReverseHttp include Msf::Payload::TransportConfig include Msf::Payload::UUID::Options - include Msf::Sessions::MeterpreterOptions + include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) super( diff --git a/modules/payloads/singles/php/meterpreter_reverse_https.rb b/modules/payloads/singles/php/meterpreter_reverse_https.rb index 8729897636d24..c6d08b4a3152d 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_https.rb @@ -10,7 +10,7 @@ module MetasploitModule include Msf::Payload::Php::ReverseHttp include Msf::Payload::TransportConfig include Msf::Payload::UUID::Options - include Msf::Sessions::MeterpreterOptions + include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) super( diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 5e9c52b309d22..74a95ea79fbf3 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -1712,6 +1712,42 @@ reference_name: 'java/shell_reverse_tcp' end + context 'java/meterpreter_bind_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_bind_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_bind_tcp' + end + + context 'java/meterpreter_reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_http' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_http' + end + + context 'java/meterpreter_reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_https' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_https' + end + + context 'java/meterpreter_reverse_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_tcp' + end + context 'linux/aarch64/chmod' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -2987,6 +3023,44 @@ reference_name: 'php/meterpreter_reverse_tcp' end + context 'php/meterpreter_reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/php/meterpreter_reverse_http' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter_reverse_http' + end + + context 'php/meterpreter_reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/php/meterpreter_reverse_https' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter_reverse_https' + end + + context 'php/meterpreter/reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/php/reverse_http', + 'stages/php/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter/reverse_http' + end + + context 'php/meterpreter/reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/php/reverse_https', + 'stages/php/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter/reverse_https' + end + context 'php/reverse_php' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ diff --git a/spec/support/acceptance/session/php.rb b/spec/support/acceptance/session/php.rb index 3e77494f0de87..e3d88163c0230 100644 --- a/spec/support/acceptance/session/php.rb +++ b/spec/support/acceptance/session/php.rb @@ -210,9 +210,7 @@ module Acceptance::Session::Php known_failures: [] }, osx: { - known_failures: [ - "[-] FAILED: should return a list of processes" - ] + known_failures: [] }, windows: { known_failures: []