🔬 Lab Difficulty: Beginner — Estimated Time: 60–90 minutes
🗂️ MITRE ATT&CK: T1071.001 — Application Layer Protocol: Web Protocols
Elastic SIEM Access request — https://hunt-forward.com
Press enter or click to view image in full size
How to use this lab: Read the story to understand the attack. Then follow the Hunt section to find it yourself in Elastic SIEM.
Document your findings in your Hunt Notebook as you go — you’ll use them to build your GitHub portfolio at the end.
📖 Part 1: The Scenario
Monday, 8:03 AM, San Francisco.
Alex Chen, junior SOC analyst, first week on the job. His team lead Dana drops a sticky note on his keyboard before vanishing into a meeting: “Routine log review. Network flows, last 24 hours. Probably nothing.”
Alex opens Kibana. Starts scrolling. AWS. Slack. Google. Normal, normal, normal — then one entry makes him pause.
192.168.1.100 → 203.0.113.42:8080 | TCP | 150 bytes out | 75 bytes in
192.168.1.100 → 203.0.113.42:8080 | TCP | 153 bytes out | 78 bytes in
192.168.1.100 → 203.0.113.42:8080 | TCP | 149 bytes out | 74 bytes inOne minute apart. Every time. He switches to histogram view. Six hours. Clockwork.
He looks up the source IP — a developer workstation belonging to Priya, who has access to the production credentials vault. He pastes 203.0.113.42 into VirusTotal.
12 vendors flag it. Category: C2 Server.
He calls Dana. “I think I found something.” / “How bad?” / “Bad enough.”
What happened behind the scenes: Three days earlier, Priya clicked a phishing link disguised as a Slack security notice. A Remote Access Trojan (RAT) installed silently. Its only job: check in with the attacker’s Command and Control (C2) server every 60 seconds — a tiny “I’m alive, any orders?” — and wait. The attacker was patient. They hadn’t moved yet. But that quiet, clockwork heartbeat is called a beacon, and it left tracks in the logs.
Now it’s your turn to find it.
The Three Signals That Give Beaconing Away
┌─────────────────────────────────────────────────────────────┐
│ 🚨 SIGNAL 1: Clockwork Timing │
│ Normal app: connects when user does something │
│ Malware: connects on a timer. Every. Single. Minute. │
├─────────────────────────────────────────────────────────────┤
│ 🚨 SIGNAL 2: Identical Byte Counts │
│ Normal app: transfers vary wildly (KB to MB) │
│ Malware: same tiny payload every time (~150 bytes) │
├─────────────────────────────────────────────────────────────┤
│ 🚨 SIGNAL 3: Unknown External Destination │
│ Normal app: talks to known CDNs, cloud providers │
│ Malware: a bare IP address, no legitimate business use │
└─────────────────────────────────────────────────────────────┘Now it’s your turn to find it.
🎯 Part 2: Your Mission
Alex found this manually by instinct and a sharp eye. You’re going to find it systematically — using KQL queries that will work against any environment, not just this one.
By the end of this lab you will have:
- ✅ Identified the beaconing host using connection frequency analysis
- ✅ Confirmed the beacon using byte consistency analysis
- ✅ Found a second infected host using DNS anomaly detection
- ✅ Traced lateral movement from the initial compromise
- ✅ Documented your full investigation in your Hunt Notebook
🔧 Part 3: Lab Setup
You’ll need a Hunt Forward account for this lab. Hunt Forward gives you a live Elastic SIEM environment with this exact dataset pre-loaded — no installation, no configuration.
👉 hunt-forward.com — 7-day free trial, then $5/month
Click : Discover Session
OR
You can follow the below steps:
- Open Kibana → click the hamburger menu (top left) → Discover
- Select the index
c2-lab-logs - Set the time range to April 24, 2024 (custom:
2024-04-24T00:00:00→2024-04-24T23:59:59)
You should see 739 log entries. That’s your crime scene. Let’s work it.
🔍 Part 4: The Hunt
Hunt 1 — Find the Machine Talking Too Much
Alex’s first instinct was right: something was making way too many connections to the same place. Let’s quantify that with a single ES|QL query.
Switch to ES|QL in Discover by clicking </>ES|QL on the top right corner of the discover.
Press enter or click to view image in full size
FROM c2-lab-logs
| WHERE event.dataset == "zeek.conn"
AND NOT destination.ip == "8.8.8.8"
| STATS connection_count = COUNT()
BY source.ip, destination.ip, destination.port
| SORT connection_count DESC
| LIMIT 20What each line does:
FROM c2-lab-logs— query this indexWHERE event.dataset == "zeek.conn"— only network connection logs, not DNSAND NOT destination.ip == "8.8.8.8"— drop Google DNS (background noise)STATS connection_count = COUNT() BY ...— count connections grouped by source IP, destination IP, and portSORT connection_count DESC— highest count firstLIMIT 20— show top 20 results
What you’re looking for: One row with a connection count dramatically higher than everything else — we’re talking 10x or more. Every legitimate application in this dataset connects to many different destinations. Malware talks to one destination, over and over.
📝 Hunt Notebook checkpoint: Write down the source IP, destination IP, destination port, and connection count you find. This is your first IOC (Indicator of Compromise). Notebook can be accessed using this icon on the blog within your dashboard.
Press enter or click to view image in full size
🏁 Milestone 1 of 4 — Suspicious Host Identified Open your Hunt Notebook in the Hunt Forward dashboard and paste this template. Fill in your findings from the query above.
## 🔍 Milestone 1: Suspicious Host Identified**Date of Hunt:** [today's date]
**Lab:** Hunt Forward #001 — C2 Beaconing Detection
**Analyst:** [your name]
### Finding
Identified a host making an abnormally high number of connections to a
single external destination.
| Field | Value |
|------------------|----------------|
| Source IP | [your finding] |
| Destination IP | [your finding] |
| Destination Port | [your finding] |
| Connection Count | [your finding] |
| Time Window | [your finding] |
### ES|QL Query Used
```esql
FROM c2-lab-logs
| WHERE event.dataset == "zeek.conn"
AND NOT destination.ip == "8.8.8.8"
| STATS connection_count = COUNT()
BY source.ip, destination.ip, destination.port
| SORT connection_count DESC
| LIMIT 20
Notes:
Connection count is significantly higher than any other source/destination pair in the dataset, indicating automated or scripted behavior.
Hunt 2 — Confirm It’s a Beacon (The Clockwork Test)
High connection count could be a chatty but legitimate app. The real test is regularity and consistency. Run this ES|QL query to calculate the byte statistics across every connection in the suspicious pair:
FROM c2-lab-logs
| WHERE event.dataset == "zeek.conn"
AND source.ip == "192.168.1.100"
AND destination.ip == "203.0.113.42"
| STATS
total_connections = COUNT(),
avg_bytes_out = AVG(network.bytes_out),
min_bytes_out = MIN(network.bytes_out),
max_bytes_out = MAX(network.bytes_out),
avg_bytes_in = AVG(network.bytes_in),
min_bytes_in = MIN(network.bytes_in),
max_bytes_in = MAX(network.bytes_in),
first_seen = MIN(@timestamp),
last_seen = MAX(@timestamp)What each line does:
WHERE source.ip == ... AND destination.ip == ...— scope to just the suspicious pairSTATS COUNT()— total number of connectionsAVG / MIN / MAXonnetwork.bytes_outandnetwork.bytes_in— calculate the spread of byte transfersMIN / MAX @timestamp— shows the full time range of the activity
What you’re looking for: Two things that confirm a beacon:
- Clockwork timing —
total_connectionsshould be ~360 over the 6-hour window betweenfirst_seenandlast_seen. Divide the duration by the count to get your approximate beacon interval. - Byte consistency — the gap between
min_bytes_outandmax_bytes_outshould be tiny relative to the average. Less than 10% variance is a strong beacon indicator. Legitimate app traffic varies wildly.
A browser session might transfer anywhere from 5KB to 5MB per connection. Malware sends the same 150-byte “I’m alive” message every single time.
📝 Hunt Notebook checkpoint: Record
total_connections, all six byte stats (avg/min/max for in and out),first_seen, andlast_seen. Calculate the approximate beacon interval and byte variance percentage.✅ Beacon confirmed.
192.168.1.100is infected and calling home every ~60 seconds.
🏁 Milestone 2 of 4 — Beacon Confirmed Add this block to your Hunt Notebook below Milestone 1.
## ✅ Milestone 2: Beacon Confirmed### Timing Analysis
| Field | Value |
|--------------------|----------------|
| Beacon interval | ~[X] seconds |
| First beacon seen | [timestamp] |
| Last beacon seen | [timestamp] |
| Total beacons | [count] |
| Duration | [X] hours |
### Byte Consistency Analysis
| Field | Value |
|--------------------|----------------|
| bytes_out min | [your finding] |
| bytes_out max | [your finding] |
| bytes_out avg | [your finding] |
| bytes_in min | [your finding] |
| bytes_in max | [your finding] |
### ES|QL Query Used
```esql
FROM c2-lab-logs
| WHERE event.dataset == "zeek.conn"
AND source.ip == "192.168.1.100"
AND destination.ip == "203.0.113.42"
| STATS
total_connections = COUNT(),
avg_bytes_out = AVG(network.bytes_out),
min_bytes_out = MIN(network.bytes_out),
max_bytes_out = MAX(network.bytes_out),
avg_bytes_in = AVG(network.bytes_in),
min_bytes_in = MIN(network.bytes_in),
max_bytes_in = MAX(network.bytes_in),
first_seen = MIN(@timestamp),
last_seen = MAX(@timestamp)
Conclusion
Byte variance is under [X]% across all connections. Combined with clockwork timing (~[X]s interval over [X] hours), this confirms automated beacon behavior — not legitimate application traffic.
Severity: High Confidence: High
Hunt 3 — Find the Second Infected Host (DGA Hunt)
Priya’s machine isn’t the only one. There’s a second infected host using a more sophisticated technique called Domain Generation Algorithms (DGA).
Get Hunt Forward’s stories in your inbox
Join Medium for free to get updates from this writer.
Instead of beaconing to a hardcoded IP, this malware generates hundreds of random-looking domain names and tries each one until it finds an active C2 server. The symptom this leaves in DNS logs is called an NXDOMAIN storm — a flood of “domain not found” failures from one host in a short time window.
Run this ES|QL query to rank every host by how many NXDOMAIN responses they generated:
FROM c2-lab-logs
| WHERE event.dataset == "zeek.dns"
AND dns.response_code == "NXDOMAIN"
| STATS nxdomain_count = COUNT()
BY source.ip
| SORT nxdomain_count DESCWhat each line does:
WHERE event.dataset == "zeek.dns"— DNS logs onlyAND dns.response_code == "NXDOMAIN"— only failed lookupsSTATS COUNT() BY source.ip— count failures per hostSORT nxdomain_count DESC— worst offender first
What you’re looking for: One IP generating dramatically more NXDOMAIN failures than anyone else. Normal hosts occasionally hit a bad domain. A DGA host hits dozens or hundreds in a short window.
Now follow the trail — find the domain the malware eventually resolved successfully:
FROM c2-lab-logs
| WHERE event.dataset == "zeek.dns"
AND source.ip == "192.168.1.105"
AND dns.response_code == "NOERROR"
| KEEP @timestamp, dns.question.name, dns.answers.data
| SORT @timestamp ASCWhat each line does:
AND source.ip == "192.168.1.105"— scope to the DGA host you just foundAND dns.response_code == "NOERROR"— only successful resolutionsKEEP— show only these three fields so results are readableSORT @timestamp ASC— earliest resolution first
You’ll see the domain the malware found — disguised to look like a software update service. That’s the active C2 domain.
📝 Hunt Notebook checkpoint: Record the second infected host IP, the number of NXDOMAIN responses, and the C2 domain it eventually resolved. Note the timestamp when the DGA storm started and when it resolved.
🏁 Milestone 3 of 4 — Second Host & DGA Identified Add this block to your Hunt Notebook below Milestone 2.
## 🌐 Milestone 3: Second Infected Host — DGA Behavior### DGA Storm
| Field | Value |
|------------------------|----------------|
| Source IP | [your finding] |
| NXDOMAIN count | [your finding] |
| Storm start time | [timestamp] |
| Storm end time | [timestamp] |
| Sample DGA domains | [domain 1] |
| | [domain 2] |
| | [domain 3] |
### C2 Domain Resolved
| Field | Value |
|------------------------|-------------------------|
| Active C2 domain | [your finding] |
| Resolved IP | [your finding] |
| First successful query | [timestamp] |
### ES|QL Queries Used
FROM c2-lab-logs
| WHERE event.dataset == "zeek.dns"
AND dns.response_code == "NXDOMAIN"
| STATS nxdomain_count = COUNT()
BY source.ip
| SORT nxdomain_count DESC
FROM c2-lab-logs
| WHERE event.dataset == "zeek.dns"
AND source.ip == "192.168.1.105"
AND dns.response_code == "NOERROR"
| KEEP @timestamp, dns.question.name, dns.answers.data
| SORT @timestamp ASC
Conclusion
Host [IP] exhibited DGA behavior — [N] NXDOMAIN failures before resolving [domain]. The use of a dynamic DNS provider and update-themed domain name are common evasion techniques.
Severity: High Confidence: High
Hunt 4 — Follow the Attacker (Lateral Movement)
Here’s where the story gets urgent.
Once the attacker had a foothold on Priya’s machine, they didn’t stop there. After four hours of maintaining their beacon, they started exploring — reaching out to other machines on the internal network, looking for servers with valuable data.
This is called lateral movement, and it’s a critical escalation in any incident.
Run this ES|QL query to find every internal host the compromised machine touched, and how much data moved in each connection:
esql
FROM c2-lab-logs
| WHERE network.direction == "internal"
AND destination.port == 445
| STATS
connection_count = COUNT(),
total_bytes_out = SUM(network.bytes_out),
total_bytes_in = SUM(network.bytes_in)
BY source.ip, destination.ip
| SORT total_bytes_out DESCWhat each line does:
WHERE network.direction == "internal"— only traffic between internal hostsAND destination.port == 445— SMB port, the most common lateral movement channelSTATS COUNT(), SUM(bytes_out), SUM(bytes_in) BY source/dest— count connections and total data per pairSORT total_bytes_out DESC— the pair with the most data transferred appears first
What you’re looking for: The compromised host (192.168.1.100) reaching multiple different internal machines. Most entries will show tiny byte counts — those are probes. One entry will show a dramatically larger transfer. That's not a probe — that's a successful connection where the attacker got in.
📝 Hunt Notebook checkpoint: Record the list of internal IPs that were probed, the timestamp range of the scanning activity, and which machine received the largest data transfer. This is your lateral movement evidence.
🏁 Milestone 4 of 4 — Lateral Movement Traced Add this final block to your Hunt Notebook below Milestone 3. This completes your investigation.
## 🔀 Milestone 4: Lateral Movement### SMB Scan Evidence
| Field | Value |
|------------------------|----------------|
| Source (attacker host) | [your finding] |
| Scan start time | [timestamp] |
| Scan end time | [timestamp] |
| Unique hosts probed | [count] |
| Port targeted | 445 (SMB) |
### Internal Hosts Probed
| Target IP | Bytes Out | Bytes In | Result |
|----------------|-----------|----------|---------------|
| [IP 1] | [bytes] | [bytes] | Probe only |
| [IP 2] | [bytes] | [bytes] | Probe only |
| [IP 3] | [bytes] | [bytes] | ⚠️ Large transfer — possible access |
### ES|QL Query Used
FROM c2-lab-logs
| WHERE network.direction == "internal"
AND destination.port == 445
| STATS
connection_count = COUNT(),
total_bytes_out = SUM(network.bytes_out),
total_bytes_in = SUM(network.bytes_in)
BY source.ip, destination.ip
| SORT total_bytes_out DESC
Conclusion
Compromised host [IP] scanned [N] internal targets over [duration]. One host ([IP]) received a significantly larger data transfer ([bytes]), indicating a successful SMB connection. That host may be compromised.
Severity: Critical — active lateral movement in progress Confidence: High
Recommended Immediate Actions
- Isolate 192.168.1.100 (primary beacon host)
2. Isolate 192.168.1.105 (DGA/secondary beacon host)
3. Isolate [IP of successful SMB target] pending investigation
4. Block 203.0.113.42 and [C2 domain] at perimeter firewall
5. Preserve memory image of 192.168.1.100 before shutdown
6. Escalate to IR team with this document as initial findings report
📋 Part 5: Building Your Timeline
You’ve found everything Alex found — and you did it systematically with repeatable queries. Now pull it together.
┌────────────────────────────────────────────────────────────────────────┐
│ INCIDENT TIMELINE — NovaPay Network Compromise │
├──────────┬─────────────────────────────────────────────────────────────┤
│ 08:00 │ 192.168.1.100 begins beaconing to 203.0.113.42:8080 │
│ │ → Every ~60 seconds, ~150 bytes out / ~75 bytes in │
├──────────┼─────────────────────────────────────────────────────────────┤
│ 08:00 │ 192.168.1.105 begins DGA domain polling │
│ │ → 100 NXDOMAIN responses to random-string domains │
├──────────┼─────────────────────────────────────────────────────────────┤
│ 10:00 │ 192.168.1.105 resolves update-svc.ddns.net │
│ │ → DGA succeeded; second host now under attacker control │
├──────────┼─────────────────────────────────────────────────────────────┤
│ 12:00 │ 192.168.1.100 begins internal SMB scanning │
│ │ → 8 internal hosts probed on port 445 in rapid succession │
├──────────┼─────────────────────────────────────────────────────────────┤
│ 12:06 │ Large data transfer to 192.168.1.20 via SMB │
│ │ → Possible successful lateral movement to file server │
└──────────┴─────────────────────────────────────────────────────────────┘📝 Part 6: Export Your Hunt Notebook → GitHub Portfolio
If you followed the milestone blocks throughout this lab, your Hunt Notebook already contains four completed sections. You have two options for turning them into a GitHub portfolio piece:
Option A — Merge your milestones. Combine all four milestone blocks into a single document, add a cover section with your name and an executive summary, and push it as hunt-001-c2-beaconing-detection.md. Everything you need is already written — just assemble it.
✅ Milestone 1 — Suspicious Host Identified
✅ Milestone 2 — Beacon Confirmed
✅ Milestone 3 — Second Host & DGA Identified
✅ Milestone 4 — Lateral Movement TracedOption B — Use the Hunt Forward pre-written report. A completed reference report for this lab is available in your Hunt Forward dashboard. Download it, push it to GitHub, and you’re done.
That said — here’s a recommendation worth taking seriously.
Write your own.
You don’t have to do it from scratch. Use AI to handle the repetitive parts — formatting tables, structuring sections, cleaning up your notes. But keep yourself in the driver’s seat: decide how to structure the narrative, choose which findings to lead with, write the executive summary in your own voice. The goal isn’t a perfect document — it’s the practice of thinking like someone who has to communicate a threat investigation clearly to another human.
That skill compounds. Every report you write makes the next one faster and sharper. And when you’re sitting in an interview and a hiring manager asks you to walk them through an investigation you’ve done, the one you wrote yourself is the one you can actually talk about with confidence.
Export from the Hunt Notebook and push to GitHub as:
hunt-001-c2-beaconing-detection.md
That’s your first portfolio piece. Four milestones of real findings, your KQL queries, a timeline you built from raw logs, and your name on it.
🛡️ Part 7: What Alex Did Next
Dana’s meeting ended sixty seconds after Alex’s call. Both hosts were isolated, the C2 IP and domain blocked at the perimeter, and Priya’s machine forensically imaged before anyone touched it. The attacker had been patient — 72 hours of silent beaconing — but Alex caught the lateral movement before a single byte of payment data left the network.
First week. Real incident. He opened his Hunt Notebook and started writing.
🎓 The Takeaway
You just practiced three core threat hunting techniques that real SOC analysts use every day:
None of these required knowing the attacker’s IP in advance. You found them purely by looking for behavior that doesn’t match normal network activity.
That’s threat hunting.
🚀 Ready for the Next Lab?
This is Hunt Forward Lab #001. New labs publish 2–3 times per week.
Coming up:
- Lab #002: LOLBAS — When the Attacker Uses Your Own Tools Against You
- Lab #003: DNS Tunneling — Finding Data Exfiltration Hidden in Plain Sight
- Lab #004: Credential Dumping — Hunting LSASS Access in Endpoint Logs
Each one has the same format: a story, a hunt, a Hunt Notebook template, and a portfolio piece waiting to be written.
👉 Access all labs at hunt-forward.com— 7-day free trial, then $5/month
And if you want to follow along as new labs drop — follow Hunt Forward on Medium. You’ll get notified every time a new scenario goes live.
Hunt Forward Lab #001 — C2 Beaconing Detection MITRE ATT&CK: T1071.001 | Dataset: c2-lab-logs | Difficulty: Beginner