Detection rules › Kusto

Hunt domains with Seamless SSO enabled in Entra ID Connect

Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

With below KQL query you can search through the IdentityLogon events of Microsoft Defender for Identity to find users and devices still using Seamless SSO in Entra ID Connect. This feature has been marked by the community multiple times as a security risk, and should be disabled if not in use. The KQL query returns the domains where Seamless SSO is enabled, allong with the related users and devices. On top of that, devices get enriched to find their OS distribution, version, and join type and tells you if Seamless SSO is expected to be used for the related device or not. If there are no results or if all results are showing 'No' for the 'Seamless SSO Expected' column, it should be save to disable the feature in Entra ID connect. !Important: This query relies on the Domain Controller EventID 4769 and Defender for Identity. Make sure the EventID is being logged and Defender for Identity is healthy. For more information see references!

References

Rule body yaml

// Get all device info we can find
let devices = (
    DeviceInfo
    // Search for 14 days
    | where TimeGenerated > ago(14d)
    // Normalize DeviceName 
    // --> if it is an IP Address we keep it
    // --> If it is not an IP Address we only use the hostname for correlation
    | extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
    // Only get interesting data
    | distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);
IdentityLogonEvents
// Get the last 30 days of logon events on Domain Controllers
| where TimeGenerated > ago(30d)
// Search for Seamless SSO events
| where Application == "Active Directory" and Protocol == "Kerberos"
| where TargetDeviceName == "AZUREADSSOACC"
// Save the domain name of the Domain Controller
| extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1])
// Normalize DeviceName 
// --> if it is an IP Address we keep it
// --> If it is not an IP Address we only use the hostname for correlation
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
// Only use interesting data and find more info regarding the source device
| distinct AccountUpn, OnPremisesDomainName, DeviceName
| join kind=leftouter devices on DeviceName 
| project-away DeviceName1
// Check if Seamless SSO usage is expected
| extend ['Seamless SSO Expected'] = case(
    // Cases where we do not expect Seamless SSO to be used
    JoinType == "Hybrid Azure AD Join" or 
    JoinType == "AAD Joined" or
    JoinType == "AAD Registered", "No",
    // Cases where we do expect Seamless SSO to be used
    JoinType == "Domain Joined" or 
    (OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes", 
    // Cases that need to be verified
    "Unknown (to verify)"
)

Stages and Predicates

Let binding: devices

let devices = (
    DeviceInfo
    | where TimeGenerated > ago(14d)
    | extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
    | distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);

Stage 1: source

IdentityLogonEvents

Stage 2: where

| where TimeGenerated > ago(30d)

Stage 3: where

| where Application == "Active Directory" and Protocol == "Kerberos"

Stage 4: where

| where TargetDeviceName == "AZUREADSSOACC"

Stage 5: extend

| extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1])

Stage 6: extend

| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
DeviceName =
ifipv4_is_in_range(DeviceName, "10.0.0.0/8")DeviceName
elsetolower(split(DeviceName, ".")[0])

Stage 7: distinct

| distinct AccountUpn, OnPremisesDomainName, DeviceName

Stage 8: join

| join kind=leftouter devices on DeviceName

Stage 9: project-away

| project-away DeviceName1

Stage 10: extend

| extend ['Seamless SSO Expected'] = case(
    JoinType == "Hybrid Azure AD Join" or 
    JoinType == "AAD Joined" or
    JoinType == "AAD Registered", "No",
    JoinType == "Domain Joined" or 
    (OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes", 
    "Unknown (to verify)"
)
Seamless SSO Expected =
if((JoinType == "Hybrid Azure AD Join" or JoinType == "AAD Joined") or JoinType == "AAD Registered")"No"
elif(JoinType == "Domain Joined" or (OSPlatform startswith "Windows" and OSVersion < 10.0))"Yes"
else"Unknown (to verify)"

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
Applicationeq
  • Active Directory transforms: cased
Protocoleq
  • Kerberos transforms: cased
TargetDeviceNameeq
  • AZUREADSSOACC transforms: cased

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
OnPremisesDomainNameextend
DeviceNameextend
Seamless SSO Expectedextend