Sign-in from unseen IP within 60 minutes of MFA disabled for account
Description
Identifies successful sign-ins from IP addresses not seen in the prior 30 days occurring within 60 minutes of MFA being disabled for the same account, consistent with post-compromise credential use after weakening authentication.
Query · kql
let timeframe = 1d;
let correlationWindow = 60m;
let lookback = 30d;
let DisabledMFA =
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName in~ (
"Disable Strong Authentication",
"User deleted security info"
)
| where Result =~ "success"
| extend AffectedUser = tolower(tostring(TargetResources[0].userPrincipalName))
| where isnotempty(AffectedUser)
| project AffectedUser, MFADisabledTime = TimeGenerated;
let KnownIPs =
SigninLogs
| where TimeGenerated >= ago(timeframe + lookback) and TimeGenerated < ago(timeframe)
| where ResultType == 0
| project UserPrincipalName = tolower(UserPrincipalName), IPAddress
| summarize KnownIPSet = make_set(IPAddress, 1000)
by UserPrincipalName;
SigninLogs
| where TimeGenerated >= ago(timeframe)
| where ResultType == 0
| extend UserUpn = tolower(UserPrincipalName)
| join kind=inner DisabledMFA on $left.UserUpn == $right.AffectedUser
| where TimeGenerated >= MFADisabledTime and TimeGenerated <= MFADisabledTime + correlationWindow
| join kind=leftouter KnownIPs on $left.UserUpn == $right.UserPrincipalName
| where isnull(KnownIPSet) or not(set_has_element(KnownIPSet, IPAddress))
| extend AccountName = tostring(split(UserUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(UserUpn, "@")[1])
| project
TimeGenerated,
UserUpn,
AccountName,
AccountUPNSuffix,
MFADisabledTime,
IPAddress,
AppDisplayName,
Location,
AutonomousSystemNumber,
CorrelationId
| sort by TimeGenerated desc