Microsoft 365 Brute Force via Entra ID Sign-Ins

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Microsoft 365 Brute Force via Entra ID Sign-Ins

edit

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.

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:

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

edit

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.

Rule query

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