A SOC team should proactively hunt for failed service logon attempts by user accounts with available AuditData as this may indicate an adversary attempting to brute-force access or enumerate valid credentials. The combination of multiple failed logons and varying IP sources suggests potential reconnaissance or credential compromise, which could lead to further lateral movement or persistence in the Azure Sentinel environment.
KQL Query
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let lookback = totimespan((endtime-starttime)*7);
let failLimit = 10;
let ipLimit = 3;
let failedSignins = SigninLogs
| where TimeGenerated between(starttime..endtime)
| where ResultType != "0" and AppDisplayName != "Windows Sign In"
| extend UserPrincipalName = tolower(UserPrincipalName)
| extend CityState = strcat(tostring(LocationDetails.city),"|", tostring(LocationDetails.state))
| extend Result = strcat(ResultType,"-",ResultDescription)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), DistinctIPAddressCount = dcount(IPAddress), IPAddresses = makeset(IPAddress),
CityStates = makeset(CityState), DistinctResultCount = dcount(Result), Results = makeset(Result), AppDisplayNames = makeset(AppDisplayName),
FailedLogonCount = count() by Type, OperationName, Category, UserPrincipalName = tolower(UserPrincipalName), ClientAppUsed, Location, CorrelationId
| project Type, StartTimeUtc, EndTimeUtc, OperationName, Category, UserPrincipalName, AppDisplayNames, DistinctIPAddressCount, IPAddresses, DistinctResultCount,
Results, FailedLogonCount, Location, CityStates
| where FailedLogonCount >= failLimit or DistinctIPAddressCount >= ipLimit
| extend Activity = pack("IPAddresses", IPAddresses, "AppDisplayNames", AppDisplayNames, "Results", Results, "Location", Location, "CityStates", CityStates)
| project Type, StartTimeUtc, EndTimeUtc, OperationName, Category, UserPrincipalName, FailedLogonCount, DistinctIPAddressCount, DistinctResultCount, Activity
| extend AccountCustomEntity = UserPrincipalName;
let accountMods = AuditLogs | where TimeGenerated >= ago(lookback)
| where Category == "UserManagement" or Category == "GroupManagement"
| extend ModProps = TargetResources.[0].modifiedProperties
| extend InitiatedBy = case(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)), tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName),
isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).displayName)), tostring(parse_json(tostring(InitiatedBy.app)).displayName),
"")
| extend UserPrincipalName = tolower(tostring(TargetResources.[0].userPrincipalName))
| mvexpand ModProps
| extend PropertyName = tostring(ModProps.displayName), oldValue = tostring(ModProps.oldValue), newValue = tostring(ModProps.newValue)
| extend ModifiedProps = pack("PropertyName",PropertyName,"oldValue",oldValue,"newValue",newValue)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Activity = make_bag(ModifiedProps) by Type, InitiatedBy, UserPrincipalName, Category, OperationName, CorrelationId, Id
| extend AccountCustomEntity = UserPrincipalName;
// Gather only Audit data for UserPrincipalNames that we have Audit data for
let accountNameOnly = failedSignins | project UserPrincipalName;
let auditMods = accountNameOnly
| join kind= innerunique (
accountMods
) on UserPrincipalName;
let availableAudits = auditMods | project UserPrincipalName;
let signinsWithAudit = availableAudits
| join kind= innerunique (
failedSignins
) on UserPrincipalName;
// Union the Current Signin failures so we do not lose them with the Auditing data we do have
let activity = (union isfuzzy=true
signinsWithAudit, auditMods)
| order by StartTimeUtc, UserPrincipalName;
activity
| project StartTimeUtc, EndTimeUtc, DataType = Type, Category, OperationName, UserPrincipalName, InitiatedBy, Activity, FailedLogonCount, DistinctIPAddressCount, DistinctResultCount, CorrelationId, Id
| order by UserPrincipalName, StartTimeUtc
| extend timestamp = StartTimeUtc, AccountCustomEntity = UserPrincipalName
id: 22f33a4c-e60f-4817-bbfe-9e2ed33cb596
name: Failed service logon attempt by user account with available AuditData
description: |
'User account failed to logon in current period. Excludes Windows Sign in attempts and limits to only more than 10 failed logons or 3 different IPs used. Results may indicate a potential malicious use of an account that is rarely used.'
description_detailed: |
'User account failed to logon in current period (default last 1 day). Excludes Windows Sign in attempts due to noise and limits to only more than 10 failed logons or 3 different IPs used.
Additionally, Azure Audit Log data from the last several days(default 7 days) related to the given UserPrincipalName will be joined if available.
This can help to understand any events for this same user related to User or Group Management.
Results may indicate a potential malicious use of an account that is rarely used. It is possible this is an account that is new or newly enabled.
The associated Azure Audit data should help determine any recent changes to this account and may help you understand why the logons are failing.
Receiving no results indicates that there were no less than 10 failed logons or that the Auditlogs related to this UserPrincipalName in the default 7 days.'
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- AuditLogs
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let lookback = totimespan((endtime-starttime)*7);
let failLimit = 10;
let ipLimit = 3;
let failedSignins = SigninLogs
| where TimeGenerated between(starttime..endtime)
| where ResultType != "0" and AppDisplayName != "Windows Sign In"
| extend UserPrincipalName = tolower(UserPrincipalName)
| extend CityState = strcat(tostring(LocationDetails.city),"|", tostring(LocationDetails.state))
| extend Result = strcat(ResultType,"-",ResultDescription)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), DistinctIPAddressCount = dcount(IPAddress), IPAddresses = makeset(IPAddress),
CityStates = makeset(CityState), DistinctResultCount = dcount(Result), Results = makeset(Result), AppDisplayNames = makeset(AppDisplayName),
FailedLogonCount = count() by Type, OperationName, Category, UserPrincipalName = tolower(UserPrincipalName), ClientAppUsed, Location, CorrelationId
| project Type, StartTimeUtc, EndTimeUtc, OperationName, Category, UserPrincipalName, AppDisplayNames, DistinctIPAddressCount, IPAddresses, DistinctResultCount,
Results, FailedLogonCount, Location, CityStates
| where FailedLogonCount >= failLimit or DistinctIPAddressCount >= ipLimit
| extend Activity = pack("IPAddresses", IPAddresses, "AppDisplayNames", AppDisplayNames, "Results", Results, "Location", Location, "CityStates", CityStates)
| project Type, StartTimeUtc, EndTimeUtc, OperationName, Category, Use
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
SigninLogs | Ensure this data connector is enabled |
Scenario: A system administrator is performing a scheduled maintenance task using a service account that has multiple failed logon attempts due to incorrect password entry during a password reset process.
Filter/Exclusion: Exclude logons associated with service accounts or users with “Password Never Expires” set in Active Directory. Use username field to filter out known admin accounts.
Scenario: A legitimate scheduled job (e.g., Task Scheduler or SQL Server Agent Job) is configured to run with a service account and is failing due to incorrect credentials after a password change.
Filter/Exclusion: Exclude logons from known scheduled tasks or services (e.g., SQLAgent or TaskScheduler). Use source or process_name fields to identify and exclude these jobs.
Scenario: A user is attempting to access a service (e.g., Windows Update or Remote Desktop Services) using a valid account but is repeatedly entering incorrect credentials due to a temporary password change or lockout.
Filter/Exclusion: Exclude logons where the user account has been recently locked out or has a password change in progress. Use event_id or status fields to identify lockout events or password change events.
Scenario: A security tool (e.g., Microsoft Defender for Identity or CrowdStrike Falcon) is configured to run under a service account and is failing to authenticate due to misconfiguration or credential rotation.
Filter/Exclusion: Exclude logons from security tools or monitoring services. Use process_name or product_name fields to identify and exclude these tools.
Scenario: A user is using a third-party application (e.g., Microsoft Endpoint Manager or Intune) to manage device configurations and is encountering failed logon attempts due to incorrect credentials during a bulk configuration push.
Filter/Exclusion: Exclude logons associated with