Potential Microsoft 365 User Account Brute Force

Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2020/11/30"
  3integration = ["o365"]
  4maturity = "production"
  5updated_date = "2025/07/16"
  6
  7[rule]
  8author = ["Elastic", "Willem D'Haese", "Austin Songer"]
  9description = """
 10Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that
 11match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force
 12authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.
 13"""
 14false_positives = [
 15    """
 16    Automated processes that attempt to authenticate using expired credentials and unbounded retries may lead to false
 17    positives.
 18    """,
 19]
 20from = "now-60m"
 21interval = "10m"
 22language = "esql"
 23license = "Elastic License v2"
 24name = "Potential Microsoft 365 User Account Brute Force"
 25note = """## Triage and Analysis
 26
 27### Investigating Potential Microsoft 365 User Account Brute Force
 28
 29Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.
 30
 31### Possible investigation steps
 32
 33- Review `user_id_list`: Enumerates the user accounts targeted. Look for naming patterns or privilege levels (e.g., admins).
 34- Check `login_errors`: A consistent error such as `"InvalidUserNameOrPassword"` confirms a spray-style attack using one or a few passwords.
 35- Examine `ip_list` and `source_orgs`: Determine if the traffic originates from a known corporate VPN, datacenter, or suspicious ASN like hosting providers or anonymizers.
 36- Review `countries` and `unique_country_count`: Geographic anomalies (e.g., login attempts from unexpected regions) may indicate malicious automation.
 37- Validate `total_attempts` vs `duration_seconds`: A high frequency of login attempts over a short period may suggest automation rather than manual logins.
 38- Cross-reference with successful logins: Pivot to surrounding sign-in logs (`azure.signinlogs`) or risk detections (`identityprotection`) for any account that eventually succeeded.
 39- Check for multi-factor challenges or bypasses: Determine if any of the accounts were protected or if the attack bypassed MFA.
 40
 41### False positive analysis
 42
 43- IT administrators using automation tools (e.g., PowerShell) during account provisioning may trigger false positives if login attempts cluster.
 44- Penetration testing or red team simulations may resemble spray activity.
 45- Infrequent, low-volume login testing tools like ADFS testing scripts can exhibit similar patterns.
 46
 47### Response and remediation
 48
 49- Initiate an internal incident ticket and inform the affected identity/IT team.
 50- Temporarily disable impacted user accounts if compromise is suspected.
 51- Investigate whether any login attempts succeeded after the spray window.
 52- Block the offending IPs or ASN temporarily via firewall or conditional access policies.
 53- Rotate passwords for all targeted accounts and audit for password reuse.
 54- Enforce or verify MFA is enabled for all user accounts.
 55- Consider deploying account lockout or progressive delay mechanisms if not already enabled.
 56"""
 57references = [
 58    "https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray",
 59    "https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties",
 60    "https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/",
 61    "https://github.com/0xZDH/Omnispray",
 62    "https://github.com/0xZDH/o365spray",
 63]
 64risk_score = 47
 65rule_id = "26f68dba-ce29-497b-8e13-b4fde1db5a2d"
 66severity = "medium"
 67tags = [
 68    "Domain: Cloud",
 69    "Domain: SaaS",
 70    "Data Source: Microsoft 365",
 71    "Data Source: Microsoft 365 Audit Logs",
 72    "Use Case: Identity and Access Audit",
 73    "Use Case: Threat Detection",
 74    "Tactic: Credential Access",
 75    "Resources: Investigation Guide",
 76]
 77timestamp_override = "event.ingested"
 78type = "esql"
 79
 80query = '''
 81from logs-o365.audit-*
 82| mv_expand event.category
 83| eval
 84    Esql.time_window_date_trunc = date_trunc(5 minutes, @timestamp),
 85    Esql_priv.o365_audit_UserId_lower = to_lower(o365.audit.UserId),
 86    Esql.o365_audit_LogonError = o365.audit.LogonError,
 87    Esql.o365_audit_ExtendedProperties_RequestType_lower = to_lower(o365.audit.ExtendedProperties.RequestType)
 88| where
 89    event.dataset == "o365.audit" and
 90    event.category == "authentication" and
 91    event.provider in ("AzureActiveDirectory", "Exchange") and
 92    event.action in ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword") and
 93    Esql.o365_audit_ExtendedProperties_RequestType_lower rlike "(oauth.*||.*login.*)" and
 94    Esql.o365_audit_LogonError != "IdsLocked" and
 95    Esql.o365_audit_LogonError not in (
 96        "EntitlementGrantsNotFound",
 97        "UserStrongAuthEnrollmentRequired",
 98        "UserStrongAuthClientAuthNRequired",
 99        "InvalidReplyTo",
100        "SsoArtifactExpiredDueToConditionalAccess",
101        "PasswordResetRegistrationRequiredInterrupt",
102        "SsoUserAccountNotFoundInResourceTenant",
103        "UserStrongAuthExpired",
104        "CmsiInterrupt"
105    ) and
106    Esql_priv.o365_audit_UserId_lower != "not available" and
107    o365.audit.Target.Type in ("0", "2", "6", "10")
108| stats
109    Esql.o365_audit_UserId_lower_count_distinct = count_distinct(Esql_priv.o365_audit_UserId_lower),
110    Esql_priv.o365_audit_UserId_lower_values = values(Esql_priv.o365_audit_UserId_lower),
111    Esql.o365_audit_LogonError_values = values(Esql.o365_audit_LogonError),
112    Esql.o365_audit_LogonError_count_distinct = count_distinct(Esql.o365_audit_LogonError),
113    Esql.o365_audit_ExtendedProperties_RequestType_values = values(Esql.o365_audit_ExtendedProperties_RequestType_lower),
114    Esql.source_ip_values = values(source.ip),
115    Esql.source_ip_count_distinct = count_distinct(source.ip),
116    Esql.source_as_organization_name_values = values(source.`as`.organization.name),
117    Esql.source_geo_country_name_values = values(source.geo.country_name),
118    Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name),
119    Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name),
120    Esql.timestamp_first_seen = min(@timestamp),
121    Esql.timestamp_last_seen = max(@timestamp),
122    Esql.event_count = count(*)
123  by Esql.time_window_date_trunc
124| eval
125    Esql.event_duration_seconds = date_diff("seconds", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
126    Esql.brute_force_type = case(
127        Esql.o365_audit_UserId_lower_count_distinct >= 15 and Esql.o365_audit_LogonError_count_distinct == 1 and Esql.event_count >= 10 and Esql.event_duration_seconds <= 1800, "password_spraying",
128        Esql.o365_audit_UserId_lower_count_distinct >= 8 and Esql.event_count >= 15 and Esql.o365_audit_LogonError_count_distinct <= 3 and Esql.source_ip_count_distinct <= 5 and Esql.event_duration_seconds <= 600, "credential_stuffing",
129        Esql.o365_audit_UserId_lower_count_distinct == 1 and Esql.o365_audit_LogonError_count_distinct == 1 and Esql.event_count >= 20 and Esql.event_duration_seconds <= 300, "password_guessing",
130        "other"
131    )
132| keep
133    Esql.time_window_date_trunc,
134    Esql.o365_audit_UserId_lower_count_distinct,
135    Esql_priv.o365_audit_UserId_lower_values,
136    Esql.o365_audit_LogonError_values,
137    Esql.o365_audit_LogonError_count_distinct,
138    Esql.o365_audit_ExtendedProperties_RequestType_values,
139    Esql.source_ip_values,
140    Esql.source_ip_count_distinct,
141    Esql.source_as_organization_name_values,
142    Esql.source_geo_country_name_values,
143    Esql.source_geo_country_name_count_distinct,
144    Esql.source_as_organization_name_count_distinct,
145    Esql.timestamp_first_seen,
146    Esql.timestamp_last_seen,
147    Esql.event_duration_seconds,
148    Esql.event_count,
149    Esql.brute_force_type
150| where Esql.brute_force_type != "other"
151'''
152
153
154[[rule.threat]]
155framework = "MITRE ATT&CK"
156[[rule.threat.technique]]
157id = "T1110"
158name = "Brute Force"
159reference = "https://attack.mitre.org/techniques/T1110/"
160[[rule.threat.technique.subtechnique]]
161id = "T1110.001"
162name = "Password Guessing"
163reference = "https://attack.mitre.org/techniques/T1110/001/"
164
165[[rule.threat.technique.subtechnique]]
166id = "T1110.003"
167name = "Password Spraying"
168reference = "https://attack.mitre.org/techniques/T1110/003/"
169
170[[rule.threat.technique.subtechnique]]
171id = "T1110.004"
172name = "Credential Stuffing"
173reference = "https://attack.mitre.org/techniques/T1110/004/"
174
175
176
177[rule.threat.tactic]
178id = "TA0006"
179name = "Credential Access"
180reference = "https://attack.mitre.org/tactics/TA0006/"

Triage and Analysis

Investigating Potential Microsoft 365 User Account Brute Force

Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.

Possible investigation steps

  • Review user_id_list: Enumerates the user accounts targeted. Look for naming patterns or privilege levels (e.g., admins).
  • Check login_errors: A consistent error such as "InvalidUserNameOrPassword" confirms a spray-style attack using one or a few passwords.
  • Examine ip_list and source_orgs: Determine if the traffic originates from a known corporate VPN, datacenter, or suspicious ASN like hosting providers or anonymizers.
  • Review countries and unique_country_count: Geographic anomalies (e.g., login attempts from unexpected regions) may indicate malicious automation.
  • Validate total_attempts vs duration_seconds: A high frequency of login attempts over a short period may suggest automation rather than manual logins.
  • Cross-reference with successful logins: Pivot to surrounding sign-in logs (azure.signinlogs) or risk detections (identityprotection) for any account that eventually succeeded.
  • Check for multi-factor challenges or bypasses: Determine if any of the accounts were protected or if the attack bypassed MFA.

False positive analysis

  • IT administrators using automation tools (e.g., PowerShell) during account provisioning may trigger false positives if login attempts cluster.
  • Penetration testing or red team simulations may resemble spray activity.
  • Infrequent, low-volume login testing tools like ADFS testing scripts can exhibit similar patterns.

Response and remediation

  • Initiate an internal incident ticket and inform the affected identity/IT team.
  • Temporarily disable impacted user accounts if compromise is suspected.
  • Investigate whether any login attempts succeeded after the spray window.
  • Block the offending IPs or ASN temporarily via firewall or conditional access policies.
  • Rotate passwords for all targeted accounts and audit for password reuse.
  • Enforce or verify MFA is enabled for all user accounts.
  • Consider deploying account lockout or progressive delay mechanisms if not already enabled.

References

Related rules

to-top