Query Details

Identity Potential MFA Spam

Query

//Detect when a user denies MFA several times within a single sign in attempt and then completes MFA.
//This could be a sign of someone trying to spam your users with MFA prompts until they accept.

//Data connector required for this query - Azure Active Directory - Signin Logs

//Select your threshold of how many times a user denies MFA before accepting
let threshold=2;
SigninLogs
| project
    TimeGenerated,
    AuthenticationRequirement,
    AuthenticationDetails,
    UserPrincipalName,
    CorrelationId
//Include only authentications that require MFA
| where AuthenticationRequirement == "multiFactorAuthentication"
//Extend authentication result description
| mv-expand todynamic(AuthenticationDetails)
| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
//Find results that include both denined and completed MFA
| where AuthResult in ("MFA completed in Azure AD", "MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification","MFA successfully completed")
//Create a list of completed and denied MFA challenges per correlation id
| summarize ['Result Types']=make_list(AuthResult) by CorrelationId, UserPrincipalName
//Ensure the list includes both completed and denied MFA challenges
| where ['Result Types'] has_any ("MFA completed in Azure AD","MFA successfully completed") and ['Result Types'] has_any ("MFA denied; user declined the authentication", "MFA denied; user did not respond to mobile app notification")
| mv-expand ['Result Types'] to typeof(string)
//Expand and count all the denied challenges and then return CorrelationId's where the MFA denied count is greater or equal to your threshold
| where ['Result Types'] has_any ("MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification")
| summarize ['Denied MFA Count']=count()by ['Result Types'], CorrelationId, UserPrincipalName
| where ['Denied MFA Count'] >= threshold

//Alternate query, instead of grouping signins by CorrelationId we group them by UserPrincipalName and 10 minute blocks of time.
//In case the bad actor is starting a whole new sign in each time and generating a new CorrelationId for each attempt.
//Select your threshold of how many times a user denies MFA before accepting
let threshold=2;
SigninLogs
| project
    TimeGenerated,
    AuthenticationRequirement,
    AuthenticationDetails,
    UserPrincipalName,
    CorrelationId
//Include only authentications that require MFA
| where AuthenticationRequirement == "multiFactorAuthentication"
//Extend authentication result description
| mv-expand todynamic(AuthenticationDetails)
| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
//Find results that include both denined and completed MFA
| where AuthResult in ("MFA completed in Azure AD", "MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification","MFA successfully completed")
//Create a list of completed and denied MFA challenges per user principal name over 10 minute periods
| summarize ['Result Types']=make_list(AuthResult) by UserPrincipalName, bin(TimeGenerated, 10m)
//Ensure the list includes both completed and denied MFA challenges
| where ['Result Types'] has_any ("MFA completed in Azure AD","MFA successfully completed") and ['Result Types'] has_any ("MFA denied; user declined the authentication", "MFA denied; user did not respond to mobile app notification")
| mv-expand ['Result Types'] to typeof(string)
//Expand and count all the denied challenges and then return UserPrincipalNames where the MFA denied count is greater or equal to your threshold
| where ['Result Types'] has_any ("MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification")
| summarize ['Denied MFA Count']=count()by ['Result Types'], UserPrincipalName
| where ['Denied MFA Count'] >= threshold

//Simple query to count users being spammed with denies or not responding in one hour time windows
SigninLogs
| project
    TimeGenerated,
    AuthenticationRequirement,
    AuthenticationDetails,
    UserPrincipalName,
    CorrelationId
| where AuthenticationRequirement == "multiFactorAuthentication"
//Extend authentication result description
| mv-expand todynamic(AuthenticationDetails)
| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
| where AuthResult in ("MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification")
| summarize ['Result Types']=make_list(AuthResult), ['Result Count']=count() by UserPrincipalName, bin(TimeGenerated, 60m)
//Find hits with greater than 3 failures in an hour
| where ['Result Count'] > 3

Explanation

The query is designed to detect when a user denies multi-factor authentication (MFA) multiple times within a single sign-in attempt and then completes MFA. This could indicate that someone is trying to spam users with MFA prompts until they accept.

The query uses Azure Active Directory - Signin Logs as the data connector. It selects a threshold for the number of times a user can deny MFA before accepting.

The query includes only authentications that require MFA and extends the authentication result description. It then finds results that include both denied and completed MFA.

The query creates a list of completed and denied MFA challenges per correlation ID or user principal name and checks if the list includes both completed and denied MFA challenges. It expands and counts all the denied challenges and returns correlation IDs or user principal names where the MFA denied count is greater than or equal to the threshold.

There is an alternate query that groups sign-ins by user principal name and 10-minute blocks of time instead of correlation ID. This is useful if the bad actor is starting a new sign-in each time and generating a new correlation ID for each attempt.

There is also a simple query that counts users being spammed with denies or not responding in one-hour time windows. It summarizes the result types and counts for each user principal name and time window and finds hits with more than three failures in an hour.

Details

Matt Zorich profile picture

Matt Zorich

Released: May 25, 2023

Tables

SigninLogs

Keywords

Keywords:SigninLogs,TimeGenerated,AuthenticationRequirement,AuthenticationDetails,UserPrincipalName,CorrelationId,threshold,mv-expand,todynamic,AuthResult,parse_json,make_list,has_any,count,bin,TimeGenerated,UserPrincipalName,['ResultTypes'],['DeniedMFACount'],['ResultCount']

Operators

letSigninLogsprojectTimeGeneratedAuthenticationRequirementAuthenticationDetailsUserPrincipalNameCorrelationIdwhere=="multiFactorAuthentication"mv-expandtodynamicAuthResulttostringparse_jsonin("MFA completed in Azure AD""MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification","MFA successfully completed")summarize['Result Types']make_listbyCorrelationIdUserPrincipalNamehas_any("MFA completed in Azure AD","MFA successfully completed")has_any("MFA denied; user declined the authentication""MFA denied; user did not respond to mobile app notification")totypeof(string)count()>=thresholdbin10m("MFA denied; user declined the authentication","MFA denied; user did not respond to mobile app notification")UserPrincipalName('Result Types')('Denied MFA Count')('Result Count')>

Actions