Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
creation_date = "2024/09/06"
integration = ["azure"]
maturity = "production"
updated_date = "2025/04/18"

updated_date = "2025/05/20"
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"]
description = """
Identifies potential brute-force attempts against Microsoft 365 user accounts by detecting a high number of failed
interactive or non-interactive login attempts within a 30-minute window. Attackers may attempt to brute force user
accounts to gain unauthorized access to Microsoft 365 services via different services such as Exchange, SharePoint, or
Teams.
Identifies potential brute-force attacks targeting Microsoft 365 user accounts by analyzing failed sign-in patterns in
Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive
authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or
password guessing. Adversaries may use these techniques to gain unauthorized access to Microsoft 365 services such as
Exchange Online, SharePoint, or Teams.
"""
false_positives = [
"""
Expand All @@ -23,46 +25,50 @@ from = "now-60m"
interval = "10m"
language = "esql"
license = "Elastic License v2"
name = "Azure Entra Sign-in Brute Force against Microsoft 365 Accounts"
name = "Potential Microsoft 365 Brute Force via Entra ID Sign-Ins"
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 Azure Entra Sign-in Brute Force against Microsoft 365 Accounts
### Investigating Potential Microsoft 365 Brute Force via Entra ID Sign-Ins

Azure Entra ID, integral to Microsoft 365, manages user identities and access. Adversaries exploit this by attempting numerous login attempts to breach accounts, targeting services like Exchange and Teams. The detection rule identifies such threats by analyzing failed login patterns within a 30-minute window, flagging unusual activity from multiple sources or excessive failed attempts, thus highlighting potential brute-force attacks.
Identifies brute-force authentication activity against Microsoft 365 services using Entra ID sign-in logs. This detection groups and classifies failed sign-in attempts based on behavior indicative of password spraying, credential stuffing, or password guessing. The classification (`bf_type`) is included for immediate triage.

### Possible investigation steps

- Review the `azure.signinlogs.properties.user_principal_name` to identify the specific user account targeted by the brute-force attempts.
- Examine the `source.ip` field to determine the origin of the failed login attempts and assess if multiple IP addresses are involved, indicating a distributed attack.
- Check the `azure.signinlogs.properties.resource_display_name` to understand which Microsoft 365 services (e.g., Exchange, SharePoint, Teams) were targeted during the login attempts.
- Analyze the `target_time_window` to confirm the timeframe of the attack and correlate it with other security events or alerts that may have occurred simultaneously.
- Investigate the `azure.signinlogs.properties.status.error_code` for specific error codes that might provide additional context on the nature of the failed login attempts.
- Assess the user's recent activity and any changes in behavior or access patterns that could indicate a compromised account or insider threat.
- Review `bf_type`: Classifies the brute-force behavior (`password_spraying`, `credential_stuffing`, `password_guessing`).
- Examine `user_id_list`: Review the identities targeted. Are they admins, service accounts, or external identities?
- Review `login_errors`: Multiple identical errors (e.g., `"Invalid grant..."`) suggest automated abuse or tooling.
- Check `ip_list` and `source_orgs`: Determine if requests came from known VPNs, hosting providers, or anonymized infrastructure.
- Validate `unique_ips` and `countries`: Multiple countries or IPs in a short window may indicate credential stuffing or distributed spray attempts.
- Compare `total_attempts` vs `duration_seconds`: High volume over a short duration supports non-human interaction.
- Inspect `user_agent.original` via `device_detail_browser`: Clients like `Python Requests` or `curl` are highly suspicious.
- Investigate `client_app_display_name` and `incoming_token_type`: Identify non-browser-based logins, token abuse or commonly mimicked clients like VSCode.
- Review `target_resource_display_name`: Confirm the service being targeted (e.g., SharePoint, Exchange). This may be what authorization is being attempted against.
- Pivot using `session_id` and `device_detail_device_id`: Determine if a single device is spraying multiple accounts.
- Check `conditional_access_status`: If "notApplied", determine whether conditional access is properly scoped.
- Correlate `user_principal_name` with successful sign-ins: Investigate surrounding logs for lateral movement or privilege abuse.

### False positive analysis

- High volume of legitimate login attempts from a single user can trigger false positives, especially during password resets or account recovery processes. To mitigate this, consider excluding known IP addresses associated with IT support or helpdesk operations.
- Automated scripts or applications that frequently access Microsoft 365 services using non-interactive logins may be misidentified as brute force attempts. Identify and whitelist these applications by their user principal names or IP addresses.
- Users traveling or working remotely may log in from multiple locations in a short period, leading to false positives. Implement geolocation-based exclusions for known travel patterns or use conditional access policies to manage these scenarios.
- Bulk operations performed by administrators, such as batch account updates or migrations, can result in numerous failed logins. Exclude these activities by recognizing the specific user principal names or IP addresses involved in such operations.
- Frequent logins from shared IP addresses, such as those from corporate VPNs or proxy servers, might be flagged. Consider excluding these IP ranges if they are known and trusted within the organization.
- Developer automation (e.g., CI/CD logins) or mobile sync errors may create noisy but benign login failures.
- Red team exercises or pentesting can resemble brute-force patterns.
- Legacy protocols or misconfigured service principals may trigger repeated login failures from the same IP or session.

### Response and remediation

- Immediately isolate the affected user accounts by disabling them to prevent further unauthorized access.
- Conduct a password reset for the compromised accounts, ensuring the new passwords are strong and unique.
- Review and block the IP addresses associated with the failed login attempts to prevent further access attempts from these sources.
- Enable multi-factor authentication (MFA) for the affected accounts and any other accounts that do not have it enabled to add an additional layer of security.
- Monitor the affected accounts and related services for any unusual activity or signs of compromise post-remediation.
- Escalate the incident to the security operations team for further investigation and to determine if there are broader implications or related threats.
- Update and enhance detection rules and monitoring to identify similar brute-force attempts in the future, ensuring quick response to any new threats.

This rule relies on Azure Entra ID sign-in logs, but filters for Microsoft 365 resources."""
- Notify identity or security operations teams to investigate further.
- Lock or reset affected user accounts if compromise is suspected.
- Block the source IP(s) or ASN temporarily using conditional access or firewall rules.
- Review tenant-wide MFA and conditional access enforcement.
- Audit targeted accounts for password reuse across systems or tenants.
- Enable lockout or throttling policies for repeated failed login attempts.
"""
references = [
"https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying",
"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://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes",
"https://github.com/0xZDH/Omnispray",
"https://github.com/0xZDH/o365spray",
]
risk_score = 47
Expand All @@ -83,31 +89,102 @@ timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-azure.signinlogs*
// truncate the timestamp to a 30-minute window
| eval target_time_window = DATE_TRUNC(30 minutes, @timestamp)
| WHERE
event.dataset == "azure.signinlogs"
and event.category == "authentication"
and to_lower(azure.signinlogs.properties.resource_display_name) rlike "(.*)365(.*)"
and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs")
and event.outcome != "success"
and not (azure.signinlogs.category == "NonInteractiveUserSignInLogs"
and azure.signinlogs.properties.status.error_code in (70043, 70044, 50057)
and azure.signinlogs.properties.incoming_token_type in ("primaryRefreshToken", "refreshToken"))
// for tuning review azure.signinlogs.properties.status.error_code
// https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes

// keep only relevant fields
| keep target_time_window, event.dataset, event.category, azure.signinlogs.properties.resource_display_name, azure.signinlogs.category, event.outcome, azure.signinlogs.properties.user_principal_name, source.ip

// 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, azure.signinlogs.properties.user_principal_name

// filter for users with more than 20 login sources or failed login attempts
| where (login_source_count >= 20 or failed_login_count >= 20)
FROM logs-azure.signinlogs*

| EVAL
time_window = DATE_TRUNC(5 minutes, @timestamp),
user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name),
ip = source.ip,
login_error = azure.signinlogs.result_description,
error_code = azure.signinlogs.result_type,
request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type),
app_name = TO_LOWER(azure.signinlogs.properties.app_display_name),
asn_org = source.`as`.organization.name,
country = source.geo.country_name,
user_agent = user_agent.original,
event_time = @timestamp

| WHERE event.dataset == "azure.signinlogs"
AND event.category == "authentication"
AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
AND azure.signinlogs.properties.resource_display_name RLIKE "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
AND event.outcome == "failure"
AND NOT STARTS_WITH("Account is locked", login_error)
AND azure.signinlogs.result_type IN (
"50034", // UserAccountNotFound
"50126", // InvalidUserNameOrPassword
"50053", // IdsLocked or too many sign-in failures
"70000", // InvalidGrant
"70008", // Expired or revoked refresh token
"70043", // Bad token due to sign-in frequency
"50057", // UserDisabled
"50055", // Password expired
"50056", // Invalid or null password
"50064", // Credential validation failure
"50076", // MFA required but not passed
"50079", // MFA registration required
"50105" // EntitlementGrantsNotFound (no access to app)
)
AND user_id IS NOT NULL AND user_id != ""
AND user_agent != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"

| STATS
authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
client_app_id = VALUES(azure.signinlogs.properties.app_id),
client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
risk_state = VALUES(azure.signinlogs.properties.risk_state),
session_id = VALUES(azure.signinlogs.properties.session_id),
user_id = VALUES(azure.signinlogs.properties.user_id),
user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
result_description = VALUES(azure.signinlogs.result_description),
result_signature = VALUES(azure.signinlogs.result_signature),
result_type = VALUES(azure.signinlogs.result_type),

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),
app_names = VALUES(app_name),
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 >= 30 AND duration_seconds <= 300, "password_guessing",
"other"
)

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

authentication_requirement, client_app_id, client_app_display_name,
target_resource_id, target_resource_display_name, conditional_access_status,
device_detail_browser, device_detail_device_id, incoming_token_type,
risk_state, session_id, user_id, user_principal_name,
result_description, result_signature, result_type

| WHERE bf_type != "other"
'''


Expand All @@ -117,6 +194,21 @@ framework = "MITRE ATT&CK"
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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
creation_date = "2020/11/30"
integration = ["o365"]
maturity = "production"
updated_date = "2025/05/09"
updated_date = "2025/05/20"
min_stack_version = "8.17.0"
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."

Expand All @@ -19,7 +19,8 @@ false_positives = [
positives.
""",
]
from = "now-9m"
from = "now-60m"
interval = "10m"
language = "esql"
license = "Elastic License v2"
name = "Potential Microsoft 365 User Account Brute Force"
Expand All @@ -29,7 +30,7 @@ note = """## Triage and Analysis

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
### 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.
Expand All @@ -39,13 +40,13 @@ Identifies brute-force authentication activity targeting Microsoft 365 user acco
- 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
### 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
### Response and remediation

- Initiate an internal incident ticket and inform the affected identity/IT team.
- Temporarily disable impacted user accounts if compromise is suspected.
Expand Down
Loading