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
| Source | What we're after |
|---|---|
Defender DeviceNetworkEvents | Outbound connections: remote IP/port, bytes, process |
| Firewall / proxy logs | URL stems, user agents, session timing |
| Sysmon Event ID 3 (NetworkConnect) | Process-to-destination mapping |
| DNS logs | Query 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)
- Run the cadence query over 24h; sort by regularity (KQL) or beacon_ratio (SPL).
- For top hits, pull the full connection timeline — beacons often pause during "working hours" evasion or go quiet on weekends.
- Check the process: is it injected (rundll32, dllhost, powershell with no window) or a legit updater with a fixed schedule?
- Resolve the destination: ASN, first-seen, VirusTotal / TI reputation, and whether other hosts talk to it.
Validating the Hit
- Inspect the process. Beacon injection lives in odd hosts — verify parent chain, command line, and loaded modules of the beaconing process.
- 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.
- Correlate the timeline. When did the cadence start? Align with phishing clicks, downloads, or lateral movement from other hunts.
- 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.





