Potential Microsoft 365 User Account Brute Force

Last updated a month ago on 2025-07-16
Created 5 years ago on 2020-11-30

About

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.
Tags
Domain: CloudDomain: SaaSData Source: Microsoft 365Data Source: Microsoft 365 Audit LogsUse Case: Identity and Access AuditUse Case: Threat DetectionTactic: Credential AccessLanguage: esql
Severity
medium
Risk Score
47
MITRE ATT&CK™

Credential Access (TA0006)(opens in a new tab or window)

False Positive Examples
Automated processes that attempt to authenticate using expired credentials and unbounded retries may lead to false positives.
License
Elastic License v2(opens in a new tab or window)

Definition

Integration Pack
Prebuilt Security Detection Rules
Related Integrations

o365(opens in a new tab or window)

Query
from 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"

Install detection rules in Elastic Security

Detect Potential Microsoft 365 User Account Brute Force in the Elastic Security detection engine by installing this rule into your Elastic Stack.

To setup this rule, check out the installation guide for Prebuilt Security Detection Rules(opens in a new tab or window).