Appearance
Hunt 003 — Lateral Movement
Objective: Identify attacker movement between internal systems via authentication chains, admin share access, pass-the-hash/pass-the-ticket indicators, and Twingate resource hopping that exceeds normal operational patterns.
Frequency: Weekly
Prerequisites:
- SecurityEvent table receiving EventID 4624, 4648, 4768, 4769, 5140 from all Windows hosts
- EventID 4688 enabled via GPO/Intune (Kill Chain Phase 1 — required for full coverage)
- Twingate_CL forwarding to cirius-logging-law-central
- CommonSecurityLog receiving Palo Alto CEF (for east-west traffic confirmation)
Environment baseline: Cirius is a small environment. On any given business day, a normal user authenticates to 1–2 systems. Rory authenticates to more given his admin role, but even he rarely exceeds 5 distinct hosts in a 30-minute window. Service accounts (svc-*) are expected to authenticate to their target resources repeatedly. Thresholds below are tuned for this environment — a large enterprise would use different numbers.
Queries
Q1 — Same Account Authenticating to 3+ Distinct Hosts in 30 Minutes (Interactive)
EventID 4624 (successful logon) summarized to find accounts hitting multiple hosts in a short window. Excludes known service account prefixes.
kql
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType in (2, 3, 10) // interactive, network, remote interactive (RDP)
| where AccountType == "User"
| where Account !startswith "svc-"
and Account !startswith "kobe-"
and Account !startswith "arctic-"
and Account !startswith "SYSTEM"
and Account !startswith "ANONYMOUS"
| extend HostShort = toupper(tostring(split(Computer, ".")[0]))
| summarize
HostCount = dcount(HostShort),
Hosts = make_set(HostShort, 20),
LogonTypes = make_set(LogonType, 5),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Account, bin(TimeGenerated, 30m)
| where HostCount >= 3
| project TimeWindow = TimeGenerated, Account, HostCount, Hosts, LogonTypes, FirstSeen, LastSeen
| order by HostCount desc, FirstSeen descLook for: Any non-service account hitting 3+ distinct hosts in a 30-minute window. Rory doing planned maintenance hitting 5+ hosts with a known CM ticket is benign. An account that normally touches 1 host suddenly appearing on 4 is a hit. LogonType 3 (network) across multiple hosts is the classic pass-the-hash pattern.
Q2 — Explicit Credential Logons (4648) — Workstation-to-Server
EventID 4648 fires when a process explicitly provides credentials (RunAs, WMI with creds, PsExec). Lateral movement tools frequently generate this. Workstation-to-server 4648 is the core signal.
kql
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4648
| where SubjectAccount !startswith "svc-"
and SubjectAccount !startswith "SYSTEM"
and SubjectAccount !startswith "ANONYMOUS"
| extend SourceHost = toupper(tostring(split(Computer, ".")[0]))
| extend TargetHost = toupper(tostring(split(TargetServerName, ".")[0]))
| where SourceHost != TargetHost // cross-host only
| summarize
TargetCount = dcount(TargetHost),
Targets = make_set(TargetHost, 20),
Accounts = make_set(SubjectAccount, 10),
Count = count()
by SourceHost, bin(TimeGenerated, 1h)
| where TargetCount >= 2 or Count >= 5
| project TimeWindow = TimeGenerated, SourceHost, TargetCount, Targets, Accounts, Count
| order by TargetCount descLook for: A workstation generating multiple 4648 events to different servers. Single 4648 from a workstation to one server can be legitimate (mapped drive, print server). 4648 from one workstation to 3+ servers in an hour is not. Pay attention to the Accounts field — multiple distinct accounts from one source host suggests credential dumping followed by use.
Q3 — Admin Share Access (5140 — C$/ADMIN$)
EventID 5140 is generated when a network share is accessed. C$ and ADMIN$ access from non-server sources is a primary lateral movement indicator for tools like PsExec and Cobalt Strike.
kql
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 5140
| where ShareName in ("\\\\*\\C$", "\\\\*\\ADMIN$", "\\\\*\\IPC$")
or ShareName endswith "\\C$"
or ShareName endswith "\\ADMIN$"
| extend SourceHost = toupper(tostring(split(IpAddress, ".")[0]))
| extend DestHost = toupper(tostring(split(Computer, ".")[0]))
| extend AccessAccount = SubjectAccount
| where AccessAccount !startswith "svc-"
and AccessAccount !startswith "SYSTEM"
| summarize
Count = count(),
Shares = make_set(ShareName, 10),
DestHosts = make_set(DestHost, 20),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by AccessAccount, IpAddress, bin(TimeGenerated, 1h)
| where Count >= 2 or array_length(DestHosts) >= 2
| project TimeWindow = TimeGenerated, AccessAccount, IpAddress, Count,
Shares, DestHosts, FirstSeen, LastSeen
| order by Count descLook for: Admin share access from workstation IPs to server hosts. A single IPC$ connection can be a Windows background operation. Any C$ or ADMIN$ access from a workstation to a server is high-priority — legitimate admins should be using Twingate + RDP, not admin shares directly. Multiple destinations from one source = likely automated tool.
Q4 — Pass-the-Hash Indicators (LogonType 3, NTLM, No Kerberos)
Pass-the-hash logons are network logons (type 3) using NTLM where you'd expect Kerberos. This query looks for NTLM network logons to sensitive systems.
kql
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType == 3
| where AuthenticationPackageName == "NTLM"
and LmPackageName != "-" // actual NTLM use, not negotiation fallback
| where Account !startswith "ANONYMOUS"
and Account !startswith "svc-"
| extend SourceHost = toupper(tostring(split(WorkstationName, ".")[0]))
| extend DestHost = toupper(tostring(split(Computer, ".")[0]))
| where SourceHost != DestHost
| summarize
DestCount = dcount(DestHost),
Destinations = make_set(DestHost, 20),
Count = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Account, SourceHost, bin(TimeGenerated, 1h)
| where DestCount >= 2 or Count >= 3
| project TimeWindow = TimeGenerated, Account, SourceHost, DestCount,
Destinations, Count, FirstSeen, LastSeen
| order by DestCount descLook for: NTLM network logons to multiple destinations in a short window. In a domain environment with Kerberos available, NTLM network logons to domain servers should be rare. Multiple NTLM logons from one source in an hour to different hosts is a pass-the-hash indicator. Single NTLM to a file server can be a legacy app — two or more different servers is suspicious.
Q5 — Pass-the-Ticket — Kerberos Service Ticket Requests for Unusual Services
Pass-the-ticket involves injecting a stolen Kerberos ticket. This query finds service ticket requests (4769) for services that the requesting account doesn't normally access, specifically looking at unusual service class combinations.
kql
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4769
| where Status == "0x0" // success
| where TicketEncryptionType == "0x17" // RC4 — weak, preferred by attackers; 0x12 = AES
| where ServiceName !endswith "$" // exclude computer accounts
and ServiceName != "krbtgt"
| where Account !startswith "svc-"
| summarize
ServiceCount = dcount(ServiceName),
Services = make_set(ServiceName, 20),
Count = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Account, IpAddress, bin(TimeGenerated, 1h)
| where ServiceCount >= 3 or Count >= 10
| project TimeWindow = TimeGenerated, Account, IpAddress, ServiceCount,
Services, Count, FirstSeen, LastSeen
| order by ServiceCount descLook for: RC4-encrypted service tickets for 3+ distinct services in an hour. RC4 (0x17) is the encryption type Mimikatz and Rubeus default to when requesting tickets. Modern environments should be using AES (0x12). Any RC4 ticket requests beyond a small baseline merit investigation. High volume RC4 requests from one IP is Kerberoasting (also covered in hunt-004).
Q6 — RDP Logons from Internal Sources Not Seen in Prior 14 Days
RDP lateral movement shows as type 10 logons (RemoteInteractive) from internal IPs that haven't RDP'd to that destination before. This is particularly relevant for detecting RDP hop chains.
kql
let RecentRDP = SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType == 10 // RemoteInteractive (RDP)
| where Account !startswith "svc-"
and Account !startswith "SYSTEM"
| extend SourceIP = IpAddress
| extend DestHost = toupper(tostring(split(Computer, ".")[0]))
| project TimeGenerated, Account, SourceIP, DestHost;
let BaselineRDP = SecurityEvent
| where TimeGenerated between (ago(21d) .. ago(7d))
| where EventID == 4624
| where LogonType == 10
| extend SourceIP = IpAddress
| extend DestHost = toupper(tostring(split(Computer, ".")[0]))
| summarize BaselineIPs = make_set(SourceIP) by Account, DestHost;
RecentRDP
| join kind=leftanti (BaselineRDP) on Account, DestHost
| summarize
Count = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Account, SourceIP, DestHost
| order by FirstSeen descLook for: RDP from a source IP not in the 14-day baseline for that account/destination pair. Rory using a new machine to RDP somewhere is benign — verify against his known IPs. A workstation RDP'ing to a server, then that server RDP'ing to another server (hop chain) in the same window is a confirmed hit.
Q7 — Twingate Resource Hopping (3+ Distinct Resources in 30 Minutes)
Twingate access to 3+ distinct resources in a short window from the same user — attacker behavior after gaining a session is to probe everything accessible.
kql
Twingate_CL
| where TimeGenerated > ago(7d)
| where status_s == "Allowed" or status_s == "allowed"
| where user_email_s != ""
| extend ResourceName = resource_name_s
| summarize
ResourceCount = dcount(ResourceName),
Resources = make_set(ResourceName, 30),
SourceIPs = make_set(client_ip_s, 5),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by user_email_s, bin(TimeGenerated, 30m)
| where ResourceCount >= 4 // 3 is borderline in this small env; 4+ is the threshold
| project TimeWindow = TimeGenerated, user_email_s, ResourceCount,
Resources, SourceIPs, FirstSeen, LastSeen
| order by ResourceCount desc, FirstSeen descLook for: A user hitting 4+ distinct Twingate resources in a 30-minute window, especially if the resources span different categories (e.g., hitting both a database resource and a server admin resource in the same window). Rory during an incident response or deployment is a valid explanation — check against CM tickets. Multiple source IPs for the same user in the same window could indicate session sharing.
Q8 — East-West Traffic Spike — Palo Alto Internal Zone Flows
Lateral movement generates internal traffic that the Palo Alto can see as east-west flows. This query looks for internal sources generating connections to many distinct internal destinations.
kql
CommonSecurityLog
| where TimeGenerated > ago(7d)
| where DeviceVendor == "Palo Alto Networks"
| where DeviceInboundInterface != DeviceOutboundInterface // cross-zone or cross-interface
| where DestinationIP startswith "10." or DestinationIP startswith "172."
or DestinationIP startswith "192.168."
| where SourceIP startswith "10." or SourceIP startswith "172."
or SourceIP startswith "192.168."
| where DestinationPort in (445, 135, 139, 3389, 5985, 5986, 22, 3306, 1433)
// SMB, RPC, NetBIOS, RDP, WinRM, SSH, MySQL, MSSQL
| summarize
DestCount = dcount(DestinationIP),
Destinations = make_set(DestinationIP, 30),
Ports = make_set(DestinationPort, 10),
BytesOut = sum(SentBytes),
Count = count()
by SourceIP, bin(TimeGenerated, 30m)
| where DestCount >= 5 // one source scanning 5+ internal hosts
| project TimeWindow = TimeGenerated, SourceIP, DestCount, Destinations,
Ports, BytesOut, Count
| order by DestCount descLook for: A single internal IP connecting to 5+ other internal IPs on lateral movement ports (SMB 445, RDP 3389, WinRM 5985/5986) in a 30-minute window. Scanner behavior is wide Dest IPs on a single port. Lateral movement is often targeted — fewer destinations but multiple ports. Anything reaching 10+ destinations in 30 minutes is likely automated scanning.
Triage Guidance
Expected / Benign:
- Rory RDP'ing to servers during maintenance windows listed in open CM tickets
- Service accounts (
svc-*) authenticating repeatedly to their designated target hosts - Arctic Wolf agent processes authenticating to multiple hosts during their scan windows
- Twingate access to 2–3 resources in a session for a normal user doing their job
- NTLM logons from legacy printers, NAS devices, or old applications that don't support Kerberos
Confirmed Hits (escalate immediately):
- Any account with 5+ distinct host authentications in 30 minutes that isn't Rory with a CM ticket
- C$ or ADMIN$ share access from a workstation IP to any server
- RDP hop chain (server A → server B → server C in same window, same account)
- Multiple distinct accounts from the same source host (credential use after dump)
- Internal IP connecting to 10+ hosts on port 445 in under 30 minutes
Borderline (investigate, don't escalate yet):
- 3-host authentication in 30 minutes — check if the account owner was doing something multi-system (helpdesk tasks, deployments)
- NTLM logon to a single server — identify the source application first
- Twingate hitting 4 resources — ask the user what they were doing before escalating
- New RDP source IP for Rory — contact him directly before raising an incident
Response
If you find a confirmed hit:
- Identify the pivot point — determine the first host the attacker controlled, then map the hop chain forward. Every host they touched is a potential secondary incident.
- Isolate affected hosts — in Cortex XDR, quarantine the suspected pivot host immediately. Do not wait for full investigation to isolate.
- Revoke sessions — if an Entra account was used, revoke all refresh tokens. If domain account, disable in AD and reset the password (do not reuse).
- Collect forensic artifacts — before isolation, if possible, capture memory (Cortex XDR memory acquisition or Arctic Wolf). After isolation, pull event logs from each hop host.
- Open a CRITICAL incident in SecOps with source_system = PROD (or DDE/AWS as applicable).
- Follow kill-chain-incident-response.md from the Lateral Movement containment phase.
- Notify Arctic Wolf — they have act-first authority. Brief them on the hop chain you've identified so they can extend the investigation to hosts outside your log coverage.