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/07/16"
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 = "Microsoft 365 Brute Force via Entra ID Sign-Ins"
27note = """## Triage and analysis
28
29### Investigating Microsoft 365 Brute Force via Entra ID Sign-Ins
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]
87timestamp_override = "event.ingested"
88type = "esql"
89
90query = '''
91from logs-azure.signinlogs*
92
93| eval
94 Esql.time_window_date_trunc = date_trunc(15 minutes, @timestamp),
95 Esql_priv.azure_signinlogs_properties_user_principal_name_lower = to_lower(azure.signinlogs.properties.user_principal_name),
96 Esql.azure_signinlogs_properties_incoming_token_type_lower = to_lower(azure.signinlogs.properties.incoming_token_type),
97 Esql.azure_signinlogs_properties_app_display_name_lower = to_lower(azure.signinlogs.properties.app_display_name),
98 Esql.user_agent_original = user_agent.original
99
100| where event.dataset == "azure.signinlogs"
101 and event.category == "authentication"
102 and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs")
103 and azure.signinlogs.properties.resource_display_name rlike "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
104 and event.outcome == "failure"
105 and azure.signinlogs.properties.status.error_code != 50053
106 and azure.signinlogs.properties.status.error_code in (
107 50034, // UserAccountNotFound
108 50126, // InvalidUsernameOrPassword
109 50055, // PasswordExpired
110 50056, // InvalidPassword
111 50057, // UserDisabled
112 50064, // CredentialValidationFailure
113 50076, // MFARequiredButNotPassed
114 50079, // MFARegistrationRequired
115 50105, // EntitlementGrantsNotFound
116 70000, // InvalidGrant
117 70008, // ExpiredOrRevokedRefreshToken
118 70043, // BadTokenDueToSignInFrequency
119 80002, // OnPremisePasswordValidatorRequestTimedOut
120 80005, // OnPremisePasswordValidatorUnpredictableWebException
121 50144, // InvalidPasswordExpiredOnPremPassword
122 50135, // PasswordChangeCompromisedPassword
123 50142, // PasswordChangeRequiredConditionalAccess
124 120000, // PasswordChangeIncorrectCurrentPassword
125 120002, // PasswordChangeInvalidNewPasswordWeak
126 120020 // PasswordChangeFailure
127 )
128 and azure.signinlogs.properties.user_principal_name is not null
129 and azure.signinlogs.properties.user_principal_name != ""
130 and user_agent.original != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
131
132| stats
133 Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
134 Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
135 Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name),
136 Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
137 Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
138 Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
139 Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser),
140 Esql.azure_signinlogs_properties_device_detail_device_id_values = values(azure.signinlogs.properties.device_detail.device_id),
141 Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system),
142 Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type),
143 Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
144 Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id),
145 Esql.azure_signinlogs_properties_user_id_values = values(azure.signinlogs.properties.user_id),
146 Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
147 Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description),
148 Esql.azure_signinlogs_result_signature_values = values(azure.signinlogs.result_signature),
149 Esql.azure_signinlogs_result_type_values = values(azure.signinlogs.result_type),
150
151 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct = count_distinct(Esql_priv.azure_signinlogs_properties_user_principal_name_lower),
152 Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values = values(Esql_priv.azure_signinlogs_properties_user_principal_name_lower),
153 Esql.azure_signinlogs_result_description_count_distinct = count_distinct(azure.signinlogs.result_description),
154 Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description),
155 Esql.azure_signinlogs_properties_status_error_code_count_distinct = count_distinct(azure.signinlogs.properties.status.error_code),
156 Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
157 Esql.azure_signinlogs_properties_incoming_token_type_lower_values = values(Esql.azure_signinlogs_properties_incoming_token_type_lower),
158 Esql.azure_signinlogs_properties_app_display_name_lower_values = values(Esql.azure_signinlogs_properties_app_display_name_lower),
159 Esql.source_ip_values = values(source.ip),
160 Esql.source_ip_count_distinct = count_distinct(source.ip),
161 Esql.source_as_organization_name_values = values(source.`as`.organization.name),
162 Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name),
163 Esql.source_geo_country_name_values = values(source.geo.country_name),
164 Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name),
165 Esql.@timestamp.min = min(@timestamp),
166 Esql.@timestamp.max = max(@timestamp),
167 Esql.event_count = count()
168by Esql.time_window_date_trunc
169
170| eval
171 Esql.event_duration_seconds = date_diff("seconds", Esql.@timestamp.min, Esql.@timestamp.max),
172 Esql.event_bf_type = case(
173 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 10
174 and Esql.event_count >= 30
175 and Esql.azure_signinlogs_result_description_count_distinct <= 3
176 and Esql.source_ip_count_distinct >= 5
177 and Esql.event_duration_seconds <= 600
178 and Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct > Esql.source_ip_count_distinct,
179 "credential_stuffing",
180
181 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 15
182 and Esql.azure_signinlogs_result_description_count_distinct == 1
183 and Esql.event_count >= 15
184 and Esql.event_duration_seconds <= 1800,
185 "password_spraying",
186
187 (Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct == 1
188 and Esql.azure_signinlogs_result_description_count_distinct == 1
189 and Esql.event_count >= 30
190 and Esql.event_duration_seconds <= 300)
191 or (Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct <= 3
192 and Esql.source_ip_count_distinct > 30
193 and Esql.event_count >= 100),
194 "password_guessing",
195
196 "other"
197 )
198
199| where Esql.event_bf_type != "other"
200
201| keep
202 Esql.time_window_date_trunc,
203 Esql.event_bf_type,
204 Esql.event_duration_seconds,
205 Esql.event_count,
206 Esql.@timestamp.min,
207 Esql.@timestamp.max,
208 Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct,
209 Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values,
210 Esql.azure_signinlogs_result_description_count_distinct,
211 Esql.azure_signinlogs_result_description_values,
212 Esql.azure_signinlogs_properties_status_error_code_count_distinct,
213 Esql.azure_signinlogs_properties_status_error_code_values,
214 Esql.azure_signinlogs_properties_incoming_token_type_lower_values,
215 Esql.azure_signinlogs_properties_app_display_name_lower_values,
216 Esql.source_ip_values,
217 Esql.source_ip_count_distinct,
218 Esql.source_as_organization_name_values,
219 Esql.source_as_organization_name_count_distinct,
220 Esql.source_geo_country_name_values,
221 Esql.source_geo_country_name_count_distinct,
222 Esql.azure_signinlogs_properties_authentication_requirement_values,
223 Esql.azure_signinlogs_properties_app_id_values,
224 Esql.azure_signinlogs_properties_app_display_name_values,
225 Esql.azure_signinlogs_properties_resource_id_values,
226 Esql.azure_signinlogs_properties_resource_display_name_values,
227 Esql.azure_signinlogs_properties_conditional_access_status_values,
228 Esql.azure_signinlogs_properties_device_detail_browser_values,
229 Esql.azure_signinlogs_properties_device_detail_device_id_values,
230 Esql.azure_signinlogs_properties_device_detail_operating_system_values,
231 Esql.azure_signinlogs_properties_incoming_token_type_values,
232 Esql.azure_signinlogs_properties_risk_state_values,
233 Esql.azure_signinlogs_properties_session_id_values,
234 Esql.azure_signinlogs_properties_user_id_values
235'''
236
237
238[[rule.threat]]
239framework = "MITRE ATT&CK"
240[[rule.threat.technique]]
241id = "T1110"
242name = "Brute Force"
243reference = "https://attack.mitre.org/techniques/T1110/"
244[[rule.threat.technique.subtechnique]]
245id = "T1110.001"
246name = "Password Guessing"
247reference = "https://attack.mitre.org/techniques/T1110/001/"
248
249[[rule.threat.technique.subtechnique]]
250id = "T1110.003"
251name = "Password Spraying"
252reference = "https://attack.mitre.org/techniques/T1110/003/"
253
254[[rule.threat.technique.subtechnique]]
255id = "T1110.004"
256name = "Credential Stuffing"
257reference = "https://attack.mitre.org/techniques/T1110/004/"
258
259
260
261[rule.threat.tactic]
262id = "TA0006"
263name = "Credential Access"
264reference = "https://attack.mitre.org/tactics/TA0006/"
Triage and analysis
Investigating 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 Exccessive Account Lockouts Detected
- Microsoft Entra ID Sign-In Brute Force Activity
- Deprecated - Azure Entra Sign-in Brute Force Microsoft 365 Accounts by Repeat Source
- Microsoft Entra ID Concurrent Sign-Ins with Suspicious Properties
- Microsoft Entra ID MFA TOTP Brute Force Attempts