Hunting Cobalt Strike Beacons in Network Traffic

2 October 2023 | justruss.tech

Cobalt Strike is a commercial adversary simulation platform. A licensed copy costs around $3,500 per year per operator. Cracked versions have been in widespread circulation since approximately 2020 and appear in the majority of large-scale
ransomware and APT intrusions. Hunting for it effectively requires understanding both the default configuration and how operators modify it.

How a Cobalt Strike beacon works

A beacon is a shellcode payload that runs in memory. Once executed, it decodes its configuration from an embedded XOR-encoded block, connects to the team server (the C2 infrastructure), and enters a sleep-check-task loop. The default sleep is 60
seconds with 0% jitter. The C2 protocol is HTTP or HTTPS by default, though DNS and SMB named pipe transports are also supported.

The configuration block can be extracted from a beacon in memory or on disk. The structure is documented and tools like CobaltStrikeParser can decode it:

pip install cobaltstrikeparser
python3 -m cobaltstrikeparser beacon.bin

# Example output:
BeaconType        - HTTPS
Port              - 443
SleepTime         - 60000 (ms)
MaxGetSize        - 1048576
Jitter            - 0
C2Server          - updates.microsoft-cdn.com
HttpGet_Uri       - /updates/check
UserAgent         - Mozilla/5.0 (Windows NT 10.0; Win64; x64)...
Watermark         - 305419896

The Watermark field is a numeric identifier tied to a specific licensed copy (or cracked build). Known watermarks for common cracked versions are documented in threat intelligence feeds.

Network indicators — default profile

Default HTTP beacon traffic has recognisable characteristics visible in Zeek conn.log and http.log:

# Beaconing pattern: regular intervals, consistent byte counts
# From conn.log:
ts          id.orig_h      id.resp_h        duration  orig_bytes  resp_bytes
1696200000  192.168.1.101  203.0.113.50     0.341     285         2048
1696200060  192.168.1.101  203.0.113.50     0.287     285         2048
1696200120  192.168.1.101  203.0.113.50     0.301     285         2048
1696200180  192.168.1.101  203.0.113.50     0.298     285         2048

Exactly 60-second intervals, almost identical byte counts — this is what 0% jitter looks like. Real user-generated traffic to the same host would show irregular timing.

JA3 fingerprinting

The default Cobalt Strike HTTPS beacon uses a specific TLS configuration that produces a consistent JA3 hash. From ssl.log:

ts          id.orig_h      id.resp_h      ja3                               ja3s                              server_name
1696200000  192.168.1.101  203.0.113.50   a0e9f5d64349fb13191bc781f81f42e1  ec74a5c51106f0419184d0dd08fb05bc  updates.microsoft-cdn.com

The JA3 hash a0e9f5d64349fb13191bc781f81f42e1 is the most widely documented Cobalt Strike default. It is generated by the Java TLS implementation used in the beacon stager. The JA3S hash ec74a5c51106f0419184d0dd08fb05bc
identifies the server-side response fingerprint.

Malleable C2 profile evasion

Experienced operators deploy custom Malleable C2 profiles that change every aspect of beacon traffic. A profile impersonating Amazon S3:

http-get {
    set uri "/s3/bucket/update.bin";
    client {
        header "Host" "s3.amazonaws.com";
        header "User-Agent" "aws-sdk-go/1.44.0 (go1.18; linux; amd64)";
        metadata {
            base64url;
            prepend "AWSAccessKeyId=";
            parameter "X-Amz-Security-Token";
        }
    }
    server {
        header "Content-Type" "application/octet-stream";
        header "x-amz-id-2" "random";
        output {
            base64;
            print;
        }
    }
}

Against a profile like this, content-based detection fails. The traffic looks indistinguishable from legitimate AWS SDK requests at the application layer.

Behavioural detection that survives profile changes

Regardless of what the traffic looks like, beacon behaviour has consistent properties:

# Zeek: calculate coefficient of variation (CV) of connection intervals
# Low CV = highly regular timing = beacon candidate
# Run against conn.log with Python:

import json, statistics
from collections import defaultdict

connections = defaultdict(list)
with open("conn.log") as f:
    for line in f:
        rec = json.loads(line)
        key = (rec["id.orig_h"], rec["id.resp_h"], rec["id.resp_p"])
        connections[key].append(float(rec["ts"]))

for key, timestamps in connections.items():
    if len(timestamps) < 10:
        continue
    timestamps.sort()
    intervals = [timestamps[i+1]-timestamps[i] for i in range(len(timestamps)-1)]
    if len(intervals)  0 else 999
    if cv < 0.1 and 30 < mean < 300:  # very regular, 30s-5min intervals
        print(f"{key}: mean_interval={mean:.1f}s stdev={stdev:.1f}s cv={cv:.3f} samples={len(intervals)}")

A CV under 0.1 (10% standard deviation relative to mean) across 10+ samples in the 30-300 second interval range is a high-confidence beaconing indicator regardless of what the HTTP traffic looks like.

Hunting named pipe beacons

SMB beacon uses named pipes for C2 and leaves no network trace to external servers. Detection requires host-based telemetry. Sysmon Event ID 17/18 (PipeCreated/PipeConnected):

EventID: 17
PipeName: \MSSE-5d04b01a-0000-0000-0000-000000000000
Image:    C:\Windows\System32\svchost.exe

Default Cobalt Strike named pipe format: \MSSE-[guid]-0000-... or \postex_[hex]. These are documented in public threat intel and easy to Sigma-rule against.