A blocked MFA user attempting to log in may indicate a compromised account or an adversary trying to regain access after being locked out. SOC teams should proactively hunt for this behavior to identify potential credential theft or lateral movement attempts in their Azure Sentinel environment.
KQL Query
let riskScoreCutoff = 20; //Adjust this based on volume of results
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let lookback = starttime - 7d;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let MFABlocked = SigninLogs
| where TimeGenerated between(starttime..endtime)
| where ResultType != "0"
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails), Status = strcat(ResultType, ": ", ResultDescription)
| where StatusDetails =~ "MFA denied; user is blocked"
| extend Unresolved = iff(Identity matches regex isGUID, true, false);
// Lookup up resolved identities from last 7 days
let identityLookup = SigninLogs
| where TimeGenerated between(lookback..starttime)
| where not(Identity matches regex isGUID)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName;
// Join resolved names to unresolved list from MFABlocked signins
let unresolvedNames = MFABlocked | where Unresolved == true | join kind= inner (
identityLookup
) on UserId
| extend UserDisplayName = lu_UserDisplayName, UserPrincipalName = lu_UserPrincipalName
| project-away lu_UserDisplayName, lu_UserPrincipalName;
// Join Signins that had resolved names with list of unresolved that now have a resolved name
let u_MFABlocked = MFABlocked | where Unresolved == false | union unresolvedNames;
u_MFABlocked
| extend OS = tostring(DeviceDetail.operatingSystem), Browser = tostring(DeviceDetail.browser)
| extend FullLocation = strcat(Location,'|', LocationDetails.state, '|', LocationDetails.city)
| summarize TimeGenerated = make_list(TimeGenerated), Status = make_list(Status), IPAddresses = make_list(IPAddress), IPAddressCount = dcount(IPAddress),
AttemptCount = count() by UserPrincipalName, UserId, UserDisplayName, AppDisplayName, Browser, OS, FullLocation , CorrelationId
| mvexpand TimeGenerated, IPAddresses, Status
| extend TimeGenerated = todatetime(tostring(TimeGenerated)), IPAddress = tostring(IPAddresses), Status = tostring(Status)
| project-away IPAddresses
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, UserId, UserDisplayName, Status, IPAddress, IPAddressCount, AppDisplayName, Browser, OS, FullLocation
| extend timestamp = StartTime, UserPrincipalName = tolower(UserPrincipalName), Account_0_Name = UserPrincipalName, IP_0_Address = IPAddress
| join kind=leftouter (
IdentityInfo
| summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
| project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
| summarize
Tags = make_set(Tags, 1000),
GroupMembership = make_set(GroupMembership, 1000),
AssignedRoles = make_set(AssignedRoles, 1000),
UserType = make_set(UserType, 1000),
UserAccountControl = make_set(UserType, 1000)
by AccountUPN
| extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName
| join kind=leftouter (
BehaviorAnalytics
| where ActivityType in ("FailedLogOn", "LogOn")
| where isnotempty(SourceIPAddress)
| project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress
| project-rename IPAddress = SourceIPAddress
| summarize
UsersInsights = make_set(UsersInsights, 1000),
DevicesInsights = make_set(DevicesInsights, 1000),
IPInvestigationPriority = sum(InvestigationPriority)
by IPAddress
) on IPAddress
| extend UEBARiskScore = IPInvestigationPriority
| where UEBARiskScore > riskScoreCutoff
| sort by UEBARiskScore desc
id: 75fd68a2-9ed4-4a1c-8bd7-18efe4c99081
name: Login attempt by Blocked MFA user
description: |
'An account could be blocked if there are too many failed authentication attempts in a row. This hunting query identifies if a MFA user account that is set to blocked tries to login to Microsoft Entra ID.
This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results.'
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: BehaviorAnalytics
dataTypes:
- BehaviorAnalytics
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let riskScoreCutoff = 20; //Adjust this based on volume of results
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let lookback = starttime - 7d;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let MFABlocked = SigninLogs
| where TimeGenerated between(starttime..endtime)
| where ResultType != "0"
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails), Status = strcat(ResultType, ": ", ResultDescription)
| where StatusDetails =~ "MFA denied; user is blocked"
| extend Unresolved = iff(Identity matches regex isGUID, true, false);
// Lookup up resolved identities from last 7 days
let identityLookup = SigninLogs
| where TimeGenerated between(lookback..starttime)
| where not(Identity matches regex isGUID)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName;
// Join resolved names to unresolved list from MFABlocked signins
let unresolvedNames = MFABlocked | where Unresolved == true | join kind= inner (
identityLookup
) on UserId
| extend UserDisplayName = lu_UserDisplayName, UserPrincipalName = lu_UserPrincipalName
| project-away lu_UserDisplayName, lu_UserPrincipalName;
// Join Signins that had resolved names with list of unresolved that now have a resolved name
let u_MFABlocked = MFABlocked | where Unresolved == false | union unresolvedNames;
u_MFABlocked
| extend OS = tostring(DeviceDetail.operatingSystem), Browser = tostring(DeviceDetail.browser)
| extend FullLocation = strcat(Location,'|', LocationDetails.state, '|', LocationDetails.city)
| summarize TimeGenerated = make_list(TimeGenerated), Status = make_list(Status), IPAddresses = make_list(IPAddress), IPAddressCount = dcount(IPAddress),
AttemptCount = count() by UserPrincipalName, UserId, UserDisplayName, AppDisplayName, Browser, OS, FullLocation , CorrelationId
| mvexpand TimeGenerated, IPAddresses, Status
| extend TimeGenerated = todatetime(tostring(TimeGenerated)), IPAddress = tostring(IPAddresses), Status = tostring(Status)
| project-away IPAddresses
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerate
| Sentinel Table | Notes |
|---|---|
BehaviorAnalytics | Ensure this data connector is enabled |
IdentityInfo | Ensure this data connector is enabled |
SigninLogs | Ensure this data connector is enabled |
Scenario: Scheduled MFA Re-enrollment Job
Description: A system administrator runs a scheduled job to re-enroll a previously blocked MFA user into the Microsoft Entra ID.
Filter/Exclusion: Exclude events where the login attempt is initiated by a service account or a known admin account used for MFA management, e.g., userPrincipalName LIKE '%admin-mfa-reenroll%' or ipAddress IN ('10.0.0.100', '10.0.0.101').
Scenario: Automated Password Reset via Azure AD Password Reset Tool
Description: An admin uses the Azure AD Password Reset tool to reset the password for a blocked MFA user, which may trigger a login attempt.
Filter/Exclusion: Exclude events where the login attempt is associated with the Azure AD Password Reset service, e.g., clientAppId = '00000002-0000-0000-c000-000000000000' or userAgent LIKE '%Azure AD Password Reset%'.
Scenario: User Login After MFA Block Resolution
Description: An admin manually unblocks a user in the Microsoft Entra ID admin center, and the user attempts to log in afterward.
Filter/Exclusion: Exclude login attempts where the user is known to be an admin or has a role that allows bypassing MFA, e.g., userPrincipalName LIKE '%admin%' or userRoles INCLUDE 'Global Administrator'.
Scenario: MFA Enrollment via PowerShell Script
Description: A script runs to enroll a user in MFA using the Microsoft Graph PowerShell module, which may result in a login attempt.
Filter/Exclusion: Exclude events where the login attempt is associated with