{"id":188,"date":"2026-01-06T09:00:00","date_gmt":"2026-01-06T09:00:00","guid":{"rendered":"http:\/\/justruss.tech\/index.php\/2023\/10\/02\/hunting-cobalt-strike-beacons-in-network-traffic\/"},"modified":"2026-05-15T10:34:55","modified_gmt":"2026-05-15T10:34:55","slug":"hunting-cobalt-strike-beacons-in-network-traffic","status":"publish","type":"post","link":"https:\/\/justruss.tech\/index.php\/2026\/01\/06\/hunting-cobalt-strike-beacons-in-network-traffic\/","title":{"rendered":"Hunting Cobalt Strike Beacons in Network Traffic"},"content":{"rendered":"<p>Cobalt Strike appears in a significant proportion of major breach reports. The tool itself is legitimate commercial adversary simulation software, but cracked and leaked versions have been in widespread use since around 2020 and feature in the majority of large-scale ransomware intrusions and nation-state operations. Understanding how it works in detail makes hunting for it considerably more practical.<\/p>\n<h3>How a Cobalt Strike beacon works<\/h3>\n<p>A beacon is a shellcode payload that runs in memory. On execution it decodes its configuration from an embedded XOR-encoded block, resolves C2 server details from that config, 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 for different operational requirements.<\/p>\n<pre>\/\/ Extracting the beacon configuration from a file or memory dump\npip install cobaltstrikeparser\n\n\/\/ From a beacon binary\npython3 -m cobaltstrikeparser beacon.bin\n\n\/\/ Example output:\n\/\/ BeaconType:        HTTPS\n\/\/ Port:              443\n\/\/ SleepTime:         60000   (milliseconds)\n\/\/ MaxGetSize:        1048576\n\/\/ Jitter:            0       (0% jitter = perfectly regular intervals)\n\/\/ C2Server:          updates.microsoft-cdn.com,\/updates\/check\n\/\/ UserAgent:         Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36\n\/\/ HttpGetHeader:     Accept: *\/*\n\/\/ Watermark:         305419896\n\/\/ bProcInject:       1\n\/\/ INJECTION_PROCESS: explorer.exe\n\n\/\/ The Watermark is a numeric identifier tied to a specific license\n\/\/ Known watermarks for cracked\/leaked copies are documented in threat intel feeds<\/pre>\n<h3>Default profile network indicators in Zeek<\/h3>\n<pre>\/\/ Default HTTP beacon traffic - zero jitter makes it unmistakable\n\/\/ From conn.log:\n\n\/\/ ts          id.orig_h      id.resp_h        duration  orig_bytes  resp_bytes\n\/\/ 1696200000  192.168.1.101  203.0.113.50     0.341     285         2048\n\/\/ 1696200060  192.168.1.101  203.0.113.50     0.287     285         2048\n\/\/ 1696200120  192.168.1.101  203.0.113.50     0.301     285         2048\n\/\/ 1696200180  192.168.1.101  203.0.113.50     0.298     285         2048\n\n\/\/ Exactly 60 second intervals, near-identical byte counts\n\/\/ Real user traffic to the same server would show completely irregular timing\n\n\/\/ From http.log (default profile):\n\/\/ method=GET  uri=\/updates\/check  user_agent=\"Mozilla\/5.0...\"\n\/\/ method=POST uri=\/submit.php     user_agent=\"Mozilla\/5.0...\"\n\n\/\/ The GET retrieves tasks, the POST submits results\n\/\/ Default URI patterns are well-documented and easy to Sigma rule against<\/pre>\n<h3>JA3 fingerprinting<\/h3>\n<pre>\/\/ Default Cobalt Strike HTTPS beacon JA3 hash\n\/\/ From ssl.log:\n\/\/ ja3: a0e9f5d64349fb13191bc781f81f42e1\n\/\/ This is produced by the Java TLS implementation used in the stager\n\n\/\/ Check all TLS connections for this hash\ncat \/opt\/zeek\/logs\/current\/ssl.log | \\\n    python3 -c \"\nimport sys, json\nfor line in sys.stdin:\n    try:\n        rec = json.loads(line)\n        if rec.get('ja3') == 'a0e9f5d64349fb13191bc781f81f42e1':\n            print('COBALT STRIKE DEFAULT JA3 DETECTED')\n            print(f'  src={rec[\\\"id.orig_h\\\"]} dst={rec[\\\"id.resp_h\\\"]}')\n            print(f'  sni={rec.get(\\\"server_name\\\",\\\"none\\\")}')\n    except:\n        pass\n\"\n\n\/\/ Sigma rule for Elastic\/Sysmon correlation\ntitle: Cobalt Strike Default JA3 Hash\nlogsource:\n    category: network\n    product: zeek\ndetection:\n    selection:\n        event.dataset: zeek.ssl\n        ssl.ja3: 'a0e9f5d64349fb13191bc781f81f42e1'\n    condition: selection\nlevel: high<\/pre>\n<h3>Malleable C2 profile evasion<\/h3>\n<p>Experienced operators use custom Malleable C2 profiles that change every aspect of beacon traffic. A profile impersonating Amazon S3 requests:<\/p>\n<pre>\/\/ Example Malleable C2 profile (publicly available, used for understanding)\nhttp-get {\n    set uri \"\/s3\/bucket\/update.bin\";\n    client {\n        header \"Host\" \"s3.amazonaws.com\";\n        header \"User-Agent\" \"aws-sdk-go\/1.44.0 (go1.18; linux; amd64)\";\n        metadata {\n            base64url;\n            prepend \"AWSAccessKeyId=\";\n            parameter \"X-Amz-Security-Token\";\n        }\n    }\n}\n\n\/\/ Against this profile, traffic looks like legitimate AWS SDK requests\n\/\/ Content-based detection fails entirely\n\/\/ You need behavioural detection instead<\/pre>\n<h3>Behavioural detection that survives any profile<\/h3>\n<pre>\/\/ Calculate connection timing regularity - works regardless of profile\npython3 &lt;&lt; EOF\nimport json, statistics\nfrom collections import defaultdict\nimport os\n\nlog_path = &quot;\/opt\/zeek\/logs\/current\/conn.log&quot;\nif not os.path.exists(log_path):\n    print(&quot;conn.log not found - adjust path&quot;)\n    exit()\n\nconnections = defaultdict(list)\nwith open(log_path) as f:\n    for line in f:\n        try:\n            rec = json.loads(line)\n            if rec.get(&quot;proto&quot;) == &quot;tcp&quot; and str(rec.get(&quot;id.resp_p&quot;)) == &quot;443&quot;:\n                key = (rec[&quot;id.orig_h&quot;], rec[&quot;id.resp_h&quot;])\n                connections[key].append(float(rec[&quot;ts&quot;]))\n        except:\n            pass\n\nprint(&quot;Checking for beaconing to port 443...&quot;)\nfor (src, dst), timestamps in sorted(connections.items()):\n    if len(timestamps) &lt; 8:\n        continue\n    timestamps.sort()\n    intervals = [timestamps[i+1]-timestamps[i] for i in range(len(timestamps)-1)]\n    if len(intervals) &lt; 5:\n        continue\n    mean_i = statistics.mean(intervals)\n    stdev_i = statistics.stdev(intervals)\n    if mean_i == 0:\n        continue\n    cv = stdev_i \/ mean_i\n    # Very regular intervals, beacon-range timing\n    if cv &lt; 0.15 and 20 &lt; mean_i  {dst}:443\")\n        print(f\"  mean_interval={mean_i:.1f}s  stdev={stdev_i:.1f}s  cv={cv:.3f}  samples={len(intervals)}\")\nEOF<\/pre>\n<h3>Named pipe beacon detection (SMB C2)<\/h3>\n<pre>\/\/ SMB beacons use named pipes for C2, leaving no external network trace\n\/\/ Detection requires host-based telemetry\n\n\/\/ Sysmon Events 17 and 18 cover pipe creation and connection\n\/\/ Default Cobalt Strike named pipe patterns:\n\/\/ \\pipe\\MSSE-[guid]-0000-0000-0000-000000000000\n\/\/ \\pipe\\postex_[hex]\n\/\/ \\pipe\\msagent_[hex]\n\n\/\/ Sigma rule for default named pipe patterns\ntitle: Cobalt Strike Named Pipe Default Pattern\nlogsource:\n    product: windows\n    category: pipe_created\ndetection:\n    selection:\n        PipeName|startswith:\n            - '\\MSSE-'\n            - '\\postex_'\n            - '\\msagent_'\n            - '\\status_'\n    condition: selection\nlevel: high\n\n\/\/ Also look for unusual processes connecting to named pipes\n\/\/ svchost.exe connecting to a pipe in \\Device\\NamedPipe\\postex_abc123\n\/\/ is not legitimate behaviour<\/pre>\n<h3>Hunting process injection from beacons<\/h3>\n<p>Cobalt Strike beacons commonly inject into other processes for post-exploitation. The spawn_to process (default: rundll32.exe) is spawned and injected into for most post-exploitation operations.<\/p>\n<pre>\/\/ Sysmon Event ID 8: CreateRemoteThread from beacon into spawn_to process\n\/\/ Default spawn_to: C:\\Windows\\SysWOW64\\rundll32.exe (no arguments)\n\n\/\/ Sigma rule for Cobalt Strike spawn_to pattern\ntitle: Cobalt Strike Spawn-To Process Default\nlogsource:\n    product: windows\n    category: create_remote_thread\ndetection:\n    selection:\n        TargetImage|endswith: '\\rundll32.exe'\n    filter_legit:\n        SourceImage|startswith:\n            - 'C:\\Windows\\System32\\'\n            - 'C:\\Windows\\SysWOW64\\'\n    condition: selection and not filter_legit\nfalsepositives:\n    - Legitimate software using rundll32 for DLL loading\nlevel: medium<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Cobalt Strike is everywhere in incident reports. Understanding<br \/>\nthe default beacon traffic patterns makes hunting it more approachable than most people expect.<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[13],"tags":[],"class_list":["post-188","post","type-post","status-publish","format-standard","hentry","category-threat-hunting"],"_links":{"self":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/188","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/comments?post=188"}],"version-history":[{"count":4,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/188\/revisions"}],"predecessor-version":[{"id":281,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/188\/revisions\/281"}],"wp:attachment":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/media?parent=188"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/categories?post=188"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/tags?post=188"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}