diff --git a/rules/integrations/o365/credential_access_microsoft_365_excessive_account_lockouts.toml b/rules/integrations/o365/credential_access_microsoft_365_excessive_account_lockouts.toml new file mode 100644 index 000000000..1aeb79dae --- /dev/null +++ b/rules/integrations/o365/credential_access_microsoft_365_excessive_account_lockouts.toml @@ -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/" +