Skip to content

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
[metadata]
creation_date = "2020/11/30"
integration = ["o365"]
maturity = "production"
updated_date = "2025/05/09"
min_stack_version = "8.17.0"
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."

[rule]
author = ["Elastic", "Willem D'Haese", "Austin Songer"]
description = """
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.
"""
false_positives = [
"""
Automated processes that attempt to authenticate using expired credentials and unbounded retries may lead to false
positives.
""",
]
from = "now-9m"
language = "esql"
license = "Elastic License v2"
name = "Potential Microsoft 365 User Account Brute Force"
note = """## 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.

### 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 Recommendations

- 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 = [
"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 = "26f68dba-ce29-497b-8e13-b4fde1db5a2d"
severity = "medium"
tags = [
"Domain: Cloud",
"Domain: SaaS",
"Data Source: Microsoft 365",
"Data Source: Microsoft 365 Audit Logs",
"Use Case: Identity and Access Audit",
"Use Case: Threat Detection",
"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 login_error NOT IN (
"EntitlementGrantsNotFound", "UserStrongAuthEnrollmentRequired", "UserStrongAuthClientAuthNRequired",
"InvalidReplyTo", "SsoArtifactExpiredDueToConditionalAccess", "PasswordResetRegistrationRequiredInterrupt",
"SsoUserAccountNotFoundInResourceTenant", "UserStrongAuthExpired", "CmsiInterrupt"
)
AND user_id != "not available"
AND o365.audit.Target.Type IN ("0", "2", "6", "10")

| STATS
unique_users = COUNT_DISTINCT(user_id),
user_id_list = VALUES(user_id),
login_errors = VALUES(login_error),
unique_login_errors = COUNT_DISTINCT(login_error),
request_types = VALUES(request_type),
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),
first_seen = MIN(event_time),
last_seen = MAX(event_time),
total_attempts = COUNT()
BY time_window

| EVAL
duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
bf_type = CASE(
unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 10 AND duration_seconds <= 1800, "password_spraying",
unique_users >= 8 AND total_attempts >= 15 AND unique_login_errors <= 3 AND unique_ips <= 5 AND duration_seconds <= 600, "credential_stuffing",
unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 20 AND duration_seconds <= 300, "password_guessing",
"other"
)

| KEEP
time_window, unique_users, user_id_list, login_errors, unique_login_errors,
request_types, ip_list, unique_ips, source_orgs, countries,
unique_country_count, unique_asn_orgs, first_seen, last_seen,
duration_seconds, total_attempts, bf_type

| WHERE
bf_type != "other"
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[[rule.threat.technique.subtechnique]]
id = "T1110.001"
name = "Password Guessing"
reference = "https://attack.mitre.org/techniques/T1110/001/"

[[rule.threat.technique.subtechnique]]
id = "T1110.003"
name = "Password Spraying"
reference = "https://attack.mitre.org/techniques/T1110/003/"

[[rule.threat.technique.subtechnique]]
id = "T1110.004"
name = "Credential Stuffing"
reference = "https://attack.mitre.org/techniques/T1110/004/"



[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"

Loading