Skip to content
HackInvasionCybersecurity Knowledge Hub

Case File: Hunting Cobalt Strike Beacons — Finding the Metronome in Your Network Noise


The Incident Pattern

Cobalt Strike beacons don't scream — they tick. A compromised host phones home on a near-fixed interval, adds a little jitter to dodge naive thresholds, and exfiltrates in small, regular bursts. Default malleable profiles leave fingerprints: default sleep times, characteristic URI patterns, JA3 hashes, and DNS that resolves a little too predictably. The evidence shows up not in one connection, but in the rhythm of thousands.

This hunt assumes a beacon is already inside the perimeter and asks: which endpoints are talking to the outside world like clockwork?

The Hypothesis

Hypothesis: If a Cobalt Strike (or similar C2) beacon is active, we will find internal hosts making repeated outbound connections to the same external destination with unusually regular inter-arrival times (low variance around a base interval, e.g. 60s ± jitter), distinct from human-driven browsing — and some will match known beacon indicators such as default ports, default URI stems, or beaconing process anomalies.

Data You'll Need

SourceWhat we're after
Defender DeviceNetworkEventsOutbound connections: remote IP/port, bytes, process
Firewall / proxy logsURL stems, user agents, session timing
Sysmon Event ID 3 (NetworkConnect)Process-to-destination mapping
DNS logsQuery cadence for beaconing domains

Hunting with KQL

Microsoft Sentinel / Defender — find hosts with suspiciously regular outbound cadence to a single external IP:

DeviceNetworkEvents
| where Timestamp > ago(1d)
| where ActionType == "ConnectionSuccess"
| where RemoteIPType == "Public"
| where InitiatingProcessFileName !in ("chrome.exe", "msedge.exe", "firefox.exe", "outlook.exe", "teams.exe", "svchost.exe")
| summarize count(), Times = make_list(Timestamp) by DeviceName, RemoteIP, RemotePort, InitiatingProcessFileName
| where count_ >= 30
| extend Sorted = array_sort_asc(Times)
| extend Gaps = series_subtract(series_add(Sorted, 0), Sorted)
| extend AvgGap = todouble(series_stats_dynamic(Gaps).avg), StdDev = todouble(series_stats_dynamic(Gaps).stdev)
| extend Regularity = StdDev / AvgGap
| where Regularity < 0.35 and AvgGap between (20s .. 600s)
| project DeviceName, RemoteIP, RemotePort, InitiatingProcessFileName, count_, AvgGap, Regularity
| order by Regularity asc

What this does: aggregates each host's connections per destination, computes the gaps between consecutive connections, and scores regularity (coefficient of variation). A beacon with 60s sleep + 20% jitter produces gaps clustered tightly around 60s — a regularity score near 0.2 — while human browsing is wildly irregular. Excluding common browsers and mail clients cuts the noise floor.

True-positive example: WS-ACCT-031 | 185.220.x.x | 443 | rundll32.exe | 1,440 connections | AvgGap: 61.2s | Regularity: 0.18 — a thousand-plus connections, one per minute, from rundll32.exe to a foreign IP on 443. Browsers don't do that. Beacons do.

Companion — known Cobalt Strike network indicators (default profile artifacts):

DeviceNetworkEvents
| where Timestamp > ago(7d)
| where RemoteUrl has_any ("/submit.php", "/ca", "/dpixel", "/pixel", "/engage", "jquery-3.3.1.min.js", "jquery-3.3.2.min.js")
   or RemoteIP in ("185.220.101.4")  // replace with current TI
| project Timestamp, DeviceName, InitiatingProcessFileName, RemoteIP, RemoteUrl

Hunting with Splunk

Beaconing cadence analysis over firewall or Sysmon network data:

index=network sourcetype=firewall OR sourcetype=Sysmon EventCode=3
| where NOT process_name IN ("chrome.exe","msedge.exe","firefox.exe","outlook.exe","teams.exe")
| bin _time span=1m
| stats dc(_time) as active_minutes, count as conns by src_ip, dest_ip, dest_port, process_name
| where active_minutes > 120 AND conns > 200
| eval beacon_ratio = round(conns/active_minutes, 2)
| where beacon_ratio > 0.8 AND beacon_ratio < 1.5
| sort - conns

What this does: buckets connections per minute and looks for source–destination pairs active across many minutes with roughly one connection per minute — the classic beacon signature. A ratio near 1.0 over hours of the day is the metronome.

Example hit: src_ip=10.4.2.31 | dest_ip=45.155.x.x | dest_port=443 | process_name=powershell.exe | active_minutes=380 | conns=392 | beacon_ratio=1.03 — PowerShell phoning a foreign IP once a minute for over six hours. Case opened.

Analyst walkthrough (click to expand)
  1. Run the cadence query over 24h; sort by regularity (KQL) or beacon_ratio (SPL).
  2. For top hits, pull the full connection timeline — beacons often pause during "working hours" evasion or go quiet on weekends.
  3. Check the process: is it injected (rundll32, dllhost, powershell with no window) or a legit updater with a fixed schedule?
  4. Resolve the destination: ASN, first-seen, VirusTotal / TI reputation, and whether other hosts talk to it.

Validating the Hit

  1. Inspect the process. Beacon injection lives in odd hosts — verify parent chain, command line, and loaded modules of the beaconing process.
  2. Look at the bytes. Small, symmetric up/down payloads on a fixed schedule differ from browsing; check for base64-ish or encrypted blobs in proxy logs.
  3. Correlate the timeline. When did the cadence start? Align with phishing clicks, downloads, or lateral movement from other hunts.
  4. Check for siblings. One beacon rarely travels alone — search the fleet for the same destination IP, JA3, or URI stem.

Tuning Out False Positives

  • Legitimate updaters and sync agents (cloud storage, AV definition pulls, patch agents) beacon on schedules — baseline by destination and process signer.
  • Long-lived but bursty apps (Teams, Slack) hold connections open rather than reconnecting; they score poorly on the cadence test.
  • NTP and telemetry are regular by design — exclude by port/protocol (UDP/123) and known Microsoft/Google endpoints.
  • Tune the regularity threshold per environment: start at 0.35, tighten to 0.25 once benign schedulers are allowlisted, and always pair with process anomalies.

What to Do Next

  • Contain: isolate the host at the network layer first — killing the process without blocking C2 risks a re-beacon from a sibling implant.
  • Capture: take a memory image before remediation; beacon configs (sleep, jitter, C2, watermark) live in memory and identify the operator's profile.
  • Block and hunt wide: block the C2 at proxy/firewall, then hunt the destination across all telemetry for additional compromised hosts.
  • Eradicate: rebuild from known-good media — beacons persist via services, WMI, and scheduled tasks that manual cleaning misses.

Filed from the hunt floor: nobody browses the web once every 61 seconds for six hours. When the network keeps time, something is conducting it.

Latest


EmoticonEmoticon