Microsoft 365 Brute Force via Entra ID Sign-Ins
editMicrosoft 365 Brute Force via Entra ID Sign-Ins
editIdentifies 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.
Rule type: esql
Rule indices: None
Severity: medium
Risk score: 47
Runs every: 15m
Searches indices from: now-60m (Date Math format, see also Additional look-back time
)
Maximum alerts per execution: 100
References:
- https://cloud.hacktricks.xyz/pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/az-password-spraying
- https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray
- https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties
- https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/
- https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
- https://github.com/0xZDH/Omnispray
- https://github.com/0xZDH/o365spray
Tags:
- Domain: Cloud
- Domain: SaaS
- Domain: Identity
- Data Source: Azure
- Data Source: Entra ID
- Data Source: Entra ID Sign-in Logs
- Use Case: Identity and Access Audit
- Use Case: Threat Detection
- Tactic: Credential Access
- Resources: Investigation Guide
Version: 106
Rule authors:
- Elastic
Rule license: Elastic License v2
Investigation guide
editTriage 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.
Rule query
editfrom logs-azure.signinlogs* | eval Esql.time_window_date_trunc = date_trunc(15 minutes, @timestamp), Esql_priv.azure_signinlogs_properties_user_principal_name_lower = to_lower(azure.signinlogs.properties.user_principal_name), Esql.azure_signinlogs_properties_incoming_token_type_lower = to_lower(azure.signinlogs.properties.incoming_token_type), Esql.azure_signinlogs_properties_app_display_name_lower = to_lower(azure.signinlogs.properties.app_display_name), Esql.user_agent_original = user_agent.original | where event.dataset == "azure.signinlogs" and event.category == "authentication" and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs") and azure.signinlogs.properties.resource_display_name rlike "(.*)365|SharePoint|Exchange|Teams|Office(.*)" and event.outcome == "failure" and azure.signinlogs.properties.status.error_code != 50053 and azure.signinlogs.properties.status.error_code in ( 50034, // UserAccountNotFound 50126, // InvalidUsernameOrPassword 50055, // PasswordExpired 50056, // InvalidPassword 50057, // UserDisabled 50064, // CredentialValidationFailure 50076, // MFARequiredButNotPassed 50079, // MFARegistrationRequired 50105, // EntitlementGrantsNotFound 70000, // InvalidGrant 70008, // ExpiredOrRevokedRefreshToken 70043, // BadTokenDueToSignInFrequency 80002, // OnPremisePasswordValidatorRequestTimedOut 80005, // OnPremisePasswordValidatorUnpredictableWebException 50144, // InvalidPasswordExpiredOnPremPassword 50135, // PasswordChangeCompromisedPassword 50142, // PasswordChangeRequiredConditionalAccess 120000, // PasswordChangeIncorrectCurrentPassword 120002, // PasswordChangeInvalidNewPasswordWeak 120020 // PasswordChangeFailure ) and azure.signinlogs.properties.user_principal_name is not null and azure.signinlogs.properties.user_principal_name != "" and user_agent.original != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0" | stats Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement), Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id), Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name), Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id), Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name), Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status), Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser), Esql.azure_signinlogs_properties_device_detail_device_id_values = values(azure.signinlogs.properties.device_detail.device_id), Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system), Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type), Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state), Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id), Esql.azure_signinlogs_properties_user_id_values = values(azure.signinlogs.properties.user_id), Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name), Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description), Esql.azure_signinlogs_result_signature_values = values(azure.signinlogs.result_signature), Esql.azure_signinlogs_result_type_values = values(azure.signinlogs.result_type), Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct = count_distinct(Esql_priv.azure_signinlogs_properties_user_principal_name_lower), Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values = values(Esql_priv.azure_signinlogs_properties_user_principal_name_lower), Esql.azure_signinlogs_result_description_count_distinct = count_distinct(azure.signinlogs.result_description), Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description), Esql.azure_signinlogs_properties_status_error_code_count_distinct = count_distinct(azure.signinlogs.properties.status.error_code), Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code), Esql.azure_signinlogs_properties_incoming_token_type_lower_values = values(Esql.azure_signinlogs_properties_incoming_token_type_lower), Esql.azure_signinlogs_properties_app_display_name_lower_values = values(Esql.azure_signinlogs_properties_app_display_name_lower), Esql.source_ip_values = values(source.ip), Esql.source_ip_count_distinct = count_distinct(source.ip), Esql.source_as_organization_name_values = values(source.`as`.organization.name), Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name), Esql.source_geo_country_name_values = values(source.geo.country_name), Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name), Esql.@timestamp.min = min(@timestamp), Esql.@timestamp.max = max(@timestamp), Esql.event_count = count() by Esql.time_window_date_trunc | eval Esql.event_duration_seconds = date_diff("seconds", Esql.@timestamp.min, Esql.@timestamp.max), Esql.event_bf_type = case( Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 10 and Esql.event_count >= 30 and Esql.azure_signinlogs_result_description_count_distinct <= 3 and Esql.source_ip_count_distinct >= 5 and Esql.event_duration_seconds <= 600 and Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct > Esql.source_ip_count_distinct, "credential_stuffing", Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 15 and Esql.azure_signinlogs_result_description_count_distinct == 1 and Esql.event_count >= 15 and Esql.event_duration_seconds <= 1800, "password_spraying", (Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct == 1 and Esql.azure_signinlogs_result_description_count_distinct == 1 and Esql.event_count >= 30 and Esql.event_duration_seconds <= 300) or (Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct <= 3 and Esql.source_ip_count_distinct > 30 and Esql.event_count >= 100), "password_guessing", "other" ) | where Esql.event_bf_type != "other" | keep Esql.time_window_date_trunc, Esql.event_bf_type, Esql.event_duration_seconds, Esql.event_count, Esql.@timestamp.min, Esql.@timestamp.max, Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct, Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values, Esql.azure_signinlogs_result_description_count_distinct, Esql.azure_signinlogs_result_description_values, Esql.azure_signinlogs_properties_status_error_code_count_distinct, Esql.azure_signinlogs_properties_status_error_code_values, Esql.azure_signinlogs_properties_incoming_token_type_lower_values, Esql.azure_signinlogs_properties_app_display_name_lower_values, Esql.source_ip_values, Esql.source_ip_count_distinct, Esql.source_as_organization_name_values, Esql.source_as_organization_name_count_distinct, Esql.source_geo_country_name_values, Esql.source_geo_country_name_count_distinct, Esql.azure_signinlogs_properties_authentication_requirement_values, Esql.azure_signinlogs_properties_app_id_values, Esql.azure_signinlogs_properties_app_display_name_values, Esql.azure_signinlogs_properties_resource_id_values, Esql.azure_signinlogs_properties_resource_display_name_values, Esql.azure_signinlogs_properties_conditional_access_status_values, Esql.azure_signinlogs_properties_device_detail_browser_values, Esql.azure_signinlogs_properties_device_detail_device_id_values, Esql.azure_signinlogs_properties_device_detail_operating_system_values, Esql.azure_signinlogs_properties_incoming_token_type_values, Esql.azure_signinlogs_properties_risk_state_values, Esql.azure_signinlogs_properties_session_id_values, Esql.azure_signinlogs_properties_user_id_values
Framework: MITRE ATT&CKTM
-
Tactic:
- Name: Credential Access
- ID: TA0006
- Reference URL: https://attack.mitre.org/tactics/TA0006/
-
Technique:
- Name: Brute Force
- ID: T1110
- Reference URL: https://attack.mitre.org/techniques/T1110/
-
Sub-technique:
- Name: Password Guessing
- ID: T1110.001
- Reference URL: https://attack.mitre.org/techniques/T1110/001/
-
Sub-technique:
- Name: Password Spraying
- ID: T1110.003
- Reference URL: https://attack.mitre.org/techniques/T1110/003/
-
Sub-technique:
- Name: Credential Stuffing
- ID: T1110.004
- Reference URL: https://attack.mitre.org/techniques/T1110/004/