Detect Encrypted Channel C2 Beaconing at Fixed Interval with Low Jitter in IBM QRadar
An implant that maintains command-and-control over an encrypted channel must periodically re-contact its controller to ask for tasking. Because TLS hides the payload from content inspection, the traffic itself cannot be signatured — but the *timing* of the check-in survives encryption and is a property of the implant's configuration, not of its payload. Implant frameworks expose a 'sleep' value (the base interval between check-ins) and a 'jitter' percentage (a random offset applied to each sleep to break up perfect periodicity). Operators routinely leave jitter low or at zero, because raising it makes interactive tasking sluggish. The observable consequence is a long-lived series of outbound TLS sessions from one process on one host to one remote endpoint, where the inter-arrival intervals cluster tightly around a mean — a coefficient of variation (standard deviation divided by mean interval) far below what human-driven or event-driven application traffic produces. This detection scores that timing signature directly rather than attempting to inspect encrypted content. It is deliberately built on two independent evidence classes so it does not depend on any single sensor: (1) endpoint-sourced connection telemetry (Microsoft Defender for Endpoint DeviceNetworkEvents, Sysmon Event ID 3, CrowdStrike NetworkConnectIP4), which records TCP socket creation and — critically — attributes it to an initiating process, letting the rule exclude browsers and surface the far more suspicious case of a non-browser binary holding a periodic TLS conversation with an external host; and (2) network-sensor TLS metadata (Zeek ssl.log, NGFW session logs), which cannot attribute a process but can observe certificate validation status and SNI, so a beacon negotiating with a self-signed or untrusted certificate, or presenting no SNI at all, is scored higher. Sensor limits are respected explicitly. Endpoint TCP-connection telemetry (Sysmon Event ID 3, MDE DeviceNetworkEvents, Falcon NetworkConnectIP4) records TCP/UDP socket activity only, so it can time the beacon but can never reveal the certificate or the TLS handshake contents; Sysmon Event ID 3 in particular carries no certificate, JA3, or SNI field. Conversely Zeek ssl.log and firewall session logs see the handshake but carry no process attribution. Neither source alone is sufficient, which is why the KQL arm is process-centric and the SPL arm is certificate-centric, and why the triage steps below pivot between them. This detection is scoped to the C2 tactic (TA0011) and to periodicity of the channel itself; a detection built on the volume of data moved would instead belong under Exfiltration.
MITRE ATT&CK
- Tactic
- Command and Control
QRadar Detection Query
SELECT
sourceip AS SourceIP,
destinationip AS DestinationIP,
destinationport AS DestinationPort,
COUNT(*) AS ConnectionCount,
SUM(sourcebytes + destinationbytes) AS TotalBytes,
(SUM(sourcebytes + destinationbytes) / COUNT(*)) AS AvgBytesPerFlow,
MIN(firstpackettime) AS FirstSeen,
MAX(lastpackettime) AS LastSeen,
((MAX(lastpackettime) - MIN(firstpackettime)) / COUNT(*)) AS MeanIntervalMillis
FROM flows
WHERE
protocolid = 6 -- TCP
AND destinationport IN (443, 8443, 9443)
AND firstpackettime > (NOW() - 86400000)
GROUP BY sourceip, destinationip, destinationport
HAVING
COUNT(*) >= 30
AND (MAX(lastpackettime) - MIN(firstpackettime)) >= 7200000
AND ((MAX(lastpackettime) - MIN(firstpackettime)) / COUNT(*)) >= 20000
AND (SUM(sourcebytes + destinationbytes) / COUNT(*)) <= 8192
ORDER BY ConnectionCount DESC QRadar AQL against the flows table (QFlow/NetFlow-derived), which records TCP session records with byte counters and precise first/last packet timestamps. Groups TCP flows to external TLS ports by source/destination pair and requires four conditions: at least 30 flows, a series persisting at least two hours, a mean inter-flow interval of at least 20 seconds, and a small average payload per flow. That last condition is the substitute for the jitter test AQL cannot express — a C2 check-in that receives no tasking transfers only a few kilobytes, so a long series of uniformly small TLS flows to one destination is the flow-level shape of a beacon, whereas genuine browsing or file transfer produces flows orders of magnitude larger. Treat the output as a candidate list and confirm interval regularity by exporting the per-flow firstpackettime values for the top pairs.
Data Sources
Required Tables
False Positives & Tuning
- Software update and telemetry agents polling vendor endpoints on a fixed schedule with small payloads, which match the small-flow condition exactly
- Monitoring and uptime probes issuing periodic lightweight HTTPS health checks to external endpoints
- IoT, OT and appliance firmware polling a vendor cloud endpoint at a hard-coded cadence
- Certificate status checking (OCSP/CRL) and time-synchronisation-over-HTTPS traffic that recurs on a fixed schedule
Other platforms for THREAT-C2-EncryptedChannelFixedIntervalBeacon
Testing Methodology
Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.
- Test 1Fixed-Interval Low-Jitter HTTPS Beacon Simulation (PowerShell)
Expected signal: DeviceNetworkEvents (or Sysmon Event ID 3): approximately 35 ConnectionSuccess records with InitiatingProcessFileName powershell.exe to the resolved public IP of the target on RemotePort 443, spaced at roughly 20-second intervals over about 12 minutes. Zeek ssl.log on a tapped segment shows a matching series of handshakes with the target SNI. Note that the endpoint records will carry no certificate or SNI field — that is expected and is the sensor boundary this detection is built around.
- Test 2Fixed-Interval Low-Jitter HTTPS Beacon Simulation (Linux curl)
Expected signal: Zeek ssl.log: about 35 handshake records from the test host to the target's public IP on port 443 at roughly 20-second spacing, each with a populated server_name and a validation_status of ok. EDR network telemetry on the host attributes the connections to the curl process.
- Test 3Fixed-Interval Beacon to Non-Standard TLS Port (macOS curl)
Expected signal: Zeek ssl.log: about 35 handshakes to 198.51.100.25 on port 8443 at roughly 20-second spacing. If the lab listener uses a self-signed certificate, validation_status reports a self-signed or unverifiable chain and server_name is absent because curl was given a bare IP rather than a hostname. Endpoint EDR telemetry attributes the TCP connections to the curl process on port 8443.
References (11)
- https://attack.mitre.org/techniques/T1573/
- https://attack.mitre.org/techniques/T1573/002/
- https://attack.mitre.org/techniques/T1071/001/
- https://attack.mitre.org/techniques/T1008/
- https://attack.mitre.org/tactics/TA0011/
- https://docs.zeek.org/en/master/logs/ssl.html
- https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-devicenetworkevents-table
- https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon
- https://learn.microsoft.com/en-us/kusto/query/stdev-aggregation-function
- https://www.elastic.co/guide/en/elasticsearch/reference/current/eql-syntax.html
- https://library.humio.com/data-analysis/functions-groupby.html
Response Playbook
Triage
- Establish which arm fired. The KQL arm names a process; the SPL arm names a certificate and SNI but not a process. If only one fired, immediately run the other over the same client IP and time window — corroboration across an endpoint sensor and a network sensor is the fastest way to separate a real implant from a scheduled polling agent.
- Identify and validate the initiating binary from the KQL result: check InitiatingProcessFolderPath against expected install locations, and pull DeviceFileEvents or DeviceProcessEvents for the same DeviceId to establish how and when the binary arrived and whether it is signed by a publisher already present in the environment. A periodic beacon from a signed vendor updater in Program Files is a very different finding from one out of %TEMP%, %APPDATA% or a user profile directory.
- Read the mean interval as a configuration value, not just a statistic. Round numbers with tight jitter (exactly 30s, 60s, 300s) point at a configured sleep timer. Vendor polling agents also use round intervals, so this alone is not decisive, but an interval that matches no documented product default in your fleet is a strong pivot.
- Inspect the TLS handshake properties from the SPL arm. A certificate whose chain does not validate, an absent SNI, or a subject/issuer with default or placeholder values on an internet-facing destination are all inconsistent with a legitimate SaaS endpoint, which will present a publicly trusted certificate matching its SNI.
- Determine whether the destination is used by any other host in the environment. A single client talking to a destination no one else contacts, with no corresponding entry in the organisation's SaaS or vendor inventory, is materially more suspicious than a destination hundreds of managed endpoints poll.
- Check whether the beacon interval changed during the observation window. Implants are frequently re-tasked to a shorter sleep for interactive work and then returned to a long sleep, producing a step change in mean interval that a fixed-schedule vendor agent will not exhibit.
- Review what else the identified host and account did during the series — process creation, credential access, or internal reconnaissance concurrent with the beaconing raises this from an anomaly to an active intrusion.
Containment
- Block the destination IP and, where SNI or RemoteUrl is known, the destination hostname at the perimeter — a beacon that cannot reach its controller receives no tasking, which contains the immediate risk while investigation continues.
- Isolate the endpoint via EDR network isolation once the initiating process is confirmed non-legitimate. Do this before killing the process: terminating the implant first may destroy in-memory configuration (controller address, sleep/jitter values, encryption keys) that is the most valuable evidence available.
- Suspend or rotate credentials for the account under which the beaconing process ran, and for any account that authenticated on the host during the beaconing window, since an encrypted C2 channel that has been open for hours should be assumed to have carried credential material.
- Where the destination proves malicious, sweep the environment for other hosts contacting the same IP, hostname or JA3 fingerprint and contain those hosts too before the operator notices the first takedown.
- Preserve, then remove, the persistence mechanism. Do not remediate the binary alone until the mechanism that restarts it (scheduled task, service, run key, cron entry, launch agent) has been identified, or the beacon will simply return.
Evidence Collection
- Full DeviceNetworkEvents and Zeek ssl.log records for the client/destination pair across the entire series, not merely the alerting window — the earliest connection dates the compromise, which the alerting window will understate.
- Memory image of the beaconing process while it is still running. The controller address, sleep and jitter configuration, and session keys typically live only in memory and are the highest-value artefacts for attribution and for scoping other affected hosts.
- The beaconing binary or script itself, together with its full path, hash, digital signature status and timestamps, plus the parent process that launched it.
- The persistence artefact: scheduled task XML, service registry key, Run/RunOnce value, cron entry, systemd unit, or macOS launch agent plist, exported intact.
- Zeek ssl.log and x509.log entries for the destination covering certificate subject, issuer, validity dates and JA3/JA3S fingerprints, which support pivoting to other infrastructure using the same TLS configuration.
- Full packet capture of the encrypted sessions where available — the contents are not readable, but handshake metadata, packet sizing and precise timing support later analysis and any TLS-inspection replay.
Escalation Criteria
- !The initiating process is unsigned, runs from a user-writable directory (%TEMP%, %APPDATA%, /tmp, a user profile path), or is a scripting or living-off-the-land binary with no business justification for periodic external TLS traffic.
- !The same destination IP, hostname or JA3 fingerprint appears across multiple hosts, indicating either lateral spread or a shared implant deployment rather than an isolated anomaly.
- !The beaconing originates from a server, domain controller, jump host, management network or other privileged asset rather than a standard user endpoint.
- !The mean interval changed during the window in a way consistent with interactive re-tasking, or the beaconing host also shows credential access, discovery or lateral movement activity in the same period.
- !The series has run for more than 24 hours before detection, which implies the channel has been available for tasking long enough for meaningful post-exploitation activity to have already occurred.
- !A persistence mechanism is confirmed alongside the beaconing process — that combination establishes an intrusion rather than an unexplained network anomaly, and should be escalated to full incident response.
Investigation Guide
Related Techniques
Forensic Artifacts
- >
MDE DeviceNetworkEvents / Sysmon Event ID 3 records — connection timing with process attribution; note these carry no TLS handshake detail whatsoever, so they establish who and when but never what certificate was presented. - >
Zeek ssl.log and x509.log — SNI, negotiated TLS version, certificate subject/issuer/validity and chain validation status, plus JA3/JA3S fingerprints for pivoting to related infrastructure. - >
NGFW/proxy session logs — independent corroboration of session start times and byte counts when neither an endpoint agent nor a Zeek sensor covers the segment. - >
Process memory of the beaconing process — controller address, sleep and jitter configuration, and session keys, none of which are recoverable once the process exits. - >
The persistence artefact that restarts the beacon: scheduled task, service, Run key, cron entry, systemd unit or launch agent. - >
The binary or script on disk with hash, signature status, and filesystem timestamps, plus prefetch or shimcache evidence establishing first execution.
Tuning Guidance
Baseline before you alert. Run the threshold-free ranking hunt over 7–14 days first and allowlist what it surfaces at the top: software update services, EDR/AV sensor heartbeats, licensing check-ins, monitoring and log-shipping agents, and MDM/RMM clients are all real low-jitter beacons and will otherwise generate the overwhelming majority of alert volume. Allowlist on the pair (signed publisher or full binary path, destination) rather than on process name alone — process name alone is trivially spoofable, and a masquerading binary named after a legitimate updater is exactly the case this detection should still catch. The thresholds are three independent dials, and each trades a different way. MaxJitterRatio at 0.25 catches implants configured with roughly 0–25% jitter, which is the common operator setting; raising it toward 0.5 will catch more heavily jittered beacons at a steep cost in false positives, so raise it only for high-value asset populations where the volume is tolerable. MinConnections at 30 combined with MinDurationHours at 2 is what makes the standard deviation statistically meaningful — lowering the connection count much below 20 makes the jitter ratio unstable and will produce noise regardless of how the other dials are set. MinMeanIntervalSeconds at 20 excludes chatty application traffic; lower it only if you are specifically hunting short-sleep interactive sessions, and expect substantially more noise if you do. Aggregating on destination IP is the right default but is defeated by two configurations. Fallback channels rotate the destination on a schedule, breaking one long series into several short ones — lower MinDurationHours and MinConnections to surface those. Domain-generation or fast-flux infrastructure changes the destination continuously — re-aggregate on (DeviceId, InitiatingProcessFileName) alone, dropping RemoteIP from the summarize key, so the process's overall connection cadence is scored rather than its cadence to any one address. Respect the sensor boundaries when tuning. Do not attempt to add certificate, SNI or JA3 conditions to the KQL arm: DeviceNetworkEvents and Sysmon Event ID 3 record TCP/UDP socket activity only and expose no TLS handshake fields at all, so such a condition matches nothing and silently disables the rule. Certificate-based scoring belongs in the Zeek/network arm. Equally, do not expect the Zeek arm to name a process — pivot to the endpoint arm on the client IP for that. Where a TLS-inspecting proxy is deployed, its logs are a strong third source and can carry both the certificate detail and a user identity, though not a process; where TLS inspection is absent, JA3 fingerprint stability across the series is the most useful available proxy for a single unchanging client implementation. Finally, treat any post-alert change in mean interval as investigative signal rather than noise: a fixed-schedule vendor agent holds one cadence indefinitely, while an operator re-tasking an implant produces a visible step change.
Hunting Queries
Threshold-free ranking hunt: computes the jitter ratio for every non-browser process/destination pair with at least ten TLS connections and sorts the most regular to the top. Run this to baseline the environment before enabling the detection in alerting mode — the recurring vendor updaters and monitoring agents that dominate the top of this list are exactly what belongs on the allowlist. The SPL variant uses Sysmon Event ID 3 (Network connection), which records the initiating Image and so provides process attribution the same way MDE does; it likewise carries no certificate or SNI data.
// Hunt: rank every non-browser process/destination pair by how regular its outbound TLS timing is, with no threshold applied.
// Sort ascending by JitterRatio and read from the top — the most metronomic pairs in the environment surface first.
let BrowserProcesses = dynamic(["chrome.exe", "msedge.exe", "firefox.exe", "iexplore.exe", "opera.exe", "brave.exe"]);
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "ConnectionSuccess"
| where RemotePort in (443, 8443, 9443)
| where RemoteIPType == "Public"
| where InitiatingProcessFileName !in~ (BrowserProcesses)
| sort by DeviceId asc, InitiatingProcessFileName asc, RemoteIP asc, Timestamp asc
| serialize
| extend PrevTimestamp = prev(Timestamp), PrevDeviceId = prev(DeviceId), PrevRemoteIP = prev(RemoteIP), PrevProcess = prev(InitiatingProcessFileName)
| where DeviceId == PrevDeviceId and RemoteIP == PrevRemoteIP and InitiatingProcessFileName == PrevProcess
| extend IntervalSeconds = todouble(datetime_diff('second', Timestamp, PrevTimestamp))
| where IntervalSeconds >= 1.0
| summarize ConnectionCount = count(), MeanIntervalSeconds = round(avg(IntervalSeconds), 2), JitterStdDev = round(stdev(IntervalSeconds), 2)
by DeviceName, InitiatingProcessFileName, RemoteIP
| where ConnectionCount >= 10
| extend JitterRatio = round(JitterStdDev / MeanIntervalSeconds, 3)
| sort by JitterRatio asc, ConnectionCount desc
| take 200 index=sysmon sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=3 Initiated=true DestinationPort IN (443, 8443, 9443)
| where NOT (cidrmatch("10.0.0.0/8", DestinationIp) OR cidrmatch("172.16.0.0/12", DestinationIp) OR cidrmatch("192.168.0.0/16", DestinationIp) OR cidrmatch("127.0.0.0/8", DestinationIp))
| sort 0 Computer, Image, DestinationIp, _time
| streamstats current=f last(_time) as PrevTime by Computer, Image, DestinationIp
| eval IntervalSeconds=_time-PrevTime
| where IntervalSeconds>=1
| stats count as ConnectionCount, avg(IntervalSeconds) as MeanIntervalSeconds, stdev(IntervalSeconds) as JitterStdDev by Computer, Image, DestinationIp
| where ConnectionCount>=10
| eval JitterRatio=round(JitterStdDev/MeanIntervalSeconds, 3)
| table Computer, Image, DestinationIp, ConnectionCount, MeanIntervalSeconds, JitterStdDev, JitterRatio
| sort JitterRatio, - ConnectionCount
| head 200 Rarity hunt that intentionally drops the timing test and asks a different question: which external TLS destinations are contacted persistently but by almost no one? This catches beacons whose operator configured high jitter specifically to defeat periodicity scoring, since the rarity of the destination is a property of the infrastructure rather than of the sleep configuration. Cross-reference results against the SaaS and vendor inventory before investigating.
// Hunt: non-browser processes making outbound TLS connections to destinations that are rare across the whole fleet.
// Beacon infrastructure is typically contacted by one or two hosts; legitimate SaaS is contacted by many.
let BrowserProcesses = dynamic(["chrome.exe", "msedge.exe", "firefox.exe", "iexplore.exe", "opera.exe", "brave.exe"]);
DeviceNetworkEvents
| where Timestamp > ago(14d)
| where ActionType == "ConnectionSuccess"
| where RemotePort in (443, 8443, 9443)
| where RemoteIPType == "Public"
| where InitiatingProcessFileName !in~ (BrowserProcesses)
| summarize ConnectionCount = count(), DevicesContacting = dcount(DeviceId), FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Processes = make_set(InitiatingProcessFileName, 10), SampleDevice = take_any(DeviceName)
by RemoteIP, RemotePort
| where DevicesContacting <= 2
| where ConnectionCount >= 30
| extend DurationHours = round(todouble(datetime_diff('second', LastSeen, FirstSeen)) / 3600.0, 2)
| where DurationHours >= 2.0
| sort by ConnectionCount desc index=zeek sourcetype="bro:ssl:json"
| where NOT (cidrmatch("10.0.0.0/8", 'id.resp_h') OR cidrmatch("172.16.0.0/12", 'id.resp_h') OR cidrmatch("192.168.0.0/16", 'id.resp_h') OR cidrmatch("127.0.0.0/8", 'id.resp_h'))
| stats count as ConnectionCount, dc('id.orig_h') as ClientsContacting, earliest(ts) as FirstSeen, latest(ts) as LastSeen, values(server_name) as ServerNames, values(validation_status) as ValidationStatuses by 'id.resp_h', 'id.resp_p'
| where ClientsContacting<=2 AND ConnectionCount>=30
| eval DurationHours=round((LastSeen-FirstSeen)/3600, 2)
| where DurationHours>=2
| eval FirstSeen=strftime(FirstSeen, "%Y-%m-%d %H:%M:%S"), LastSeen=strftime(LastSeen, "%Y-%m-%d %H:%M:%S")
| table FirstSeen, LastSeen, 'id.resp_h', 'id.resp_p', ClientsContacting, ConnectionCount, DurationHours, ServerNames, ValidationStatuses
| sort - ConnectionCount Targets the intersection of encrypted C2 and living-off-the-land execution: repeated outbound TLS from a scripting host or dual-use system binary. Because sustained periodic external TLS from powershell.exe, mshta.exe, rundll32.exe or certutil.exe is rarely a legitimate pattern on a user endpoint, the connection threshold is lowered to ten and no jitter test is applied — the identity of the process is itself most of the signal.
// Hunt: TLS beaconing where the initiating process is a scripting host or living-off-the-land binary.
// Thresholds are deliberately low here because periodic external TLS from these binaries is rarely legitimate.
let LolBins = dynamic(["powershell.exe", "pwsh.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe", "certutil.exe", "bitsadmin.exe", "curl.exe", "python.exe", "node.exe"]);
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "ConnectionSuccess"
| where RemotePort in (443, 8443, 9443)
| where RemoteIPType == "Public"
| where InitiatingProcessFileName in~ (LolBins)
| summarize ConnectionCount = count(), DistinctDestinations = dcount(RemoteIP), FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
SampleCommandLine = take_any(InitiatingProcessCommandLine), FolderPath = take_any(InitiatingProcessFolderPath)
by DeviceName, DeviceId, InitiatingProcessFileName, RemoteIP, RemotePort
| where ConnectionCount >= 10
| extend DurationHours = round(todouble(datetime_diff('second', LastSeen, FirstSeen)) / 3600.0, 2)
| sort by ConnectionCount desc index=sysmon sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=3 Initiated=true DestinationPort IN (443, 8443, 9443)
| where NOT (cidrmatch("10.0.0.0/8", DestinationIp) OR cidrmatch("172.16.0.0/12", DestinationIp) OR cidrmatch("192.168.0.0/16", DestinationIp))
| rex field=Image "(?<ProcessName>[^\\\\]+)$"
| search ProcessName IN ("powershell.exe", "pwsh.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe", "certutil.exe", "bitsadmin.exe", "curl.exe", "python.exe", "node.exe")
| stats count as ConnectionCount, dc(DestinationIp) as DistinctDestinations, earliest(_time) as FirstSeen, latest(_time) as LastSeen, values(User) as Users by Computer, ProcessName, DestinationIp, DestinationPort
| where ConnectionCount>=10
| eval DurationHours=round((LastSeen-FirstSeen)/3600, 2)
| eval FirstSeen=strftime(FirstSeen, "%Y-%m-%d %H:%M:%S"), LastSeen=strftime(LastSeen, "%Y-%m-%d %H:%M:%S")
| table FirstSeen, LastSeen, Computer, ProcessName, Users, DestinationIp, DestinationPort, ConnectionCount, DistinctDestinations, DurationHours
| sort - ConnectionCount Atomic Red Team Tests
Simulates the timing signature this detection targets by issuing a series of outbound HTTPS requests from powershell.exe to a benign external endpoint at a fixed 20-second interval with no jitter. This reproduces the low coefficient-of-variation pattern of an implant sleep timer using only a standard scripting host and normal web requests — no implant, framework or C2 tooling is installed or used. Run from a host covered by MDE or Sysmon so that connection telemetry with process attribution is produced.
Command
$Target = 'https://example.com'; $Iterations = 35; $SleepSeconds = 20; for ($i = 1; $i -le $Iterations; $i++) { try { Invoke-WebRequest -Uri $Target -UseBasicParsing -TimeoutSec 10 | Out-Null } catch { } ; Start-Sleep -Seconds $SleepSeconds }; Write-Output "Beacon simulation complete: $Iterations HTTPS connections at ${SleepSeconds}s fixed interval" Expected Telemetry
DeviceNetworkEvents (or Sysmon Event ID 3): approximately 35 ConnectionSuccess records with InitiatingProcessFileName powershell.exe to the resolved public IP of the target on RemotePort 443, spaced at roughly 20-second intervals over about 12 minutes. Zeek ssl.log on a tapped segment shows a matching series of handshakes with the target SNI. Note that the endpoint records will carry no certificate or SNI field — that is expected and is the sensor boundary this detection is built around.
Expected Detection
KQL arm: ConnectionCount of about 34 derived intervals meets MinConnections; MeanIntervalSeconds is close to 20 and meets MinMeanIntervalSeconds; JitterStdDev is small so JitterRatio falls well under 0.25. DurationHours is only about 0.2, so for lab validation lower MinDurationHours to 0.1 or extend the iteration count to roughly 400 to satisfy the two-hour production threshold. The threshold-free ranking hunt will surface this pair at or near the top with no tuning at all.
Linux equivalent of the beaconing timing simulation, issuing HTTPS requests from curl at a fixed interval with no jitter to a benign external endpoint. Exercises both the endpoint arm (via an EDR agent recording TCP connections with process attribution) and the network arm (via Zeek ssl.log on a tapped egress segment). Uses only curl and sleep; nothing is installed and no C2 software is involved.
Command
for i in $(seq 1 35); do curl -s -o /dev/null --max-time 10 https://example.com; sleep 20; done; echo 'Beacon simulation complete: 35 HTTPS connections at 20s fixed interval' Expected Telemetry
Zeek ssl.log: about 35 handshake records from the test host to the target's public IP on port 443 at roughly 20-second spacing, each with a populated server_name and a validation_status of ok. EDR network telemetry on the host attributes the connections to the curl process.
Expected Detection
SPL arm: ConnectionCount and MeanIntervalSeconds thresholds are met and JitterRatio falls well below 0.25. BeaconScore will reach only 1 in this test because the target presents a valid publicly trusted certificate with SNI and therefore scores no points for UntrustedCert or MissingSNI, though SingleJA3 will add 1 since curl's fingerprint is constant — confirming the timing arm fires while the certificate arm correctly withholds points from benign TLS. To exercise the full score, repeat against a lab HTTPS listener using a self-signed certificate.
Varies the destination port to 8443 to confirm the detection's port coverage extends beyond 443, and runs from macOS to validate cross-platform endpoint telemetry coverage. Targets a lab-controlled HTTPS listener on an external test address; substitute an address you control and are authorised to test against. No C2 tooling is used.
Command
TARGET="https://198.51.100.25:8443/"; for i in $(seq 1 35); do curl -s -k -o /dev/null --max-time 10 "$TARGET"; sleep 20; done; echo 'Beacon simulation complete: 35 HTTPS connections to port 8443 at 20s fixed interval' Expected Telemetry
Zeek ssl.log: about 35 handshakes to 198.51.100.25 on port 8443 at roughly 20-second spacing. If the lab listener uses a self-signed certificate, validation_status reports a self-signed or unverifiable chain and server_name is absent because curl was given a bare IP rather than a hostname. Endpoint EDR telemetry attributes the TCP connections to the curl process on port 8443.
Expected Detection
SPL arm: the timing thresholds are met, and because the listener presents a self-signed certificate and no SNI is sent, UntrustedCert and MissingSNI each add a point and BeaconScore reaches 4 — the strongest case this detection reports. KQL arm: RemotePort 8443 is within the monitored port set, so the same series is detected on the endpoint side, though with no certificate detail available there.