← Blog · · df00tech

Detecting Lateral Movement in Microsoft Sentinel: KQL Queries for T1021

KQL lateral movement T1021 Microsoft Sentinel Defender for Endpoint detection engineering

Lateral movement is where a breach becomes a crisis — one compromised laptop becomes thirty servers in a weekend. By the time an attacker is pivoting between hosts with stolen credentials, your initial-access detection has already failed. Everything downstream depends on catching them here.

This is also where most SOCs have the weakest coverage. Lateral movement uses legitimate Windows protocols — RDP, SMB, WinRM — that IT teams rely on daily. The detection problem is separating admin activity from adversary activity when they look nearly identical on the wire.

Below are five production-tested lateral movement detection KQL queries for Microsoft Sentinel and Defender for Endpoint, pulled verbatim from the df00tech detection library. Each targets a specific T1021 sub-technique. Every one is free to copy and deploy.

1. RDP Lateral Movement and Brute Force (T1021.001)

RDP is the single most common lateral movement vector in ransomware intrusions. Wizard Spider, BlackByte, Akira, FIN7, Kimsuky, and Volt Typhoon have all used it. Attackers steal credentials via phishing or credential dumping, then use RDP to expand access and deploy ransomware interactively.

The challenge: your IT team also uses RDP constantly. The detection needs to flag the patterns that distinguish lateral movement from normal admin work — unexpected source IPs, access to domain controllers, brute force leading to success.

// Detect suspicious RDP lateral movement patterns
let SensitiveHosts = dynamic(["dc", "domain-controller", "dc01", "pdc"]);
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4624
| where LogonType == 10  // RemoteInteractive = RDP
| extend TargetHost = tostring(Computer)
| extend SourceIP = tostring(IpAddress)
| where SourceIP !startswith "127." and SourceIP != "-" and SourceIP != ""
// Flag logons to sensitive hosts or from unexpected sources
| extend ToSensitiveHost = TargetHost has_any (SensitiveHosts)
| extend IsPrivilegedAccount = TargetUserName has_any ("admin", "administrator", "svc", "service")
// Join with logon failures to identify brute-force followed by success
| project TimeGenerated, Computer, TargetUserName, TargetDomainName, SourceIP, LogonType, SubjectUserName, ToSensitiveHost, IsPrivilegedAccount
| sort by TimeGenerated desc
| union (
    SecurityEvent
    | where TimeGenerated > ago(24h)
    | where EventID == 4625
    | where LogonType == 10
    | where IpAddress !startswith "127." and IpAddress != "-"
    | summarize FailureCount=count(), Accounts=make_set(TargetUserName) by IpAddress, bin(TimeGenerated, 5m)
    | where FailureCount >= 5
    | extend AlertType = "RDP BruteForce", Computer = "", TargetUserName = tostring(Accounts), SourceIP = IpAddress
    | project TimeGenerated, Computer, TargetUserName, SourceIP, FailureCount, AlertType
)

What this catches: The key filter is LogonType == 10 — RemoteInteractive, which only fires for RDP sessions. The query flags successful RDP logons to sensitive hosts (domain controllers) or using privileged account names, and separately catches brute force patterns (5+ failed RDP attempts from one source IP in 5 minutes).

How to use it: Deploy as a scheduled analytic rule. Build a watchlist of authorized RDP source IPs (jump servers, bastion hosts, admin workstations) and join against it to suppress legitimate admin activity. Any RDP to a domain controller from outside that list should page on-call immediately.

The full T1021.001 detection includes hunting queries for cross-host RDP sweeps, first-seen IP/host combinations, and off-hours activity.

2. Hunting Cross-Host RDP Sweeps

One workstation RDPing into one server is normal. One source IP RDPing into five servers in an hour is automated lateral movement — either an attacker with a credential sweeping the network or an early-stage ransomware deployment.

// Hunt for RDP connections to multiple hosts from a single source — lateral movement sweep
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624 and LogonType == 10
| where IpAddress !startswith "127." and IpAddress != "-"
| summarize TargetHosts=make_set(Computer), ConnectionCount=count(), UniqueHosts=dcount(Computer)
  by IpAddress, TargetUserName, bin(TimeGenerated, 1h)
| where UniqueHosts >= 3
| sort by UniqueHosts desc

What this catches: A single source IP and account combination establishing RDP sessions to 3+ unique destination hosts within one hour. Legitimate admins rarely RDP to many hosts in a short window — they use centralized management tools instead. Conti, LockBit, and BlackCat affiliates all produce this pattern during deployment.

How to use it: Run this daily. Compare hits against your list of bastion hosts and PAWs. Anything outside that list is worth a same-day investigation. If the source is a user workstation rather than a jump server, treat it as a confirmed incident and isolate the source host.

3. SMB Admin Share Abuse (T1021.002)

SMB admin shares (C$, ADMIN$, IPC$) are the backbone of Windows lateral movement. Conti, Ryuk, NotPetya, Emotet, Royal, and RansomHub all use them. An attacker with a valid admin credential can copy payloads to ADMIN$, create a remote service, and execute code — the classic PsExec pattern.

The challenge: SCCM, backup agents, and legitimate sysadmins all hit admin shares daily. Detection has to focus on the execution patterns that follow.

// Detect suspicious SMB admin share access and lateral tool transfer
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemotePort == 445
| where ActionType in ("ConnectionSuccess", "ConnectionFound")
// Exclude known legitimate management traffic
| where not (InitiatingProcessFileName in~ ("svchost.exe", "System") and RemotePort == 445)
| extend IsAdminShare = InitiatingProcessCommandLine has_any ("ADMIN$", "C$", "IPC$", "\\\\")
| project Timestamp, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine,
         RemoteIP, RemotePort, RemoteUrl, ActionType
| union (
    // Detect PsExec-style service creation over SMB
    DeviceProcessEvents
    | where Timestamp > ago(24h)
    | where FileName in~ ("psexec.exe", "psexec64.exe", "paexec.exe", "remcom.exe")
    | extend Source = "PsExec"
    | project Timestamp, DeviceName, InitiatingProcessFileName, ProcessCommandLine,
             AccountName, Source
)
| union (
    // Detect net use commands establishing share connections
    DeviceProcessEvents
    | where Timestamp > ago(24h)
    | where FileName =~ "net.exe" or FileName =~ "net1.exe"
    | where ProcessCommandLine has "use" and ProcessCommandLine has_any ("ADMIN$", "C$", "IPC$", "\\\\")
    | project Timestamp, DeviceName, InitiatingProcessFileName, ProcessCommandLine, AccountName
)
| sort by Timestamp desc

What this catches: Three complementary signals. First, outbound SMB (port 445) connections from unusual processes — this catches the network primitive regardless of tool. Second, execution of PsExec, PaExec, or remcom — the canonical SMB lateral movement tools. Third, net use commands explicitly mapping admin shares.

How to use it: The union design matters. Attackers sometimes use custom tools that don’t match the PsExec filename list but still produce the port 445 connection pattern. If you’re only watching for PsExec.exe by name, you’ll miss every in-memory Impacket SMBExec or PAExec variant. Tune by allowlisting your SCCM distribution points, backup servers, and management agent source IPs — everything else is worth investigating.

Pair this with detection for credential dumping (T1003.001) — most SMB admin share abuse depends on credentials harvested via LSASS dumping or Kerberos ticket theft.

4. Hunting NTLM Relay and Pass-the-Hash

SMB lateral movement often runs on stolen hashes rather than plaintext passwords. NTLM relay attacks (Responder, ntlmrelayx) and Pass-the-Hash (Mimikatz sekurlsa::pth) both produce a distinctive pattern: one source authenticating via NTLM to multiple destination hosts.

// Hunt for NTLM relay indicators — SMB connections with mismatched authentication
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where LogonType == 3  // Network logon
| where AuthenticationPackageName == "NTLM"
| where TargetUserName !endswith "$"  // Exclude machine accounts
| summarize AuthCount=count(), UniqueHosts=dcount(Computer), Hosts=make_set(Computer)
  by IpAddress, TargetUserName, bin(TimeGenerated, 30m)
| where UniqueHosts >= 3
| sort by UniqueHosts desc

What this catches: One source IP authenticating with NTLM (not Kerberos) to 3+ unique hosts within 30 minutes. Normal domain authentication should be Kerberos. Heavy NTLM usage to multiple destinations is suspicious — either tooling is forcing NTLM downgrade, or an attacker is relaying/replaying captured hashes. Scattered Spider and APT29 both rely on credential abuse patterns like this.

How to use it: The machine-account filter (!endswith "$") is critical — computer objects authenticate via NTLM constantly and would drown the results. Focus on user accounts. Any hit with a privileged account targeting servers warrants immediate response: reset the account, invalidate cached credentials, and start an investigation into the source host.

5. WinRM and PowerShell Remoting (T1021.006)

WinRM is the quieter lateral movement channel. Evil-WinRM (used by Storm-0501), Cobalt Strike beacons, and Brute Ratel C4 all ride on it. Because PowerShell remoting is a legitimate admin tool and defaults to encrypted transport, it blends into enterprise traffic better than RDP or SMB.

// Detect WinRM lateral movement — remote command execution and suspicious WinRM usage
DeviceProcessEvents
| where Timestamp > ago(24h)
// Pattern 1: wsmprovhost.exe spawning suspicious children on destination (remote execution)
| where InitiatingProcessFileName =~ "wsmprovhost.exe"
| where FileName !in~ ("conhost.exe", "WerFault.exe")  // Exclude normal WinRM child processes
| extend Pattern = "WinRM_RemoteExec"
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
         InitiatingProcessFileName, Pattern
| union (
    // Pattern 2: PowerShell WinRM usage (Invoke-Command, Enter-PSSession, New-PSSession)
    DeviceProcessEvents
    | where Timestamp > ago(24h)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | where ProcessCommandLine has_any (
        "Invoke-Command", "Enter-PSSession", "New-PSSession",
        "winrm", "PSSession", "-ComputerName", "WSMan"
      )
    | where ProcessCommandLine has_any ("-ComputerName", "-Session", "wsman://")
    | extend Pattern = "WinRM_PSRemoting"
    | project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
             InitiatingProcessFileName, Pattern
)
| union (
    // Pattern 3: winrm.cmd or winrs.exe execution
    DeviceProcessEvents
    | where Timestamp > ago(24h)
    | where FileName in~ ("winrs.exe", "winrm.cmd")
    | extend Pattern = "WinRM_DirectTool"
    | project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
             InitiatingProcessFileName, Pattern
)
| sort by Timestamp desc

What this catches: The first branch is the highest-signal piece. wsmprovhost.exe is the WinRM provider host — it only runs on the destination side of a remote session. Any child process spawning from it is remote code execution. If you see wsmprovhost.exe spawning cmd.exe, powershell.exe, or anything other than conhost, someone is running commands on that host via WinRM.

The second branch catches the source-side PowerShell invocation. The third catches winrs.exe direct tool usage.

How to use it: Start with Pattern 1 — it’s the cleanest indicator and has minimal false positives. Patterns 2 and 3 will fire on legitimate admin PSRemoting, so tune by allowlisting your IT jump servers and automation accounts. Ansible environments will need to allowlist the Ansible control node’s IP range specifically.

The full T1021.006 detection adds WinRM Operational log event IDs 91 (session created) and 168 (authentication) for destination-side monitoring.

Building Coverage Across T1021

These five queries cover three of the most heavily abused sub-techniques, but T1021 has eight branches. If your environment has mixed operating systems, add detection for SSH lateral movement (T1021.004) — used by Volt Typhoon and every Linux-aware ransomware operation.

A few principles for lateral movement detection specifically:

Watch the protocol, not the tool. Attackers rotate tools. The network primitive (port 3389, 445, 5985/5986) doesn’t change. Your SMB detection should catch Impacket SMBExec even if you’ve never heard of it — because the port 445 connection from a non-system process is what matters.

Baseline your admin patterns. Lateral movement detection is impossible without knowing what normal looks like. Build watchlists of authorized RDP sources, SCCM distribution points, backup agent IPs, and Ansible control nodes. Every exclusion is documented; every exclusion is a blind spot you know about.

Chain detections with credential access. Lateral movement runs on stolen credentials. A detection that fires on LSASS dumping AND shortly after fires on unusual SMB activity from the same host is a high-confidence compromise. Correlate across tactics rather than alerting on each in isolation.

Prioritize destination sensitivity. An RDP session to a printer is worth noting. An RDP session to a domain controller is worth paging on. Add host-based context to your queries — severity should scale with the value of what the attacker just reached.

Browse the full df00tech detection library — 700+ KQL and SPL queries mapped to MITRE ATT&CK, free forever. For playbooks, atomic tests, and tuning guidance, check out df00tech Pro.