Detection rules › Kusto
Hunt for ADWS requests from unknown devices
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
- https://falconforce.nl/soaphound-tool-to-collect-active-directory-data-via-adws/
- https://github.com/FalconForceTeam/FalconFriday/blob/master/Discovery/ADWS_Connection_from_Unexpected_Binary-Win.md
- https://github.com/FalconForceTeam/FalconFriday/blob/master/Discovery/ADWS_Connection_from_Process_Injection_Target-Win.md
- https://cyberlandsec.com/soapy-the-ultimate-stealthy-active-directory-enumeration-tool-via-adws/
- https://github.com/FalconForceTeam/SOAPHound
Event coverage
| Provider | Event/ActionType | Title |
|---|---|---|
| Sysmon | Event ID 3 | Network connection |
| Security-Auditing | Event ID 5156 | The Windows Filtering Platform has permitted a connection. |
| Defender-DeviceNetworkEvents | any | Network activity (any) |
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.
| Field | Kind | Values |
|---|---|---|
ActionType | ne |
|
InitiatingProcessFolderPath | eq |
|
LocalPort | eq |
|
OnboardingStatus | ne |
|
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.
| Field | Source |
|---|---|
ConnectionTimes | project |
DeviceName | project |
RemoteDeviceFirstSeen | project |
RemoteDeviceId | project |
RemoteDeviceName | project |
RemoteIP | project |
RemoteOnboardingStatus | project |