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 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

  • 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

to-top