[Rule Tuning] Potential Microsoft 365 User Account Brute Force (#4716)

* tuning M365 brute force rule

* updated logic

* updated references

* adds minstack for values

* removed ignoring MSFT ASN

* Update rules/integrations/o365/credential_access_microsoft_365_potential_user_account_brute_force.toml

Co-authored-by: shashank-elastic <91139415+shashank-elastic@users.noreply.github.com>

---------

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:08:38 -04:00
committed by GitHub
parent 0d366d6a15
commit 3e0a9ec47b
2 changed files with 173 additions and 139 deletions
@@ -1,139 +0,0 @@
[metadata]
creation_date = "2020/11/30"
integration = ["o365"]
maturity = "production"
updated_date = "2025/03/20"
[rule]
author = ["Elastic", "Willem D'Haese", "Austin Songer"]
description = """
Identifies potential brute-force attempts against Microsoft 365 user accounts by detecting a high number of failed login
attempts or login sources within a 30-minute window. Attackers may attempt to brute force user accounts to gain
unauthorized access to Microsoft 365 services.
"""
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 = "Attempts to Brute Force a Microsoft 365 User Account"
note = """## Triage and analysis
> **Disclaimer**:
> This investigation guide was created using generative AI technology and has been reviewed to improve its accuracy and relevance. While every effort has been made to ensure its quality, we recommend validating the content and adapting it to suit your specific environment and operational needs.
### Investigating Attempts to Brute Force a Microsoft 365 User Account
Microsoft 365 is a cloud-based service that provides productivity tools and services. Adversaries may attempt to gain unauthorized access by brute-forcing user accounts, exploiting weak passwords. The detection rule identifies such attempts by analyzing audit logs for numerous failed logins or diverse login sources within a short timeframe, indicating potential brute-force activity.
### Possible investigation steps
- Review the audit logs for the specific user identified by o365.audit.UserId to gather additional context on the failed login attempts, including timestamps and source IP addresses.
- Analyze the source.ip field to identify any unusual or suspicious IP addresses, such as those originating from unexpected geographic locations or known malicious sources.
- Check the o365.audit.LogonError field for any patterns or specific errors that might provide insight into the nature of the failed login attempts.
- Investigate the o365.audit.ExtendedProperties.RequestType to determine if the login attempts were consistent with typical user behavior or if they suggest automated or scripted activity.
- Correlate the findings with other security events or alerts in the environment to assess if the brute-force attempts are part of a larger attack campaign or isolated incidents.
- Contact the affected user to verify if they experienced any issues accessing their account and to ensure they are aware of the potential security threat.
### False positive analysis
- High volume of legitimate login attempts from a single user can trigger false positives, especially during password resets or account recovery. To mitigate, consider excluding specific users or IP ranges known for such activities.
- Automated scripts or applications performing frequent logins for legitimate purposes may be misidentified as brute-force attempts. Identify and whitelist these scripts or applications by their user IDs or IP addresses.
- Users traveling or using VPNs may log in from multiple locations in a short period, leading to false positives. Implement geolocation-based exceptions for known travel patterns or VPN IP addresses.
- Shared accounts accessed by multiple users from different locations can appear as multiple login sources. Limit monitoring on shared accounts or establish a baseline of expected behavior to differentiate between normal and suspicious activity.
- Temporary spikes in login attempts due to system maintenance or updates can be mistaken for brute-force attacks. Schedule monitoring exclusions during planned maintenance windows to avoid false alerts.
### Response and remediation
- Immediately isolate the affected user account by disabling it to prevent further unauthorized access attempts.
- Notify the user and relevant IT security personnel about the potential compromise and provide guidance on secure password creation.
- Conduct a password reset for the affected user account, ensuring the new password adheres to strong password policies.
- Review and analyze the source IP addresses involved in the failed login attempts to identify any patterns or known malicious sources.
- Implement conditional access policies to restrict login attempts from suspicious or untrusted locations and devices.
- Monitor the affected account and related accounts for any unusual activity or further unauthorized access attempts.
- Escalate the incident to the security operations center (SOC) or incident response team for further investigation and to determine if additional accounts or systems are affected."""
references = [
"https://blueteamblog.com/7-ways-to-monitor-your-office-365-logs-using-siem",
"https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties",
]
risk_score = 47
rule_id = "26f68dba-ce29-497b-8e13-b4fde1db5a2d"
severity = "medium"
tags = [
"Domain: Cloud",
"Domain: SaaS",
"Data Source: Microsoft 365",
"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-*
// truncate the timestamp to a 30-minute window
| eval target_time_window = DATE_TRUNC(30 minutes, @timestamp)
| mv_expand event.category
| where event.dataset == "o365.audit"
and event.category == "authentication"
// filter only on Entra ID or Exchange audit logs in O365 integration
and event.provider in ("AzureActiveDirectory", "Exchange")
// filter only for UserLoginFailed or partial failures
and event.action in ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword")
// ignore specific logon errors
and not o365.audit.LogonError in (
"EntitlementGrantsNotFound",
"UserStrongAuthEnrollmentRequired",
"UserStrongAuthClientAuthNRequired",
"InvalidReplyTo",
"SsoArtifactExpiredDueToConditionalAccess",
"PasswordResetRegistrationRequiredInterrupt",
"SsoUserAccountNotFoundInResourceTenant",
"UserStrongAuthExpired",
"CmsiInterrupt"
)
// ignore unavailable
and o365.audit.UserId != "Not Available"
// filters out non user or application logins based on target
and o365.audit.Target.Type in ("0", "2", "3", "5", "6", "10")
// filters only for logins from user or application, ignoring oauth:token
and to_lower(o365.audit.ExtendedProperties.RequestType) rlike "(.*)login(.*)"
// keep only relevant fields
| keep event.provider, event.dataset, event.category, o365.audit.UserId, event.action, source.ip, o365.audit.LogonError, o365.audit.ExtendedProperties.RequestType, o365.audit.Target.Type, target_time_window
// count the number of login sources and failed login attempts
| stats
login_source_count = count(source.ip),
failed_login_count = count(*) by target_time_window, o365.audit.UserId
// filter for users with more than 20 login sources or failed login attempts
| where (login_source_count >= 20 or failed_login_count >= 20)
'''
[[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/"
@@ -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/"