Detection rules › Kusto

Hunt for ADWS requests from unknown devices

Group by
DeviceName, IPAddress, RemoteIP
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This hunting rule searches for incomming ADWS connections on Domain Controllers (DC's need to be onboarded in Defender for Endpoint) from IP Addresses that cannot be linked to MDE onboarded devices.

MITRE ATT&CK coverage

References

Event coverage

Rule body yaml

let device_info = (
    // Get device network info from last 7 days
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    // Expand the IP Addresses of the devices
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Distinct IP address for each device
    | distinct DeviceName, DeviceId, IPAddress
    // Search for each device if it is onboarded or not
    | join kind=inner (
        DeviceInfo 
        | where Timestamp > ago(7d)
        | distinct DeviceName, DeviceId, OnboardingStatus
        // Get the first timestamp the device was seen
        | join kind=inner (
            DeviceInfo
            | where Timestamp > ago(30d)
            | summarize FirstSeen = arg_min(Timestamp, DeviceId) by DeviceId
        ) on DeviceId
        | project-away DeviceId1, DeviceId2
    ) on DeviceId, DeviceName
    | project-away DeviceName1, DeviceId1
);
// Get incomming traffic on ADWS port and save unique remote IP addresses
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where ActionType != "ListeningConnectionCreated"
| where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe"
| where LocalPort == "9389"
| summarize ConnectionTimes=make_list(Timestamp) by RemoteIP, DeviceName
// Get device information of remote IP addresses, results for IP we do not find information for are allowed
| join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress
| project-away IPAddress
// Check if the remote IPs are onboarded devices or not
| where OnboardingStatus != "Onboarded"
// Make output better
| project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen

let device_info = (
    // Get device network info from last 7 days
    DeviceNetworkInfo
    | where TimeGenerated > ago(7d)
    // Expand the IP Addresses of the devices
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Distinct IP address for each device
    | distinct DeviceName, DeviceId, IPAddress
    // Search for each device if it is onboarded or not
    | join kind=inner (
        DeviceInfo 
        | where TimeGenerated > ago(7d)
        | distinct DeviceName, DeviceId, OnboardingStatus
        // Get the first timestamp the device was seen
        | join kind=inner (
            DeviceInfo
            | where TimeGenerated > ago(30d)
            | summarize FirstSeen = arg_min(TimeGenerated, DeviceId) by DeviceId
        ) on DeviceId
        | project-away DeviceId1, DeviceId2
    ) on DeviceId, DeviceName
    | project-away DeviceName1, DeviceId1
);
// Get incomming traffic on ADWS port and save unique remote IP addresses
DeviceNetworkEvents
| where TimeGenerated > ago(30d)
| where ActionType != "ListeningConnectionCreated"
| where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe"
| where LocalPort == "9389"
| summarize ConnectionTimes=make_list(TimeGenerated) by RemoteIP, DeviceName
// Get device information of remote IP addresses, results for IP we do not find information for are allowed
| join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress
| project-away IPAddress
// Check if the remote IPs are onboarded devices or not
| where OnboardingStatus != "Onboarded"
// Make output better
| project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen

Stages and Predicates

Let binding: device_info

let device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    | distinct DeviceName, DeviceId, IPAddress
    | join kind=inner (
        DeviceInfo 
        | where Timestamp > ago(7d)
        | distinct DeviceName, DeviceId, OnboardingStatus
        | join kind=inner (
            DeviceInfo
            | where Timestamp > ago(30d)
            | summarize FirstSeen = arg_min(Timestamp, DeviceId) by DeviceId
        ) on DeviceId
        | project-away DeviceId1, DeviceId2
    ) on DeviceId, DeviceName
    | project-away DeviceName1, DeviceId1
);

Stage 1: source

DeviceNetworkEvents

Stage 2: where

| where Timestamp > ago(30d)

Stage 3: where

| where ActionType != "ListeningConnectionCreated"

Stage 4: where

| where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe"

Stage 5: where

| where LocalPort == "9389"

Stage 6: summarize

| summarize ConnectionTimes=make_list(Timestamp) by RemoteIP, DeviceName

Stage 7: join

| join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress

Stage 8: project-away

| project-away IPAddress

Stage 9: where

| where OnboardingStatus != "Onboarded"

Stage 10: project

| project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
ActionTypene
  • ListeningConnectionCreated transforms: cased corpus 3 (kusto 3)
InitiatingProcessFolderPatheq
  • c:\windows\adws\microsoft.activedirectory.webservices.exe transforms: cased
LocalPorteq
  • 9389 transforms: cased
OnboardingStatusne
  • Onboarded transforms: cased corpus 5 (kusto 5)

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
ConnectionTimesproject
DeviceNameproject
RemoteDeviceFirstSeenproject
RemoteDeviceIdproject
RemoteDeviceNameproject
RemoteIPproject
RemoteOnboardingStatusproject