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.
Rule type: esql
Rule indices:
Rule Severity: medium
Risk Score: 47
Runs every:
Searches indices from: now-9m
Maximum alerts per execution: ?
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: ?
Rule authors:
- Elastic
Rule license: Elastic License v2
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 ES|QL 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.
- 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.
- 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.
- 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
.
FROM logs-o365.audit-*
| MV_EXPAND event.category
| EVAL
time_window = DATE_TRUNC(5 minutes, @timestamp),
user_id = TO_LOWER(o365.audit.UserId),
ip = source.ip,
login_error = o365.audit.LogonError,
request_type = TO_LOWER(o365.audit.ExtendedProperties.RequestType),
asn_org = source.`as`.organization.name,
country = source.geo.country_name,
event_time = @timestamp
| WHERE event.dataset == "o365.audit"
AND event.category == "authentication"
AND event.provider IN ("AzureActiveDirectory", "Exchange")
AND event.action IN ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword")
AND request_type RLIKE "(oauth.*||.*login.*)"
AND login_error == "IdsLocked"
AND user_id != "not available"
AND o365.audit.Target.Type IN ("0", "2", "6", "10")
AND asn_org != "MICROSOFT-CORP-MSN-AS-BLOCK"
| STATS
unique_users = COUNT_DISTINCT(user_id),
user_id_list = VALUES(user_id),
ip_list = VALUES(ip),
unique_ips = COUNT_DISTINCT(ip),
source_orgs = VALUES(asn_org),
countries = VALUES(country),
unique_country_count = COUNT_DISTINCT(country),
unique_asn_orgs = COUNT_DISTINCT(asn_org),
request_types = VALUES(request_type),
first_seen = MIN(event_time),
last_seen = MAX(event_time),
total_lockout_responses = COUNT()
BY time_window
| EVAL
duration_seconds = DATE_DIFF("seconds", first_seen, last_seen)
| KEEP
time_window, unique_users, user_id_list, ip_list,
unique_ips, source_orgs, countries, unique_country_count,
unique_asn_orgs, request_types, first_seen, last_seen,
total_lockout_responses, duration_seconds
| WHERE
unique_users >= 10 AND
total_lockout_responses >= 10 AND
duration_seconds <= 300
Framework: MITRE ATT&CK
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/