Privileged Entra ID account sign-in via legacy authentication protocol
Description
Identifies directory role holders signing in via legacy authentication protocols (which bypass Conditional Access MFA), correlated with a high-impact audit operation within one hour. Suggests credential theft or MFA bypass on legacy-auth accounts.
Query · kql
let timeframe = 1d;
let roleBaseline = 90d;
let correlationWindow = 1h;
let LegacyClients = dynamic([
"Exchange ActiveSync",
"IMAP4",
"MAPI over HTTP",
"POP3",
"SMTP Auth",
"Authenticated SMTP",
"Other clients"
]);
let HighImpactOps = dynamic([
"Add member to role.",
"Add member to role",
"Add service principal credentials.",
"Add service principal credentials",
"Update application - Certificates and secrets management",
"Add owner to service principal.",
"Add owner to service principal",
"Reset user password.",
"Reset user password",
"Set domain authentication.",
"Set domain authentication"
]);
// Role holders from role assignment events over a 90-day baseline
// Using a longer window than the detection timeframe to capture accounts
// that received roles weeks or months ago, not just within the last day
let PrivilegedAccounts =
AuditLogs
| where TimeGenerated >= ago(roleBaseline)
| where OperationName in~ ("Add member to role.", "Add member to role")
| where Result =~ "success"
| mv-expand TargetResource = TargetResources
| where tostring(TargetResource.type) =~ "User"
| extend TargetUpn = tolower(tostring(TargetResource.userPrincipalName))
| where isnotempty(TargetUpn)
| summarize by TargetUpn;
// Legacy auth sign-ins from privileged accounts
let LegacySignIns =
SigninLogs
| where TimeGenerated >= ago(timeframe)
| where ResultType == 0
| where ClientAppUsed in~ (LegacyClients)
| extend UserUpn = tolower(UserPrincipalName)
| join kind=inner PrivilegedAccounts on $left.UserUpn == $right.TargetUpn
| project
SignInTime = TimeGenerated,
UserUpn,
IPAddress,
ClientAppUsed,
AppDisplayName,
Location,
AutonomousSystemNumber,
CorrelationId;
// High-impact audit operations by the same account within the correlation window
let AuditActivity =
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName in~ (HighImpactOps)
| where Result =~ "success"
| extend ActorUpn = tolower(tostring(InitiatedBy.user.userPrincipalName))
| where isnotempty(ActorUpn)
| project AuditTime = TimeGenerated, ActorUpn, OperationName;
LegacySignIns
| join kind=inner AuditActivity on $left.UserUpn == $right.ActorUpn
| where AuditTime between (SignInTime .. (SignInTime + correlationWindow))
| extend AccountName = tostring(split(UserUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(UserUpn, "@")[1])
| project
SignInTime,
UserUpn,
AccountName,
AccountUPNSuffix,
IPAddress,
ClientAppUsed,
AppDisplayName,
Location,
AuditTime,
OperationName,
CorrelationId
| sort by SignInTime desc