Adversaries may block legitimate user accounts to mask their own persistent access or to evade detection by disrupting normal account activity. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential account compromise or adversarial manipulation of account states.
KQL Query
let starttime = totimespan('{{StartTimeISO}}');
let endtime = totimespan('{{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 aadFunc = (tableName:string){
table(tableName)
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where not(Identity matches regex isGUID)
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let blocked_users =
union isfuzzy=true aadSignin, aadNonInt
// Blocked or locked account due to failed attempts for various reasons.
| where ResultType != "0"
| where ResultDescription has_any ("blocked", "locked") or ResultType in (50053, 50131, 53003, 500121)
| summarize FirstBlockedAttempt = min(TimeGenerated), LastBlockedAttempt = max(TimeGenerated) by UserPrincipalName, ResultDescription, ResultType;
blocked_users
| join kind= inner (
union isfuzzy=true aadSignin, aadNonInt
| where ResultType == 0
| summarize FirstSuccessfulSignin = min(TimeGenerated), LastSuccessfulSignin = max(TimeGenerated), make_set(IPAddress), make_set(ClientAppUsed), make_set(UserAgent), make_set(AppDisplayName) by UserPrincipalName, UserDisplayName
) on UserPrincipalName
| where LastSuccessfulSignin > LastBlockedAttempt //Checking if successul login is after lastblockedattempts
| extend timestamp = LastSuccessfulSignin, AccountCustomEntity = UserPrincipalName
id: dbc82bc1-c7df-44e3-838a-5846a313cf35
name: User Accounts - Blocked Accounts
description: |
'An account could be blocked/locked out due to multiple reasons. This hunting query summarize blocked/lockout accounts and checks if most recent signin events for them is after last blocked accounts
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-successful-unusual-sign-ins'
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
tactics:
- InitialAccess
relevantTechniques:
- T1078
tags:
- AADSecOpsGuide
query: |
let starttime = totimespan('{{StartTimeISO}}');
let endtime = totimespan('{{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 aadFunc = (tableName:string){
table(tableName)
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where not(Identity matches regex isGUID)
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let blocked_users =
union isfuzzy=true aadSignin, aadNonInt
// Blocked or locked account due to failed attempts for various reasons.
| where ResultType != "0"
| where ResultDescription has_any ("blocked", "locked") or ResultType in (50053, 50131, 53003, 500121)
| summarize FirstBlockedAttempt = min(TimeGenerated), LastBlockedAttempt = max(TimeGenerated) by UserPrincipalName, ResultDescription, ResultType;
blocked_users
| join kind= inner (
union isfuzzy=true aadSignin, aadNonInt
| where ResultType == 0
| summarize FirstSuccessfulSignin = min(TimeGenerated), LastSuccessfulSignin = max(TimeGenerated), make_set(IPAddress), make_set(ClientAppUsed), make_set(UserAgent), make_set(AppDisplayName) by UserPrincipalName, UserDisplayName
) on UserPrincipalName
| where LastSuccessfulSignin > LastBlockedAttempt //Checking if successul login is after lastblockedattempts
| extend timestamp = LastSuccessfulSignin, AccountCustomEntity = UserPrincipalName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
version: 1.0.0
| Sentinel Table | Notes |
|---|---|
AADNonInteractiveUserSignInLogs | Ensure this data connector is enabled |
SigninLogs | Ensure this data connector is enabled |
Scenario: Scheduled Job Temporarily Blocks Account for Maintenance
Description: A system maintenance job (e.g., using PowerShell or Ansible) temporarily blocks an account to perform configuration changes, then unblocks it shortly after.
Filter/Exclusion: Exclude accounts that are part of a known maintenance schedule using account_name in a custom field or a scheduled_job_id field.
Scenario: Admin Locks Account for Investigation
Description: An admin manually locks an account (via Local Security Policy, Group Policy, or Active Directory Users and Computers) to investigate suspicious activity.
Filter/Exclusion: Exclude accounts where the last sign-in is within the last 24 hours and the account is marked as “locked by admin” in a custom field or via a lockout_reason field.
Scenario: Automated Script Blocks Account During Patching
Description: A patching script (e.g., using SCCM, Ansible, or Chef) temporarily blocks an account to prevent access during system updates.
Filter/Exclusion: Exclude accounts associated with patching scripts by checking the script_name or patching_job_id field.
Scenario: User Changes Password and Gets Locked Out
Description: A user changes their password using net user or the Change Password tool, which may trigger a lockout due to policy enforcement.
Filter/Exclusion: Exclude accounts where the last sign-in is within the last 10 minutes and the account was recently password-changed using a known password change tool.
Scenario: False Positive from Legacy Authentication Tool
Description: A legacy authentication tool (e.g., RADIUS, LDAP, or Kerberos) incorrectly reports an account as blocked due to a misconfiguration or timeout.
*