Chapter 38

Cobalt Strike Traffic

Cobalt Strike is the most widely used commercial offensive framework and the C2 tool you will encounter most often in real-world intrusions. Understanding exactly what Cobalt Strike traffic looks like — in default configuration and with Malleable C2 profiles — is essential for detection engineering. Cobalt Strike's C2 traffic has multiple layers of identifiable characteristics even when an operator has configured a custom Malleable C2 profile.

Scenario

An intrusion analyst receives a PCAP from a compromised host. The traffic shows regular HTTPS connections to a VPS IP every 60 seconds. You need to confirm this is Cobalt Strike, not a legitimate application. You analyze: the JA3 hash (matches default CS profile), the beacon timing (CV = 0.008, essentially zero jitter), the HTTP response body size pattern (exactly 4 bytes for empty check-in responses), and the URI pattern (/submit.php with 150-byte fixed POST bodies). Each individually is ambiguous; all together confirm Cobalt Strike with high confidence.

Cobalt Strike Default Indicators

  Cobalt Strike Network Indicators — Default Profile
  ═══════════════════════════════════════════════════════════════════

  Transport: HTTP or HTTPS (most common), DNS, SMB pipe (lateral movement)

  HTTP/S Default Profile (no Malleable C2):
  ├── GET check-in: GET /[random 4-char].js or /updates
  │     No Cookie header (default)
  │     User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322)
  │     Note: IE 7.0 on Windows XP in 2024 = red flag
  ├── POST for task data: POST /[random]/submit.php
  │     Metadata encoded in URI or POST body
  │     Default body: ~150 bytes, very consistent size
  └── Server response: 4 bytes "    " (spaces) for empty task

  JA3 Hashes (Java-based TLS):
  ├── Default: 51c64c77e60f3980eea90869b68c58a8 (3.12-3.14)
  ├── Variant:  07d14d16d21d21d07c07d14d07d21d69e3ff64f35a08ef2a6d5bb20d6e2fc (JARM)
  └── Java TLS fingerprint is more unique than browser TLS

  SMB C2 (named pipe for lateral movement):
  ├── Named pipe: \\.\pipe\MSSE-[hex]-server (default)
  ├── Written to C:\Windows\Temp\ or %TEMP%
  └── Parent process: spawned via PsExec-style or WMI

  Timing: default sleep = 60 seconds, jitter = 0%
    → CV ≈ 0.001 (practically zero)

  Staged vs Stageless:
  ├── Staged: initial beacon downloads shellcode (~200-400KB GET response)
  └── Stageless: full shellcode in initial payload (no staging request)

Cobalt Strike Detection Script

bashcobalt-strike-detect.sh
#!/bin/bash
PCAP="$1"

echo "=== Known Cobalt Strike JA3 Hashes ==="
CS_JA3="51c64c77e60f3980eea90869b68c58a8"
tshark -r "$PCAP" -n -Y "tls.handshake.type==1" -T fields \
  -E separator="\t" \
  -e ip.src -e ip.dst -e tcp.dstport \
  | while read src dst port; do
    # Would need JA3 computation here — use Zeek ssl.log ja3 field instead
    echo "Connection: $src → $dst:$port"
done

echo ""
echo "=== Cobalt Strike default User-Agent (IE7/XP) ==="
tshark -r "$PCAP" -n \
  -Y "http.user_agent contains \"MSIE 7.0\"" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch -e ip.src -e ip.dst \
  -e http.user_agent -e http.request.uri

echo ""
echo "=== Stager download indicators (200-400KB HTTP GET response) ==="
tshark -r "$PCAP" -n \
  -Y "http.response.code==200 and http.content_length >= 200000 and http.content_length <= 500000" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch -e ip.dst -e ip.src \
  -e http.content_length -e http.request.uri

echo ""
echo "=== Cobalt Strike SMB pipe names ==="
tshark -r "$PCAP" -n \
  -Y "smb2.filename matches \"MSSE-[0-9a-f]+-server\"" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch -e ip.src -e ip.dst -e smb2.filename

echo ""
echo "=== Consistent small POST bodies (C2 metadata) ==="
tshark -r "$PCAP" -n \
  -Y "http.request.method==POST" \
  -T fields \
  -E separator="\t" \
  -e frame.time_epoch -e ip.src -e ip.dst \
  -e http.request.uri -e http.content_length \
  | awk -F'\t' 'NF>=5 && $5>100 && $5<300 {print}' | head -30

Malleable C2 Profiles — What Changes, What Stays

IndicatorMalleable C2 Can ChangeCannot Change / Residual
User-AgentYes — any stringConsistent across all sessions
URI pathsYes — any path patternPattern consistency across sessions
HTTP headersYes — add/remove headersJava TLS fingerprint more resistant to change
JA3 hashPartially — cipher order adjustableJava TLS base fingerprint differs from NSS/BoringSSL
Beacon timingYes — sleep + jitterTimer drift over days produces detectable pattern
Response sizeYes — padding configurableEmpty check-in response has minimum size
Pipe namesYes — custom pipe namesPipe over SMB IPC$ pattern remains
JARM hashNo (without redirector)Java TLS stack's JARM is distinctive
Staging URLYes — custom stagerLarge shellcode download (200-400KB) is visible
Mental model: Cobalt Strike detection is about corroboration

Any single Cobalt Strike indicator — a suspicious JA3, a regular beacon interval, a POST to /submit.php — could be a false positive from legitimate software. Detection confidence comes from corroboration: how many independent indicators point to the same conclusion? A connection with CV = 0.003 (extremely regular), a Java TLS JA3 hash, a User-Agent for an ancient browser version, and URIs matching the CS default stager pattern — four independent indicators pointing to the same source IP and destination IP — is a confirmed Cobalt Strike beacon. Build your detection logic to accumulate evidence: score each indicator separately and alert when the cumulative score exceeds a threshold, rather than alerting on any single indicator.

Q & A

Q: The operator is using a Malleable C2 profile that mimics Microsoft Windows Update traffic. How do I detect it?

Mimicking Windows Update is a sophisticated but detectable approach. Vectors: (1) Destination IP/domain: Windows Update uses a specific set of Microsoft IPs and domains (windowsupdate.com, update.microsoft.com). A Cobalt Strike implant can mimic the URI patterns and User-Agent of Windows Update but must use a different IP/domain (the attacker doesn't control Microsoft's servers). Comparing the destination to known Microsoft IP ranges will expose the fake. (2) Certificate CN: Windows Update servers present certificates with Microsoft-issued certificates. An attacker's server will present a different certificate — either self-signed, a different CA, or a lookalike domain cert. (3) Traffic pattern**: real Windows Update downloads happen after user-initiated checks or scheduled triggers and involve large downloads (hundreds of MB). A beacon checking in every 60 seconds with 150-byte POST bodies is not Windows Update, even if the URI looks right. (4) Frequency: Windows Update checks happen roughly daily, not every 60 seconds. The beacon interval is the most reliable discriminator once you have a clean baseline of real Windows Update timing on your network.