Entra ID Sign-in Brute Force Attempted (Microsoft 365)
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 = "2026/09/18"
6
7[rule]
8author = ["Elastic"]
9description = """
10Identifies potential brute-force attacks targeting Microsoft 365 user accounts by analyzing failed sign-in patterns in
11Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive
12authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or
13password guessing. Adversaries may use these techniques to gain unauthorized access to Microsoft 365 services such as
14Exchange Online, SharePoint, or Teams.
15"""
16false_positives = [
17 """
18 Automated processes that attempt to authenticate using expired credentials or have misconfigured authentication
19 settings may lead to false positives.
20 """,
21]
22from = "now-60m"
23interval = "15m"
24language = "esql"
25license = "Elastic License v2"
26name = "Entra ID Sign-in Brute Force Attempted (Microsoft 365)"
27note = """## Triage and analysis
28
29### Investigating Entra ID Sign-in Brute Force Attempted (Microsoft 365)
30
31Identifies 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.
32
33### Possible investigation steps
34
35- Review `bf_type`: Classifies the brute-force behavior (`password_spraying`, `credential_stuffing`, `password_guessing`).
36- Examine `user_id_list`: Review the identities targeted. Are they admins, service accounts, or external identities?
37- Review `login_errors`: Multiple identical errors (e.g., `"Invalid grant..."`) suggest automated abuse or tooling.
38- Check `ip_list` and `source_orgs`: Determine if requests came from known VPNs, hosting providers, or anonymized infrastructure.
39- Validate `unique_ips` and `countries`: Multiple countries or IPs in a short window may indicate credential stuffing or distributed spray attempts.
40- Compare `total_attempts` vs `duration_seconds`: High volume over a short duration supports non-human interaction.
41- Inspect `user_agent.original` via `device_detail_browser`: Clients like `Python Requests` or `curl` are highly suspicious.
42- Investigate `client_app_display_name` and `incoming_token_type`: Identify non-browser-based logins, token abuse or commonly mimicked clients like VSCode.
43- Review `target_resource_display_name`: Confirm the service being targeted (e.g., SharePoint, Exchange). This may be what authorization is being attempted against.
44- Pivot using `session_id` and `device_detail_device_id`: Determine if a single device is spraying multiple accounts.
45- Check `conditional_access_status`: If "notApplied", determine whether conditional access is properly scoped.
46- Correlate `user_principal_name` with successful sign-ins: Investigate surrounding logs for lateral movement or privilege abuse.
47
48### False positive analysis
49
50- Developer automation (e.g., CI/CD logins) or mobile sync errors may create noisy but benign login failures.
51- Red team exercises or pentesting can resemble brute-force patterns.
52- Legacy protocols or misconfigured service principals may trigger repeated login failures from the same IP or session.
53
54### Response and remediation
55
56- Notify identity or security operations teams to investigate further.
57- Lock or reset affected user accounts if compromise is suspected.
58- Block the source IP(s) or ASN temporarily using conditional access or firewall rules.
59- Review tenant-wide MFA and conditional access enforcement.
60- Audit targeted accounts for password reuse across systems or tenants.
61- Enable lockout or throttling policies for repeated failed login attempts.
62"""
63references = [
64 "https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying",
65 "https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray",
66 "https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties",
67 "https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/",
68 "https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes",
69 "https://github.com/0xZDH/Omnispray",
70 "https://github.com/0xZDH/o365spray",
71]
72risk_score = 47
73rule_id = "35ab3cfa-6c67-11ef-ab4d-f661ea17fbcc"
74severity = "medium"
75tags = [
76 "Domain: Cloud",
77 "Domain: SaaS",
78 "Domain: Identity",
79 "Data Source: Azure",
80 "Data Source: Entra ID",
81 "Data Source: Entra ID Sign-In Logs",
82 "Use Case: Identity and Access Audit",
83 "Use Case: Threat Detection",
84 "Tactic: Credential Access",
85 "Resources: Investigation Guide",
86 "Noise: High",
87 "Performance: Normal",
88 "Profile: Aggressive",
89 "Threat: Brute Force",
90 "Rule Type: ES|QL",
91 "Platform: Entra ID",
92]
93timestamp_override = "event.ingested"
94type = "esql"
95
96query = '''
97from logs-azure.signinlogs-*
98
99| eval
100 Esql.time_window_date_trunc = date_trunc(15 minutes, @timestamp),
101 Esql_priv.azure_signinlogs_properties_user_principal_name_lower = to_lower(azure.signinlogs.properties.user_principal_name),
102 Esql.azure_signinlogs_properties_incoming_token_type_lower = to_lower(azure.signinlogs.properties.incoming_token_type),
103 Esql.azure_signinlogs_properties_app_display_name_lower = to_lower(azure.signinlogs.properties.app_display_name),
104 Esql.user_agent_original = user_agent.original
105
106| where data_stream.dataset == "azure.signinlogs"
107 and event.category == "authentication"
108 and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs")
109 and azure.signinlogs.properties.resource_display_name rlike "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
110 and event.outcome == "failure"
111 and azure.signinlogs.properties.status.error_code != 50053
112 and azure.signinlogs.properties.status.error_code in (
113 50034, // UserAccountNotFound
114 50126, // InvalidUsernameOrPassword
115 50055, // PasswordExpired
116 50056, // InvalidPassword
117 50057, // UserDisabled
118 50064, // CredentialValidationFailure
119 50076, // MFARequiredButNotPassed
120 50079, // MFARegistrationRequired
121 50105, // EntitlementGrantsNotFound
122 70000, // InvalidGrant
123 70008, // ExpiredOrRevokedRefreshToken
124 70043, // BadTokenDueToSignInFrequency
125 80002, // OnPremisePasswordValidatorRequestTimedOut
126 80005, // OnPremisePasswordValidatorUnpredictableWebException
127 50144, // InvalidPasswordExpiredOnPremPassword
128 50135, // PasswordChangeCompromisedPassword
129 50142, // PasswordChangeRequiredConditionalAccess
130 120000, // PasswordChangeIncorrectCurrentPassword
131 120002, // PasswordChangeInvalidNewPasswordWeak
132 120020 // PasswordChangeFailure
133 )
134 and azure.signinlogs.properties.user_principal_name is not null
135 and azure.signinlogs.properties.user_principal_name != ""
136 and user_agent.original != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
137
138| stats
139 Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
140 Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
141 Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name),
142 Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
143 Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
144 Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
145 Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser),
146 Esql.azure_signinlogs_properties_device_detail_device_id_values = values(azure.signinlogs.properties.device_detail.device_id),
147 Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system),
148 Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type),
149 Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
150 Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id),
151 Esql.azure_signinlogs_properties_user_id_values = values(azure.signinlogs.properties.user_id),
152 Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
153 Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description),
154 Esql.azure_signinlogs_result_signature_values = values(azure.signinlogs.result_signature),
155 Esql.azure_signinlogs_result_type_values = values(azure.signinlogs.result_type),
156
157 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct = count_distinct(Esql_priv.azure_signinlogs_properties_user_principal_name_lower),
158 Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values = values(Esql_priv.azure_signinlogs_properties_user_principal_name_lower),
159 Esql.azure_signinlogs_result_description_count_distinct = count_distinct(azure.signinlogs.result_description),
160 Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description),
161 Esql.azure_signinlogs_properties_status_error_code_count_distinct = count_distinct(azure.signinlogs.properties.status.error_code),
162 Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
163 Esql.azure_signinlogs_properties_incoming_token_type_lower_values = values(Esql.azure_signinlogs_properties_incoming_token_type_lower),
164 Esql.azure_signinlogs_properties_app_display_name_lower_values = values(Esql.azure_signinlogs_properties_app_display_name_lower),
165 Esql.source_ip_values = values(source.ip),
166 Esql.source_ip_count_distinct = count_distinct(source.ip),
167 Esql.source_as_organization_name_values = values(source.`as`.organization.name),
168 Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name),
169 Esql.source_geo_country_name_values = values(source.geo.country_name),
170 Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name),
171 Esql.@timestamp.min = min(@timestamp),
172 Esql.@timestamp.max = max(@timestamp),
173 Esql.event_count = count()
174by Esql.time_window_date_trunc
175
176| eval
177 Esql.event_duration_seconds = date_diff("seconds", Esql.@timestamp.min, Esql.@timestamp.max),
178 Esql.event_bf_type = case(
179 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 10
180 and Esql.event_count >= 30
181 and Esql.azure_signinlogs_result_description_count_distinct <= 3
182 and Esql.source_ip_count_distinct >= 5
183 and Esql.event_duration_seconds <= 600
184 and Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct > Esql.source_ip_count_distinct,
185 "credential_stuffing",
186
187 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 15
188 and Esql.azure_signinlogs_result_description_count_distinct == 1
189 and Esql.event_count >= 15
190 and Esql.event_duration_seconds <= 1800,
191 "password_spraying",
192
193 (Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct == 1
194 and Esql.azure_signinlogs_result_description_count_distinct == 1
195 and Esql.event_count >= 30
196 and Esql.event_duration_seconds <= 300)
197 or (Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct <= 3
198 and Esql.source_ip_count_distinct > 30
199 and Esql.event_count >= 100),
200 "password_guessing",
201
202 "other"
203 )
204
205| where Esql.event_bf_type != "other"
206
207| keep
208 Esql.time_window_date_trunc,
209 Esql.event_bf_type,
210 Esql.event_duration_seconds,
211 Esql.event_count,
212 Esql.@timestamp.min,
213 Esql.@timestamp.max,
214 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct,
215 Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values,
216 Esql.azure_signinlogs_result_description_count_distinct,
217 Esql.azure_signinlogs_result_description_values,
218 Esql.azure_signinlogs_properties_status_error_code_count_distinct,
219 Esql.azure_signinlogs_properties_status_error_code_values,
220 Esql.azure_signinlogs_properties_incoming_token_type_lower_values,
221 Esql.azure_signinlogs_properties_app_display_name_lower_values,
222 Esql.source_ip_values,
223 Esql.source_ip_count_distinct,
224 Esql.source_as_organization_name_values,
225 Esql.source_as_organization_name_count_distinct,
226 Esql.source_geo_country_name_values,
227 Esql.source_geo_country_name_count_distinct,
228 Esql.azure_signinlogs_properties_authentication_requirement_values,
229 Esql.azure_signinlogs_properties_app_id_values,
230 Esql.azure_signinlogs_properties_app_display_name_values,
231 Esql.azure_signinlogs_properties_resource_id_values,
232 Esql.azure_signinlogs_properties_resource_display_name_values,
233 Esql.azure_signinlogs_properties_conditional_access_status_values,
234 Esql.azure_signinlogs_properties_device_detail_browser_values,
235 Esql.azure_signinlogs_properties_device_detail_device_id_values,
236 Esql.azure_signinlogs_properties_device_detail_operating_system_values,
237 Esql.azure_signinlogs_properties_incoming_token_type_values,
238 Esql.azure_signinlogs_properties_risk_state_values,
239 Esql.azure_signinlogs_properties_session_id_values,
240 Esql.azure_signinlogs_properties_user_id_values
241'''
242
243
244[[rule.threat]]
245framework = "MITRE ATT&CK"
246[[rule.threat.technique]]
247id = "T1110"
248name = "Brute Force"
249reference = "https://attack.mitre.org/techniques/T1110/"
250[[rule.threat.technique.subtechnique]]
251id = "T1110.001"
252name = "Password Guessing"
253reference = "https://attack.mitre.org/techniques/T1110/001/"
254
255[[rule.threat.technique.subtechnique]]
256id = "T1110.003"
257name = "Password Spraying"
258reference = "https://attack.mitre.org/techniques/T1110/003/"
259
260[[rule.threat.technique.subtechnique]]
261id = "T1110.004"
262name = "Credential Stuffing"
263reference = "https://attack.mitre.org/techniques/T1110/004/"
264
265
266
267[rule.threat.tactic]
268id = "TA0006"
269name = "Credential Access"
270reference = "https://attack.mitre.org/tactics/TA0006/"
Triage and analysis
Investigating Entra ID Sign-in Brute Force Attempted (Microsoft 365)
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_listandsource_orgs: Determine if requests came from known VPNs, hosting providers, or anonymized infrastructure. - Validate
unique_ipsandcountries: Multiple countries or IPs in a short window may indicate credential stuffing or distributed spray attempts. - Compare
total_attemptsvsduration_seconds: High volume over a short duration supports non-human interaction. - Inspect
user_agent.originalviadevice_detail_browser: Clients likePython Requestsorcurlare highly suspicious. - Investigate
client_app_display_nameandincoming_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_idanddevice_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_namewith 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
- Entra ID Concurrent Sign-in with Suspicious Properties
- Entra ID MFA TOTP Brute Force Attempted
- Entra ID User Sign-in Brute Force Attempted
- M365 or Entra ID Identity Sign-in from a Suspicious Source
- Entra ID OAuth Device Code Flow with Concurrent Sign-ins