Multiple Microsoft 365 User Account Lockouts in Short Time Window
editMultiple Microsoft 365 User Account Lockouts in Short Time Window
editDetects a burst of Microsoft 365 user account lockouts within a short 5-minute window. A high number of IdsLocked login errors across multiple user accounts may indicate brute-force attempts for the same users resulting in lockouts.
Rule type: esql
Rule indices: None
Severity: medium
Risk score: 47
Runs every: 5m
Searches indices from: now-9m (Date Math format, see also Additional look-back time
)
Maximum alerts per execution: 100
References:
- https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray
- https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties
- https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/
- https://github.com/0xZDH/Omnispray
- https://github.com/0xZDH/o365spray
Tags:
- Domain: Cloud
- Domain: SaaS
- Data Source: Microsoft 365
- Data Source: Microsoft 365 Audit Logs
- Use Case: Threat Detection
- Use Case: Identity and Access Audit
- Tactic: Credential Access
- Resources: Investigation Guide
Version: 3
Rule authors:
- Elastic
Rule license: Elastic License v2
Investigation guide
editTriage and Analysis
Investigating Multiple Microsoft 365 User Account Lockouts in Short Time Window
Detects a burst of Microsoft 365 user account lockouts within a short 5-minute window. A high number of IdsLocked login errors across multiple user accounts may indicate brute-force attempts for the same users resulting in lockouts.
This rule uses ESQL aggregations and thus has dynamically generated fields. Correlation of the values in the alert document may need to be performed to the original sign-in and Graph events for further context.
Investigation Steps
-
Review the
user_id_list
: Are specific naming patterns targeted (e.g., admin, helpdesk)? -
Examine
ip_list
andsource_orgs
: Look for suspicious ISPs or hosting providers. -
Check
duration_seconds
: A very short window with a high lockout rate often indicates automation. - Confirm lockout policy thresholds with IAM or Entra ID admins. Did the policy trigger correctly?
-
Use the
first_seen
andlast_seen
values to pivot into related authentication or audit logs. - Correlate with any recent detection of password spraying or credential stuffing activity.
-
Review the
request_type
field to identify which authentication methods were used (e.g., OAuth, SAML, etc.). - Check for any successful logins from the same IP or ASN after the lockouts.
False Positive Analysis
- Automated systems with stale credentials may cause repeated failed logins.
- Legitimate bulk provisioning or scripted tests could unintentionally cause account lockouts.
- Red team exercises or penetration tests may resemble the same lockout pattern.
- Some organizations may have a high volume of lockouts due to user behavior or legacy systems.
Response Recommendations
- Notify affected users and confirm whether activity was expected or suspicious.
- Lock or reset credentials for impacted accounts.
- Block the source IP(s) or ASN temporarily using conditional access or firewall rules.
- Strengthen lockout and retry delay policies if necessary.
-
Review the originating application(s) involved via
request_types
.
Rule query
editfrom logs-o365.audit-* | mv_expand event.category | eval Esql.time_window_date_trunc = date_trunc(5 minutes, @timestamp) | where event.dataset == "o365.audit" and event.category == "authentication" and event.provider in ("AzureActiveDirectory", "Exchange") and event.action in ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword") and to_lower(o365.audit.ExtendedProperties.RequestType) rlike "(oauth.*||.*login.*)" and o365.audit.LogonError == "IdsLocked" and to_lower(o365.audit.UserId) != "not available" and o365.audit.Target.Type in ("0", "2", "6", "10") and source.`as`.organization.name != "MICROSOFT-CORP-MSN-as-BLOCK" | stats Esql_priv.o365_audit_UserId_count_distinct = count_distinct(to_lower(o365.audit.UserId)), Esql_priv.o365_audit_UserId_values = values(to_lower(o365.audit.UserId)), Esql.source_ip_values = values(source.ip), Esql.source_ip_count_distinct = count_distinct(source.ip), Esql.source_as_organization_name_values = values(source.`as`.organization.name), Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name), Esql.source_geo_country_name_values = values(source.geo.country_name), Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name), Esql.o365_audit_ExtendedProperties_RequestType_values = values(to_lower(o365.audit.ExtendedProperties.RequestType)), Esql.timestamp_first_seen = min(@timestamp), Esql.timestamp_last_seen = max(@timestamp), Esql.event_count = count(*) by Esql.time_window_date_trunc | eval Esql.event_duration_seconds = date_diff("seconds", Esql.timestamp_first_seen, Esql.timestamp_last_seen) | keep Esql.time_window_date_trunc, Esql_priv.o365_audit_UserId_count_distinct, Esql_priv.o365_audit_UserId_values, Esql.source_ip_values, Esql.source_ip_count_distinct, Esql.source_as_organization_name_values, Esql.source_as_organization_name_count_distinct, Esql.source_geo_country_name_values, Esql.source_geo_country_name_count_distinct, Esql.o365_audit_ExtendedProperties_RequestType_values, Esql.timestamp_first_seen, Esql.timestamp_last_seen, Esql.event_count, Esql.event_duration_seconds | where Esql_priv.o365_audit_UserId_count_distinct >= 10 and Esql.event_count >= 10 and Esql.event_duration_seconds <= 300
Framework: MITRE ATT&CKTM
-
Tactic:
- Name: Credential Access
- ID: TA0006
- Reference URL: https://attack.mitre.org/tactics/TA0006/
-
Technique:
- Name: Brute Force
- ID: T1110
- Reference URL: https://attack.mitre.org/techniques/T1110/