An adversary may use RDP Nesting to move laterally by establishing an RDP session from a compromised host to another system, masking their presence through nested connections. SOC teams should proactively hunt for this behavior in Azure Sentinel to detect and mitigate potential lateral movement and persistent access in the network.
KQL Query
let endtime = 1d;
// Function to resolve hostname to IP address using DNS logs or a lookup table (example syntax)
let rdpConnections =
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
// Labeling the first RDP connection time, computer and ip
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 10 // Labeling the first RDP connection time, computer and ip
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)
))
| join kind=inner (
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
// Labeling the second RDP connection time, computer and ip
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where LogonType == 10 // Labeling the second RDP connection time, computer and ip
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
))
)
on Account
| distinct
Account,
FirstHop,
FirstComputer,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName;
// Resolve hostnames to IP addresses device network Ip's
let listOfFirstComputer = rdpConnections | distinct FirstComputer;
let listOfSecondComputer = rdpConnections | distinct SecondComputer;
let resolvedIPs =
DeviceNetworkInfo
| where TimeGenerated >= ago(endtime)
| where isnotempty(ConnectedNetworks) and NetworkAdapterStatus == "Up"
| extend ClientIP = tostring(parse_json(IPAddresses[0]).IPAddress)
| where isnotempty(ClientIP)
| where DeviceName in~ (listOfFirstComputer) or DeviceName in~ (listOfSecondComputer)
| summarize arg_max(TimeGenerated, ClientIP) by Computer= DeviceName
| project Computer=toupper(Computer), ResolvedIP = ClientIP;
// Join resolved IPs with the RDP connections
rdpConnections
| join kind=inner (resolvedIPs) on $left.FirstComputer == $right.Computer
| join kind=inner (resolvedIPs) on $left.SecondComputer == $right.Computer
// | where ResolvedIP != ResolvedIP1
| distinct
Account,
FirstHop,
FirstComputer,
FirstComputerIP = ResolvedIP,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondComputerIP = ResolvedIP1,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName
// Ensure the first connection is before the second connection
// Identify only RDP to another computer from within the first RDP connection by only choosing matches where the Computer names do not match
// Ensure the IPAddresses do not match by excluding connections from the same computers with first hop RDP connections to multiple computers
| where FirstComputer != SecondComputer
and FirstRemoteIPAddress != SecondRemoteIPAddress
and SecondHop > FirstHop
// Ensure the second hop occurs within 30 minutes of the first hop
| where SecondHop <= FirstHop + 30m
| where SecondRemoteIPAddress == FirstComputerIP
| summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)
by
Account,
FirstComputer,
FirstComputerIP,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondComputerIP,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName
| extend
AccountName = tostring(split(Account, @"")[1]),
AccountNTDomain = tostring(split(Account, @"")[0])
| extend
HostName1 = tostring(split(FirstComputer, ".")[0]),
DomainIndex = toint(indexof(FirstComputer, '.'))
| extend HostNameDomain1 = iff(DomainIndex != -1, substring(FirstComputer, DomainIndex + 1), FirstComputer)
| extend
HostName2 = tostring(split(SecondComputer, ".")[0]),
DomainIndex = toint(indexof(SecondComputer, '.'))
| extend HostNameDomain2 = iff(DomainIndex != -1, substring(SecondComputer, DomainIndex + 1), SecondComputer)
| project-away DomainIndex
id: 69a45b05-71f5-45ca-8944-2e038747fb39
name: RDP Nesting
description: |
'Query detects potential lateral movement within a network by identifying when an RDP connection (EventID 4624, LogonType 10) is made to an initial system, followed by a subsequent RDP connection from that system to another, using the same account within a 60-minute window.
To reduce false positives, it excludes scenarios where the same account has made 5 or more connections to the same set of computers in the previous 7 days. This approach focuses on highlighting unusual RDP behaviour that suggests lateral movement, which is often associated with attacker tactics during a network breach.'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1021
query: |
let endtime = 1d;
// Function to resolve hostname to IP address using DNS logs or a lookup table (example syntax)
let rdpConnections =
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
// Labeling the first RDP connection time, computer and ip
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 10 // Labeling the first RDP connection time, computer and ip
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)
))
| join kind=inner (
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
// Labeling the second RDP connection time, computer and ip
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where LogonType == 10 // Labeling the second RDP connection time, computer and ip
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRe
| Sentinel Table | Notes |
|---|---|
SecurityEvent | Ensure this data connector is enabled |
WindowsEvent | Ensure this data connector is enabled |
Scenario: Remote Administration Tool (RAT) Usage
Description: A legitimate security tool like Mandiant’s Red Canary or Carbon Black may establish an RDP session to a system for remote analysis or incident response.
Filter/Exclusion: Exclude IP addresses associated with known security tools or use a filter like SourceIP in (192.168.1.100, 192.168.1.101) for trusted security infrastructure.
Scenario: Scheduled System Maintenance via RDP
Description: An admin may use RDP to perform routine maintenance on a server, such as updating software or applying patches.
Filter/Exclusion: Filter by UserAccount like User contains "Admin" or "ServiceAccount" and check for EventID 4663 (logoff) to identify legitimate session closures.
Scenario: Remote PowerShell Session with RDP
Description: An admin may connect via RDP to a system and then run PowerShell scripts remotely, which can be mistaken for lateral movement.
Filter/Exclusion: Use a filter like ProcessName != "powershell.exe" or check for EventID 4688 (process creation) to identify script execution and correlate with RDP sessions.
Scenario: RDP to a Jump Server for Access to Internal Systems
Description: A user may log in to a jump server (e.g., Cisco ASA, Fortigate) via RDP and then access internal systems, which can be flagged as nesting.
Filter/Exclusion: Filter by ComputerName like "JumpServer01" or use a UserGroup filter such as UserGroup contains "NetworkOps" to identify authorized access.
**Scenario: RDP Session from a