Potential Microsoft 365 User Account Brute Force
editPotential Microsoft 365 User Account Brute Force
editIdentifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.
Rule type: esql
Rule indices: None
Severity: medium
Risk score: 47
Runs every: 10m
Searches indices from: now-60m (Date Math format, see also Additional look-back time
)
Maximum alerts per execution: 100
References:
- 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://github.com/0xZDH/Omnispray
- https://github.com/0xZDH/o365spray
Tags:
- Domain: Cloud
- Domain: SaaS
- Data Source: Microsoft 365
- Data Source: Microsoft 365 Audit Logs
- Use Case: Identity and Access Audit
- Use Case: Threat Detection
- Tactic: Credential Access
- Resources: Investigation Guide
Version: 414
Rule authors:
- Elastic
- Willem D’Haese
- Austin Songer
Rule license: Elastic License v2
Investigation guide
editTriage and Analysis
Investigating Potential Microsoft 365 User Account Brute Force
Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.
Possible investigation steps
-
Review
user_id_list
: Enumerates the user accounts targeted. Look for naming patterns or privilege levels (e.g., admins). -
Check
login_errors
: A consistent error such as"InvalidUserNameOrPassword"
confirms a spray-style attack using one or a few passwords. -
Examine
ip_list
andsource_orgs
: Determine if the traffic originates from a known corporate VPN, datacenter, or suspicious ASN like hosting providers or anonymizers. -
Review
countries
andunique_country_count
: Geographic anomalies (e.g., login attempts from unexpected regions) may indicate malicious automation. -
Validate
total_attempts
vsduration_seconds
: A high frequency of login attempts over a short period may suggest automation rather than manual logins. -
Cross-reference with successful logins: Pivot to surrounding sign-in logs (
azure.signinlogs
) or risk detections (identityprotection
) for any account that eventually succeeded. - Check for multi-factor challenges or bypasses: Determine if any of the accounts were protected or if the attack bypassed MFA.
False positive analysis
- IT administrators using automation tools (e.g., PowerShell) during account provisioning may trigger false positives if login attempts cluster.
- Penetration testing or red team simulations may resemble spray activity.
- Infrequent, low-volume login testing tools like ADFS testing scripts can exhibit similar patterns.
Response and remediation
- Initiate an internal incident ticket and inform the affected identity/IT team.
- Temporarily disable impacted user accounts if compromise is suspected.
- Investigate whether any login attempts succeeded after the spray window.
- Block the offending IPs or ASN temporarily via firewall or conditional access policies.
- Rotate passwords for all targeted accounts and audit for password reuse.
- Enforce or verify MFA is enabled for all user accounts.
- Consider deploying account lockout or progressive delay mechanisms if not already enabled.
Rule query
editfrom logs-o365.audit-* | mv_expand event.category | eval Esql.time_window_date_trunc = date_trunc(5 minutes, @timestamp), Esql_priv.o365_audit_UserId_lower = to_lower(o365.audit.UserId), Esql.o365_audit_LogonError = o365.audit.LogonError, Esql.o365_audit_ExtendedProperties_RequestType_lower = to_lower(o365.audit.ExtendedProperties.RequestType) | where event.dataset == "o365.audit" and event.category == "authentication" and event.provider in ("AzureActiveDirectory", "Exchange") and event.action in ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword") and Esql.o365_audit_ExtendedProperties_RequestType_lower rlike "(oauth.*||.*login.*)" and Esql.o365_audit_LogonError != "IdsLocked" and Esql.o365_audit_LogonError not in ( "EntitlementGrantsNotFound", "UserStrongAuthEnrollmentRequired", "UserStrongAuthClientAuthNRequired", "InvalidReplyTo", "SsoArtifactExpiredDueToConditionalAccess", "PasswordResetRegistrationRequiredInterrupt", "SsoUserAccountNotFoundInResourceTenant", "UserStrongAuthExpired", "CmsiInterrupt" ) and Esql_priv.o365_audit_UserId_lower != "not available" and o365.audit.Target.Type in ("0", "2", "6", "10") | stats Esql.o365_audit_UserId_lower_count_distinct = count_distinct(Esql_priv.o365_audit_UserId_lower), Esql_priv.o365_audit_UserId_lower_values = values(Esql_priv.o365_audit_UserId_lower), Esql.o365_audit_LogonError_values = values(Esql.o365_audit_LogonError), Esql.o365_audit_LogonError_count_distinct = count_distinct(Esql.o365_audit_LogonError), Esql.o365_audit_ExtendedProperties_RequestType_values = values(Esql.o365_audit_ExtendedProperties_RequestType_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_geo_country_name_values = values(source.geo.country_name), Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name), Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name), Esql.timestamp_first_seen = min(@timestamp), Esql.timestamp_last_seen = max(@timestamp), Esql.event_count = count(*) by Esql.time_window_date_trunc | eval Esql.event_duration_seconds = date_diff("seconds", Esql.timestamp_first_seen, Esql.timestamp_last_seen), Esql.brute_force_type = case( Esql.o365_audit_UserId_lower_count_distinct >= 15 and Esql.o365_audit_LogonError_count_distinct == 1 and Esql.event_count >= 10 and Esql.event_duration_seconds <= 1800, "password_spraying", Esql.o365_audit_UserId_lower_count_distinct >= 8 and Esql.event_count >= 15 and Esql.o365_audit_LogonError_count_distinct <= 3 and Esql.source_ip_count_distinct <= 5 and Esql.event_duration_seconds <= 600, "credential_stuffing", Esql.o365_audit_UserId_lower_count_distinct == 1 and Esql.o365_audit_LogonError_count_distinct == 1 and Esql.event_count >= 20 and Esql.event_duration_seconds <= 300, "password_guessing", "other" ) | keep Esql.time_window_date_trunc, Esql.o365_audit_UserId_lower_count_distinct, Esql_priv.o365_audit_UserId_lower_values, Esql.o365_audit_LogonError_values, Esql.o365_audit_LogonError_count_distinct, Esql.o365_audit_ExtendedProperties_RequestType_values, Esql.source_ip_values, Esql.source_ip_count_distinct, Esql.source_as_organization_name_values, Esql.source_geo_country_name_values, Esql.source_geo_country_name_count_distinct, Esql.source_as_organization_name_count_distinct, Esql.timestamp_first_seen, Esql.timestamp_last_seen, Esql.event_duration_seconds, Esql.event_count, Esql.brute_force_type | where Esql.brute_force_type != "other"
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/