Potential Microsoft 365 Brute Force via Entra ID Sign-Ins
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.
Elastic rule (View on GitHub)
1[metadata]
2creation_date = "2024/09/06"
3integration = ["azure"]
4maturity = "production"
5updated_date = "2025/05/20"
6min_stack_version = "8.17.0"
7min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above."
8
9[rule]
10author = ["Elastic"]
11description = """
12Identifies potential brute-force attacks targeting Microsoft 365 user accounts by analyzing failed sign-in patterns in
13Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive
14authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or
15password guessing. Adversaries may use these techniques to gain unauthorized access to Microsoft 365 services such as
16Exchange Online, SharePoint, or Teams.
17"""
18false_positives = [
19 """
20 Automated processes that attempt to authenticate using expired credentials or have misconfigured authentication
21 settings may lead to false positives.
22 """,
23]
24from = "now-60m"
25interval = "10m"
26language = "esql"
27license = "Elastic License v2"
28name = "Potential Microsoft 365 Brute Force via Entra ID Sign-Ins"
29note = """## Triage and analysis
30
31### Investigating Potential Microsoft 365 Brute Force via Entra ID Sign-Ins
32
33Identifies 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.
34
35### Possible investigation steps
36
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.
49
50### False positive analysis
51
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.
55
56### Response and remediation
57
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"""
65references = [
66 "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",
72 "https://github.com/0xZDH/o365spray",
73]
74risk_score = 47
75rule_id = "35ab3cfa-6c67-11ef-ab4d-f661ea17fbcc"
76severity = "medium"
77tags = [
78 "Domain: Cloud",
79 "Domain: SaaS",
80 "Data Source: Azure",
81 "Data Source: Entra ID",
82 "Data Source: Entra ID Sign-in",
83 "Use Case: Identity and Access Audit",
84 "Use Case: Threat Detection",
85 "Tactic: Credential Access",
86 "Resources: Investigation Guide",
87]
88timestamp_override = "event.ingested"
89type = "esql"
90
91query = '''
92FROM 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"
188'''
189
190
191[[rule.threat]]
192framework = "MITRE ATT&CK"
193[[rule.threat.technique]]
194id = "T1110"
195name = "Brute Force"
196reference = "https://attack.mitre.org/techniques/T1110/"
197[[rule.threat.technique.subtechnique]]
198id = "T1110.001"
199name = "Password Guessing"
200reference = "https://attack.mitre.org/techniques/T1110/001/"
201
202[[rule.threat.technique.subtechnique]]
203id = "T1110.003"
204name = "Password Spraying"
205reference = "https://attack.mitre.org/techniques/T1110/003/"
206
207[[rule.threat.technique.subtechnique]]
208id = "T1110.004"
209name = "Credential Stuffing"
210reference = "https://attack.mitre.org/techniques/T1110/004/"
211
212
213
214[rule.threat.tactic]
215id = "TA0006"
216name = "Credential Access"
217reference = "https://attack.mitre.org/tactics/TA0006/"
Triage and analysis
Investigating Potential Microsoft 365 Brute Force via Entra ID Sign-Ins
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
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
andsource_orgs
: Determine if requests came from known VPNs, hosting providers, or anonymized infrastructure. - Validate
unique_ips
andcountries
: Multiple countries or IPs in a short window may indicate credential stuffing or distributed spray attempts. - Compare
total_attempts
vsduration_seconds
: High volume over a short duration supports non-human interaction. - Inspect
user_agent.original
viadevice_detail_browser
: Clients likePython Requests
orcurl
are highly suspicious. - Investigate
client_app_display_name
andincoming_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
anddevice_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
- 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
- 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
Related rules
- Microsoft Entra ID Concurrent Sign-Ins with Suspicious Properties
- Azure Entra MFA TOTP Brute Force Attempts
- Azure Entra ID Password Spraying (Non-Interactive SFA)
- Microsoft Azure or Mail Sign-in from a Suspicious Source
- Azure Entra ID Rare App ID for Principal Authentication