[New Rule] Multiple Microsoft 365 User Account Lockouts in Short Time Window (#4717)

* new rule 'Multiple Microsoft 365 User Account Lockouts in Short Time Window'

* adjusted logic

---------

Co-authored-by: shashank-elastic <91139415+shashank-elastic@users.noreply.github.com>
Co-authored-by: Colson Wilhoit <48036388+DefSecSentinel@users.noreply.github.com>
This commit is contained in:
Terrance DeJesus
2025-05-19 14:44:46 -04:00
committed by GitHub
parent 3e0a9ec47b
commit fcd70b284b
@@ -0,0 +1,143 @@
[metadata]
creation_date = "2025/05/10"
integration = ["o365"]
maturity = "production"
min_stack_version = "8.17.0"
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."
updated_date = "2025/05/10"
[rule]
author = ["Elastic"]
description = """
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.
"""
from = "now-9m"
language = "esql"
license = "Elastic License v2"
name = "Multiple Microsoft 365 User Account Lockouts in Short Time Window"
note = """## Triage 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 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.
### Investigation Steps
- Review the `user_id_list`: Are specific naming patterns targeted (e.g., admin, helpdesk)?
- Examine `ip_list` and `source_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` and `last_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`.
"""
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",
]
risk_score = 47
rule_id = "de67f85e-2d43-11f0-b8c9-f661ea17fbcc"
severity = "medium"
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",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
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
'''
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"