Skip to content

Commit 17d98cc

Browse files
[Rule Tuning] Tuning Azure Entra Sign-in Brute Force against Microsoft 365 Accounts (#4737)
* rule tuning 'Potential Microsoft 365 Brute Force via Entra ID Sign-Ins' * updated lookback windows, date truncation times * updated investigation guide
1 parent 4bd8469 commit 17d98cc

File tree

2 files changed

+155
-62
lines changed

2 files changed

+155
-62
lines changed

rules/integrations/azure/credential_access_entra_signin_brute_force_microsoft_365.toml

Lines changed: 149 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
creation_date = "2024/09/06"
33
integration = ["azure"]
44
maturity = "production"
5-
updated_date = "2025/04/18"
6-
5+
updated_date = "2025/05/20"
6+
min_stack_version = "8.17.0"
7+
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."
78

89
[rule]
910
author = ["Elastic"]
1011
description = """
11-
Identifies potential brute-force attempts against Microsoft 365 user accounts by detecting a high number of failed
12-
interactive or non-interactive login attempts within a 30-minute window. Attackers may attempt to brute force user
13-
accounts to gain unauthorized access to Microsoft 365 services via different services such as Exchange, SharePoint, or
14-
Teams.
12+
Identifies potential brute-force attacks targeting Microsoft 365 user accounts by analyzing failed sign-in patterns in
13+
Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive
14+
authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or
15+
password guessing. Adversaries may use these techniques to gain unauthorized access to Microsoft 365 services such as
16+
Exchange Online, SharePoint, or Teams.
1517
"""
1618
false_positives = [
1719
"""
@@ -23,46 +25,50 @@ from = "now-60m"
2325
interval = "10m"
2426
language = "esql"
2527
license = "Elastic License v2"
26-
name = "Azure Entra Sign-in Brute Force against Microsoft 365 Accounts"
28+
name = "Potential Microsoft 365 Brute Force via Entra ID Sign-Ins"
2729
note = """## Triage and analysis
2830
29-
> **Disclaimer**:
30-
> 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.
31-
32-
### Investigating Azure Entra Sign-in Brute Force against Microsoft 365 Accounts
31+
### Investigating Potential Microsoft 365 Brute Force via Entra ID Sign-Ins
3332
34-
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.
33+
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.
3534
3635
### Possible investigation steps
3736
38-
- Review the `azure.signinlogs.properties.user_principal_name` to identify the specific user account targeted by the brute-force attempts.
39-
- 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.
40-
- 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.
41-
- 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.
42-
- 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.
43-
- Assess the user's recent activity and any changes in behavior or access patterns that could indicate a compromised account or insider threat.
37+
- Review `bf_type`: Classifies the brute-force behavior (`password_spraying`, `credential_stuffing`, `password_guessing`).
38+
- Examine `user_id_list`: Review the identities targeted. Are they admins, service accounts, or external identities?
39+
- Review `login_errors`: Multiple identical errors (e.g., `"Invalid grant..."`) suggest automated abuse or tooling.
40+
- Check `ip_list` and `source_orgs`: Determine if requests came from known VPNs, hosting providers, or anonymized infrastructure.
41+
- Validate `unique_ips` and `countries`: Multiple countries or IPs in a short window may indicate credential stuffing or distributed spray attempts.
42+
- Compare `total_attempts` vs `duration_seconds`: High volume over a short duration supports non-human interaction.
43+
- Inspect `user_agent.original` via `device_detail_browser`: Clients like `Python Requests` or `curl` are highly suspicious.
44+
- Investigate `client_app_display_name` and `incoming_token_type`: Identify non-browser-based logins, token abuse or commonly mimicked clients like VSCode.
45+
- Review `target_resource_display_name`: Confirm the service being targeted (e.g., SharePoint, Exchange). This may be what authorization is being attempted against.
46+
- Pivot using `session_id` and `device_detail_device_id`: Determine if a single device is spraying multiple accounts.
47+
- Check `conditional_access_status`: If "notApplied", determine whether conditional access is properly scoped.
48+
- Correlate `user_principal_name` with successful sign-ins: Investigate surrounding logs for lateral movement or privilege abuse.
4449
4550
### False positive analysis
4651
47-
- 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.
48-
- 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.
49-
- 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.
50-
- 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.
51-
- 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.
52+
- Developer automation (e.g., CI/CD logins) or mobile sync errors may create noisy but benign login failures.
53+
- Red team exercises or pentesting can resemble brute-force patterns.
54+
- Legacy protocols or misconfigured service principals may trigger repeated login failures from the same IP or session.
5255
5356
### Response and remediation
5457
55-
- Immediately isolate the affected user accounts by disabling them to prevent further unauthorized access.
56-
- Conduct a password reset for the compromised accounts, ensuring the new passwords are strong and unique.
57-
- Review and block the IP addresses associated with the failed login attempts to prevent further access attempts from these sources.
58-
- 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.
59-
- Monitor the affected accounts and related services for any unusual activity or signs of compromise post-remediation.
60-
- Escalate the incident to the security operations team for further investigation and to determine if there are broader implications or related threats.
61-
- Update and enhance detection rules and monitoring to identify similar brute-force attempts in the future, ensuring quick response to any new threats.
62-
63-
This rule relies on Azure Entra ID sign-in logs, but filters for Microsoft 365 resources."""
58+
- Notify identity or security operations teams to investigate further.
59+
- Lock or reset affected user accounts if compromise is suspected.
60+
- Block the source IP(s) or ASN temporarily using conditional access or firewall rules.
61+
- Review tenant-wide MFA and conditional access enforcement.
62+
- Audit targeted accounts for password reuse across systems or tenants.
63+
- Enable lockout or throttling policies for repeated failed login attempts.
64+
"""
6465
references = [
6566
"https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying",
67+
"https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray",
68+
"https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties",
69+
"https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/",
70+
"https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes",
71+
"https://github.com/0xZDH/Omnispray",
6672
"https://github.com/0xZDH/o365spray",
6773
]
6874
risk_score = 47
@@ -83,31 +89,102 @@ timestamp_override = "event.ingested"
8389
type = "esql"
8490

8591
query = '''
86-
from logs-azure.signinlogs*
87-
// truncate the timestamp to a 30-minute window
88-
| eval target_time_window = DATE_TRUNC(30 minutes, @timestamp)
89-
| WHERE
90-
event.dataset == "azure.signinlogs"
91-
and event.category == "authentication"
92-
and to_lower(azure.signinlogs.properties.resource_display_name) rlike "(.*)365(.*)"
93-
and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs")
94-
and event.outcome != "success"
95-
and not (azure.signinlogs.category == "NonInteractiveUserSignInLogs"
96-
and azure.signinlogs.properties.status.error_code in (70043, 70044, 50057)
97-
and azure.signinlogs.properties.incoming_token_type in ("primaryRefreshToken", "refreshToken"))
98-
// for tuning review azure.signinlogs.properties.status.error_code
99-
// https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
100-
101-
// keep only relevant fields
102-
| 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
103-
104-
// count the number of login sources and failed login attempts
105-
| stats
106-
login_source_count = count(source.ip),
107-
failed_login_count = count(*) by target_time_window, azure.signinlogs.properties.user_principal_name
108-
109-
// filter for users with more than 20 login sources or failed login attempts
110-
| where (login_source_count >= 20 or failed_login_count >= 20)
92+
FROM logs-azure.signinlogs*
93+
94+
| EVAL
95+
time_window = DATE_TRUNC(5 minutes, @timestamp),
96+
user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name),
97+
ip = source.ip,
98+
login_error = azure.signinlogs.result_description,
99+
error_code = azure.signinlogs.result_type,
100+
request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type),
101+
app_name = TO_LOWER(azure.signinlogs.properties.app_display_name),
102+
asn_org = source.`as`.organization.name,
103+
country = source.geo.country_name,
104+
user_agent = user_agent.original,
105+
event_time = @timestamp
106+
107+
| WHERE event.dataset == "azure.signinlogs"
108+
AND event.category == "authentication"
109+
AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
110+
AND azure.signinlogs.properties.resource_display_name RLIKE "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
111+
AND event.outcome == "failure"
112+
AND NOT STARTS_WITH("Account is locked", login_error)
113+
AND azure.signinlogs.result_type IN (
114+
"50034", // UserAccountNotFound
115+
"50126", // InvalidUserNameOrPassword
116+
"50053", // IdsLocked or too many sign-in failures
117+
"70000", // InvalidGrant
118+
"70008", // Expired or revoked refresh token
119+
"70043", // Bad token due to sign-in frequency
120+
"50057", // UserDisabled
121+
"50055", // Password expired
122+
"50056", // Invalid or null password
123+
"50064", // Credential validation failure
124+
"50076", // MFA required but not passed
125+
"50079", // MFA registration required
126+
"50105" // EntitlementGrantsNotFound (no access to app)
127+
)
128+
AND user_id IS NOT NULL AND user_id != ""
129+
AND user_agent != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
130+
131+
| STATS
132+
authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
133+
client_app_id = VALUES(azure.signinlogs.properties.app_id),
134+
client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
135+
target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
136+
target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
137+
conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
138+
device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
139+
device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
140+
incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
141+
risk_state = VALUES(azure.signinlogs.properties.risk_state),
142+
session_id = VALUES(azure.signinlogs.properties.session_id),
143+
user_id = VALUES(azure.signinlogs.properties.user_id),
144+
user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
145+
result_description = VALUES(azure.signinlogs.result_description),
146+
result_signature = VALUES(azure.signinlogs.result_signature),
147+
result_type = VALUES(azure.signinlogs.result_type),
148+
149+
unique_users = COUNT_DISTINCT(user_id),
150+
user_id_list = VALUES(user_id),
151+
login_errors = VALUES(login_error),
152+
unique_login_errors = COUNT_DISTINCT(login_error),
153+
request_types = VALUES(request_type),
154+
app_names = VALUES(app_name),
155+
ip_list = VALUES(ip),
156+
unique_ips = COUNT_DISTINCT(ip),
157+
source_orgs = VALUES(asn_org),
158+
countries = VALUES(country),
159+
unique_country_count = COUNT_DISTINCT(country),
160+
unique_asn_orgs = COUNT_DISTINCT(asn_org),
161+
first_seen = MIN(event_time),
162+
last_seen = MAX(event_time),
163+
total_attempts = COUNT()
164+
BY time_window
165+
166+
| EVAL
167+
duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
168+
bf_type = CASE(
169+
unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 10 AND duration_seconds <= 1800, "password_spraying",
170+
unique_users >= 8 AND total_attempts >= 15 AND unique_login_errors <= 3 AND unique_ips <= 5 AND duration_seconds <= 600, "credential_stuffing",
171+
unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300, "password_guessing",
172+
"other"
173+
)
174+
175+
| KEEP
176+
time_window, bf_type, duration_seconds, total_attempts, first_seen, last_seen,
177+
unique_users, user_id_list, login_errors, unique_login_errors, request_types,
178+
app_names, ip_list, unique_ips, source_orgs, countries,
179+
unique_country_count, unique_asn_orgs,
180+
181+
authentication_requirement, client_app_id, client_app_display_name,
182+
target_resource_id, target_resource_display_name, conditional_access_status,
183+
device_detail_browser, device_detail_device_id, incoming_token_type,
184+
risk_state, session_id, user_id, user_principal_name,
185+
result_description, result_signature, result_type
186+
187+
| WHERE bf_type != "other"
111188
'''
112189

113190

@@ -117,6 +194,21 @@ framework = "MITRE ATT&CK"
117194
id = "T1110"
118195
name = "Brute Force"
119196
reference = "https://attack.mitre.org/techniques/T1110/"
197+
[[rule.threat.technique.subtechnique]]
198+
id = "T1110.001"
199+
name = "Password Guessing"
200+
reference = "https://attack.mitre.org/techniques/T1110/001/"
201+
202+
[[rule.threat.technique.subtechnique]]
203+
id = "T1110.003"
204+
name = "Password Spraying"
205+
reference = "https://attack.mitre.org/techniques/T1110/003/"
206+
207+
[[rule.threat.technique.subtechnique]]
208+
id = "T1110.004"
209+
name = "Credential Stuffing"
210+
reference = "https://attack.mitre.org/techniques/T1110/004/"
211+
120212

121213

122214
[rule.threat.tactic]

rules/integrations/o365/credential_access_microsoft_365_potential_user_account_brute_force.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
creation_date = "2020/11/30"
33
integration = ["o365"]
44
maturity = "production"
5-
updated_date = "2025/05/09"
5+
updated_date = "2025/05/20"
66
min_stack_version = "8.17.0"
77
min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."
88

@@ -19,7 +19,8 @@ false_positives = [
1919
positives.
2020
""",
2121
]
22-
from = "now-9m"
22+
from = "now-60m"
23+
interval = "10m"
2324
language = "esql"
2425
license = "Elastic License v2"
2526
name = "Potential Microsoft 365 User Account Brute Force"
@@ -29,7 +30,7 @@ note = """## Triage and Analysis
2930
3031
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.
3132
32-
### Investigation Steps
33+
### Possible investigation steps
3334
3435
- Review `user_id_list`: Enumerates the user accounts targeted. Look for naming patterns or privilege levels (e.g., admins).
3536
- Check `login_errors`: A consistent error such as `"InvalidUserNameOrPassword"` confirms a spray-style attack using one or a few passwords.
@@ -39,13 +40,13 @@ Identifies brute-force authentication activity targeting Microsoft 365 user acco
3940
- Cross-reference with successful logins: Pivot to surrounding sign-in logs (`azure.signinlogs`) or risk detections (`identityprotection`) for any account that eventually succeeded.
4041
- Check for multi-factor challenges or bypasses: Determine if any of the accounts were protected or if the attack bypassed MFA.
4142
42-
### False Positive Analysis
43+
### False positive analysis
4344
4445
- IT administrators using automation tools (e.g., PowerShell) during account provisioning may trigger false positives if login attempts cluster.
4546
- Penetration testing or red team simulations may resemble spray activity.
4647
- Infrequent, low-volume login testing tools like ADFS testing scripts can exhibit similar patterns.
4748
48-
### Response Recommendations
49+
### Response and remediation
4950
5051
- Initiate an internal incident ticket and inform the affected identity/IT team.
5152
- Temporarily disable impacted user accounts if compromise is suspected.

0 commit comments

Comments
 (0)