top of page

Potential Lateral Movement via RDP Detected

Writer: Aniket RTAniket RT

Updated: Jan 12

High Level Overview:

Quick Deploy:




Description:

 

This query detects potential lateral movement within a network by identifying when an RDP connection (EventID 4624, LogonTypes 3,7 and 10) is made to an initial system, followed by a subsequent RDP connection from the initial compromised system to a second system, using the same account within a 30-minute window and considering only logon type 10 (Interactive Remote). This approach focuses on highlighting unusual RDP behavior that suggests lateral movement, which is often associated with attacker tactics during a network breach.


Framework:

MITRE ATT&CKTM

 

Tactic:

Lateral Movement

TA0008

 

Technique:

Remote Services

T1021

 

Rule type: KQL

 

Rule indices:


Rule Id: de3fffa4-2bc6-47fb-948c-9a9b1acb727d

 

Severity: Medium/High

 

Runs every: 1day/1day

 

Trigger an alert for each event: Yes

 

References: None

 

Domain: Endpoint

OS: Windows

Event ID: 4625

Use Case: Threat Detection

Tactic: Lateral Movement

Version: 1.1

 

Rule authors: Aniket


Pre-Requisites

 

This rule requires data coming in from Azure Monitor Agent using a data collection rule within Microsoft Sentinel.


Windows Security Events via AMA data connector.


Microsoft Sentinel Workspace and enough RBAC permissions!

 

Investigation Guide

 

Triage and analysis

 

Investigating Suspicious Lateral Movement

 

The rule identifies movement within a network by identifying when an RDP connection (EventID 4624, LogonTypes 3,7 and 10) is made to an initial system, followed by a subsequent RDP connection from the initial compromised system to a second system, using the same account within a 30-minute window and considering only logon type 10; thus indicating lateral movement

 

This rule will generate a lot of noise for systems that are being used as a jump host or in an environment where administrators usually move from one server to the other.

 

In case this rule generates too much noise, consider grouping the alerts together and add whitelisted entities to a watchlist to exempt them from this rule.


Also; consider removing logon types 3,7 and keep only 10 in order to avoid unncessary noise.

 

Possible investigation steps

 

  • Investigate the login failure user name(s).

  • Investigate the source IP address of the failed SSH login attempt(s).

  • Run a check against Threat Intelligence database for the IP address.

  • Investigate other alerts associated with the user/host during the past 48 hours.

  • Identify the source and the target computer and their roles in the environment.

  • False positive analysis and exemption creation, if required.

 

Related Rules

 

RDP Nesting - 69a45b05-71f5-45ca-8944-2e038747fb39

 

Response and remediation

 

  • Initiate the incident response process based on the outcome of the triage.

  • Isolate the involved hosts to prevent further post-compromise behavior.

  • Add the attacker IP Address to Threat intelligence and consider blocking at network level.

  • Investigate credential exposure on systems compromised or used by the attacker to ensure all compromised accounts are identified. Reset passwords for these accounts and other potentially compromised credentials, such as email, business systems, and web services.

  • Determine the initial vector abused by the attacker and take action to prevent reinfection through the same vector.


Overview

 

I was trying to trigger out of the box RDP Nesting rule however noticed that it did not work as expected. Upon closer look at the query, I noticed that it is trying to join on the field "Account" which in my case was different as I was trying to move laterally across using local account. For eg. winserver01/aniket != winserver02/aniket

 

Another caveat that I noticed in the out of the box rule was that it was trying to check for the IP Address within a table namely "DeviceNetworkInfo" which did not exist in my environment. As this table was not there, the subsequent joins that follow will give an empty result hence rest of the query will never execute.

 

So I sat down and tried to optimize the query, have a look and finetune according to your environment!


Test Setup

 

I deployed three virtual machines within Azure for demo purposes. I placed them all in the same Virtual Network and made sure to create public inbound rules both against HTTPS and RDP.

 

For the first part of this demo, I will RDP into Server 1 using my Public IP and then RDP from Server 1 into Server 2 in a short time-period.

 

For the second part of this demo, I will then RDP from Server 2 into Server 3.

 

Make sure the following inbound rules are allowed.


Once logged in, try to RDP to the next server 10.0.0.5

 

You might get the following error but don’t worry, the event is logged.


Next, I will try to RDP using my public ip to server 3 (10.0.0.6) and then try to RDP locally to server 2 (10.0.0.5)


Incident:

 

Note- Here, I have used alert grouping and hence, you can see that there are 2 events attached to this incident.


Let's have a look at the most recent incident that I had to wait for!




Tip: If you don't like to wait and want to see the incident immediately, consider using the Start Running property (set to 15 minutes from current date-time) when setting up the Rule Logic within the analytic rule.



Rule Query:


//Suspicious Lateral Movement across Two servers via RDP
let endtime = 1d;
let rdpConnection1=
  SecurityEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4624 and LogonType in (7,3,10)
  // Labeling the first RDP connection time, computer and ip
  | extend
  FirstHop = bin(TimeGenerated, 1m),
  FirstComputer = toupper(Computer),
  FirstRemoteIPAddress = IpAddress,
  FirstComputerDomain = tostring(split(Account, @"\")[0]),
  Account = tolower(Account);
let rdpConnection2=
  SecurityEvent
  | where TimeGenerated >= ago(endtime)
  | where EventID == 4624 and LogonType in (7,3,10) 
  // Labeling the second RDP connection time, computer and ip
  | extend
  SecondHop = bin(TimeGenerated, 1m),
  SecondComputer = toupper(Computer),
  SecondRemoteIPAddress = IpAddress,
  SecondComputerDomain = tostring(split(Account, @"\")[0]),
  Account = tolower(Account);
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType in (7,3,10)
| join kind=innerunique rdpConnection1 on TargetUserName
| join kind=innerunique rdpConnection2 on TargetUserName
| where FirstComputer != SecondComputer
| where FirstRemoteIPAddress != SecondRemoteIPAddress
  and SecondHop > FirstHop
// Ensure the second hop occurs within 30 minutes of the first hop
| where SecondHop <= FirstHop + 30m
  //| where SecondRemoteIPAddress == FirstRemoteIPAddress
  //| where ThirdRemoteIPAddress == SecondComputerIP
 // | extend AccountNTDomain = tostring(split(Account, @"\")[0])
  //| extend Account = tostring(split(Account, @"\")[1])
  | summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)
  by
  TargetUserName,
  FirstHop,
  FirstComputer,
  FirstRemoteIPAddress,
  FirstComputerDomain,
  SecondHop,
  SecondComputer,
  SecondRemoteIPAddress,
  SecondComputerDomain,
  AccountType,
  Activity,
  LogonTypeName
  | where LogonTypeName =~ '10 - RemoteInteractive'
  | distinct
  TargetUserName,
  FirstHop,
  FirstComputer,
  FirstRemoteIPAddress,
  FirstComputerDomain,
  SecondHop,
  SecondComputer,
  SecondRemoteIPAddress,
  SecondComputerDomain,
  AccountType,
  Activity,
  LogonTypeName

Query Breakdown

 

Step 1: Define Time Range

let endtime = 1d;
  • Set the time period/range to the last 24 hours (1d).

 

Step 2: Extract First RDP Connections

 

let rdpConnection1 = SecurityEvent

| where TimeGenerated >= ago(endtime)

| where EventID == 4624 and LogonType in (7,3,10)

| extend FirstHop = bin(TimeGenerated, 1m),          FirstComputer = toupper(Computer),          FirstRemoteIPAddress = IpAddress,          FirstComputerDomain = tostring(split(Account, @"\")[0]),          Account = tolower(Account);

  • Filters logon events for RDP (LogonType 7, 3, 10) in the last day.

  • Labels the first RDP hop with time, computer, and remote IP.

  • Consider reducing to only logon-type 10 to reduce noise

 

Step 3: Extract Second RDP Connections

let rdpConnection2 = SecurityEvent

| where TimeGenerated >= ago(endtime)

| where EventID == 4624 and LogonType in (7,3,10)

| extend SecondHop = bin(TimeGenerated, 1m),          SecondComputer = toupper(Computer),          SecondRemoteIPAddress = IpAddress,          SecondComputerDomain = tostring(split(Account, @"\")[0]),          Account = tolower(Account);
  • Performs the same process as Step 2 but for potential second RDP hops.

  • Consider reducing to only logon-type 10 to reduce noise

 

Step 4: Correlate First and Second Connections

SecurityEvent

| where TimeGenerated >= ago(endtime)

| where EventID == 4624 and LogonType in (7,3,10)

| join kind=innerunique rdpConnection1 on TargetUserName

| join kind=innerunique rdpConnection2 on TargetUserName
  • Joins the main dataset with rdpConnection1 and rdpConnection2 using the TargetUserName field.

  • Ensures the same user is involved in both hops.

 

Step 5: Filter Suspicious Behavior


| where FirstComputer != SecondComputer

| where FirstRemoteIPAddress != SecondRemoteIPAddress and SecondHop > FirstHop

| where SecondHop <= FirstHop + 30m
  • Ensures the source and destination computers/IPs are different.

  • Ensures the second hop happens only after the first hop but within 30 minutes.

 

Step 6: Summarize and Refine Results

| summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)  by TargetUserName, FirstHop, FirstComputer, FirstRemoteIPAddress,     FirstComputerDomain, SecondHop, SecondComputer, SecondRemoteIPAddress,     SecondComputerDomain, AccountType, Activity, LogonTypeName

| where LogonTypeName =~ '10 - RemoteInteractive'

| distinct TargetUserName, FirstHop, FirstComputer, FirstRemoteIPAddress,           FirstComputerDomain, SecondHop, SecondComputer, SecondRemoteIPAddress,           SecondComputerDomain, AccountType, Activity, LogonTypeName
  • Groups the data by user and connection attributes.

  • Filters only LogonTypeName 10 - RemoteInteractive events (specific to RDP).

  • Extracts unique results for easier interpretation.


Entity Mapping



Conclusion

 

This query is useful in detecting lateral movement in the network, a technique often used by attackers post-initial compromise to move between machines and escalate privileges. By identifying unexpected RDP connections between servers involving the same user within a short timeframe, security teams can investigate potential breaches. Automation can also be leveraged to block malicious ip address.


ARM Template


{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "workspace": {
            "type": "String"
        }
    },
    "resources": [
        {
            "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/de3fffa4-2bc6-47fb-948c-9a9b1acb727d')]",
            "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/de3fffa4-2bc6-47fb-948c-9a9b1acb727d')]",
            "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
            "kind": "Scheduled",
            "apiVersion": "2023-12-01-preview",
            "properties": {
                "displayName": "Suspicious Lateral Movement Detected via RDP (2 Servers)",
                "description": "Query detects potential lateral movement within a network by identifying when an RDP connection (EventID 4624, LogonTypes 3,7 and 10) is made to an initial system, followed by a subsequent RDP connection from the initial compromised system to a second system, using the same account within a 30-minute window and considering only logon type 10 (Interactive Remote). This approach focuses on highlighting unusual RDP behavior that suggests lateral movement, which is often associated with attacker tactics during a network breach.",
                "severity": "Medium",
                "enabled": true,
                "query": "//Suspicious Lateral Movement across Two servers via RDP\r\nlet endtime = 1d;\r\nlet rdpConnection1=\r\n  SecurityEvent\r\n  | where TimeGenerated >= ago(endtime)\r\n  | where EventID == 4624 and LogonType in (7,3,10)\r\n  // Labeling the first RDP connection time, computer and ip\r\n  | extend\r\n  FirstHop = bin(TimeGenerated, 1m),\r\n  FirstComputer = toupper(Computer),\r\n  FirstRemoteIPAddress = IpAddress,\r\n  FirstComputerDomain = tostring(split(Account, @\"\\\")[0]),\r\n  Account = tolower(Account);\r\nlet rdpConnection2=\r\n  SecurityEvent\r\n  | where TimeGenerated >= ago(endtime)\r\n  | where EventID == 4624 and LogonType in (7,3,10)\r\n  // Labeling the second RDP connection time, computer and ip\r\n  | extend\r\n  SecondHop = bin(TimeGenerated, 1m),\r\n  SecondComputer = toupper(Computer),\r\n  SecondRemoteIPAddress = IpAddress,\r\n  SecondComputerDomain = tostring(split(Account, @\"\\\")[0]),\r\n  Account = tolower(Account);\r\nSecurityEvent\r\n| where TimeGenerated >= ago(endtime)\r\n| where EventID == 4624 and LogonType in (7,3,10)\r\n| join kind=innerunique rdpConnection1 on TargetUserName\r\n| join kind=innerunique rdpConnection2 on TargetUserName\r\n| where FirstComputer != SecondComputer\r\n| where FirstRemoteIPAddress != SecondRemoteIPAddress\r\n  and SecondHop > FirstHop\r\n// Ensure the second hop occurs within 30 minutes of the first hop\r\n| where SecondHop <= FirstHop + 30m\r\n  //| where SecondRemoteIPAddress == FirstRemoteIPAddress\r\n  //| where ThirdRemoteIPAddress == SecondComputerIP\r\n // | extend AccountNTDomain = tostring(split(Account, @\"\\\")[0])\r\n  //| extend Account = tostring(split(Account, @\"\\\")[1])\r\n  | summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)\r\n  by\r\n  TargetUserName,\r\n  FirstHop,\r\n  FirstComputer,\r\n  FirstRemoteIPAddress,\r\n  FirstComputerDomain,\r\n  SecondHop,\r\n  SecondComputer,\r\n  SecondRemoteIPAddress,\r\n  SecondComputerDomain,\r\n  AccountType,\r\n  Activity,\r\n  LogonTypeName\r\n  | where LogonTypeName =~ '10 - RemoteInteractive'\r\n  | distinct\r\n  TargetUserName,\r\n  FirstHop,\r\n  FirstComputer,\r\n  FirstRemoteIPAddress,\r\n  FirstComputerDomain,\r\n  SecondHop,\r\n  SecondComputer,\r\n  SecondRemoteIPAddress,\r\n  SecondComputerDomain,\r\n  AccountType,\r\n  Activity,\r\n  LogonTypeName\r\n",
                "queryFrequency": "P1D",
                "queryPeriod": "P7D",
                "triggerOperator": "GreaterThan",
                "triggerThreshold": 0,
                "suppressionDuration": "PT5H",
                "suppressionEnabled": false,
                "startTimeUtc": null,
                "tactics": [
                    "LateralMovement"
                ],
                "techniques": [
                    "T1021"
                ],
                "subTechniques": [],
                "alertRuleTemplateName": null,
                "incidentConfiguration": {
                    "createIncident": true,
                    "groupingConfiguration": {
                        "enabled": false,
                        "reopenClosedIncident": false,
                        "lookbackDuration": "PT5H",
                        "matchingMethod": "AllEntities",
                        "groupByEntities": [],
                        "groupByAlertDetails": [],
                        "groupByCustomDetails": []
                    }
                },
                "eventGroupingSettings": {
                    "aggregationKind": "AlertPerResult"
                },
                "alertDetailsOverride": null,
                "customDetails": null,
                "entityMappings": [
                    {
                        "entityType": "Account",
                        "fieldMappings": [
                            {
                                "identifier": "Name",
                                "columnName": "TargetUserName"
                            }
                        ]
                    },
                    {
                        "entityType": "Host",
                        "fieldMappings": [
                            {
                                "identifier": "HostName",
                                "columnName": "FirstComputer"
                            }
                        ]
                    },
                    {
                        "entityType": "Host",
                        "fieldMappings": [
                            {
                                "identifier": "HostName",
                                "columnName": "SecondComputer"
                            }
                        ]
                    },
                    {
                        "entityType": "IP",
                        "fieldMappings": [
                            {
                                "identifier": "Address",
                                "columnName": "FirstRemoteIPAddress"
                            }
                        ]
                    }
                ],
                "sentinelEntitiesMappings": null,
                "templateVersion": null
            }
        }
    ]
}

 
 
 

Comments


Microsoft Sentinel Blog

©2024 by Microsoft Sentinel Blog. Proudly created with Wix.com

bottom of page