Detection rules › Kusto

RDP Nesting

Severity
medium
Time window
8d
Group by
Account, AccountType, Activity, Computer, FirstComputer, FirstComputerIP, FirstRemoteIPAddress, LogonTypeName, ProcessName, SecondComputer, SecondComputerIP, SecondHop, SecondRemoteIPAddress
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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.

MITRE ATT&CK coverage

TacticTechniques
Lateral Movement

Telemetry coverage

Rule body

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),
  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
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: FirstComputer
      - identifier: HostName
        columnName: HostName1
      - identifier: NTDomain
        columnName: HostNameDomain1
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: SecondComputer
      - identifier: HostName
        columnName: HostName2
      - identifier: NTDomain
        columnName: HostNameDomain2
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: FirstIPAddress
version: 1.2.8
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Stage 0: let

let endtime = 1d;
let rdpConnections = (union <inlined as stages below>;
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;

Stage 1: source

let rdpConnections

Stage 2: source

let listOfFirstComputer

Stage 3: source

let listOfSecondComputer

Stage 4: source

let resolvedIPs

Stage 5: union

union of 2 branches

Stage 6: source

SecurityEvent

Stage 7: where

where TimeGenerated >= ago(86400s)

Stage 8: where

where EventID == 4624 and LogonType == 10

Stage 9: extend

extend Account, FirstComputer, FirstHop, FirstRemoteIPAddress

Stage 10: source

WindowsEvent

Stage 11: where

where TimeGenerated >= ago(86400s)

Stage 12: where

where EventData contains 10 and EventID == 4624

Stage 13: extend

extend LogonType

Stage 14: where

where LogonType == 10

Stage 15: extend (3 consecutive steps)

extend Account, FirstComputer, FirstHop, FirstRemoteIPAddress, IpAddress

Stage 16: join

join kind=inner (...) on Account

Stage 17: distinct

distinct Account, AccountType, Activity, FirstComputer, FirstHop, FirstRemoteIPAddress, LogonTypeName, ProcessName, SecondComputer, SecondHop, SecondRemoteIPAddress

Stage 18: join

join kind=inner (resolvedIPs) on FirstComputer, Computer

Stage 19: join

join kind=inner (resolvedIPs) on SecondComputer, Computer

Stage 20: distinct

distinct Account, AccountType, Activity, FirstComputer, FirstHop, FirstRemoteIPAddress, LogonTypeName, ProcessName, SecondComputer, SecondHop, SecondRemoteIPAddress

Stage 21: where

where FirstComputer != SecondComputer and FirstRemoteIPAddress != SecondRemoteIPAddress and SecondHop > FirstHop

Stage 22: where SecondHop - FirstHop <= 30m

where SecondHop - FirstHop <= 30m

Stage 23: where

where SecondRemoteIPAddress =~ FirstComputerIP

Stage 24: summarize

summarize FirstHopFirstSeen, FirstHopLastSeen by Account, FirstComputer, FirstComputerIP, FirstRemoteIPAddress, SecondHop, SecondComputer, SecondComputerIP, SecondRemoteIPAddress, AccountType, Activity, LogonTypeName, ProcessName

Stage 25: extend (5 consecutive steps)

extend AccountNTDomain, AccountName, DomainIndex, HostName1, HostName2, HostNameDomain1, HostNameDomain2

Stage 26: project-away

project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientIPis_not_null
  • (no value, null check)
field:"ClientIP" kind:is_not_null
ConnectedNetworksis_not_null
  • (no value, null check)
field:"ConnectedNetworks" kind:is_not_null
DeviceNamein
  • listOfFirstComputer
  • listOfSecondComputer
field:"Computer" kind:in
EventDatamatch
  • 10 transforms: term corpus 3 (kusto 3)
field:"EventData" kind:match value:"10"
EventIDeq
  • 4624 corpus 29 (splunk 13, kusto 11, chronicle 4, elastic 1)
field:"EventID" kind:eq value:"4624"
FirstComputercross_field_compare
  • SecondComputer transforms: op:ne
field:"FirstComputer" kind:cross_field_compare value:"SecondComputer"
FirstRemoteIPAddresscross_field_compare
  • SecondRemoteIPAddress transforms: op:ne
field:"FirstRemoteIPAddress" kind:cross_field_compare value:"SecondRemoteIPAddress"
LogonTypeeq
  • 10 corpus 8 (kusto 4, sigma 3, splunk 1)
field:"LogonType" kind:eq value:"10"
NetworkAdapterStatuseq
  • Up
field:"NetworkAdapterStatus" kind:eq value:"Up"
SecondHopcross_field_compare
  • FirstHop transforms: op:gt
field:"SecondHop" kind:cross_field_compare value:"FirstHop"
SecondRemoteIPAddresscross_field_compare
  • FirstComputerIP transforms: op:eq
field:"SecondRemoteIPAddress" kind:cross_field_compare value:"FirstComputerIP"

Output fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
AccountTypesummarize
Activitysummarize
FirstComputersummarize
FirstComputerIPsummarize
FirstHopFirstSeensummarize
FirstHopLastSeensummarize
FirstRemoteIPAddresssummarize
LogonTypeNamesummarize
ProcessNamesummarize
SecondComputersummarize
SecondComputerIPsummarize
SecondHopsummarize
SecondRemoteIPAddresssummarize
AccountNTDomainextend
AccountNameextend
HostName1extend
HostNameDomain1extend
HostName2extend
HostNameDomain2extend