Microsoft Entra ID Sign-In Brute Force Activity

Last updated 3 days ago on 2025-07-10
Created 10 months ago on 2024-09-06

About

Identifies potential brute-force attacks targeting 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 applications integrated with Entra ID or to compromise valid user accounts.
Tags
Domain: CloudDomain: IdentityData Source: AzureData Source: Entra IDData Source: Entra ID Sign-in 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 or have misconfigured authentication settings 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

azure(opens in a new tab or window)

Query
FROM logs-azure.signinlogs*

// Define a time window for grouping and maintain the original event timestamp
| EVAL
    time_window = DATE_TRUNC(15 minutes, @timestamp),
    event_time = @timestamp

// Filter relevant failed authentication events with specific error codes
| WHERE event.dataset == "azure.signinlogs"
    AND event.category == "authentication"
    AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
    AND event.outcome == "failure"
    AND azure.signinlogs.properties.authentication_requirement == "singleFactorAuthentication"
    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"
    AND source.`as`.organization.name != "MICROSOFT-CORP-MSN-AS-BLOCK"

// Aggregate statistics for behavioral pattern analysis
| STATS
    authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
    client_app_id = VALUES(azure.signinlogs.properties.app_id),
    client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
    target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
    target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
    conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
    device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
    device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
    device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system),
    incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
    risk_state = VALUES(azure.signinlogs.properties.risk_state),
    session_id = VALUES(azure.signinlogs.properties.session_id),
    user_id = VALUES(azure.signinlogs.properties.user_id),
    user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
    result_description = VALUES(azure.signinlogs.result_description),
    result_signature = VALUES(azure.signinlogs.result_signature),
    result_type = VALUES(azure.signinlogs.result_type),

    unique_users = COUNT_DISTINCT(azure.signinlogs.properties.user_id),
    user_id_list = VALUES(azure.signinlogs.properties.user_id),
    login_errors = VALUES(azure.signinlogs.result_description),
    unique_login_errors = COUNT_DISTINCT(azure.signinlogs.result_description),
    error_codes = VALUES(azure.signinlogs.properties.status.error_code),
    unique_error_codes = COUNT_DISTINCT(azure.signinlogs.properties.status.error_code),
    request_types = VALUES(azure.signinlogs.properties.incoming_token_type),
    app_names = VALUES(azure.signinlogs.properties.app_display_name),
    ip_list = VALUES(source.ip),
    unique_ips = COUNT_DISTINCT(source.ip),
    source_orgs = VALUES(source.`as`.organization.name),
    countries = VALUES(source.geo.country_name),
    unique_country_count = COUNT_DISTINCT(source.geo.country_name),
    unique_asn_orgs = COUNT_DISTINCT(source.`as`.organization.name),
    first_seen = MIN(@timestamp),
    last_seen = MAX(@timestamp),
    total_attempts = COUNT()
BY time_window

// Determine brute force behavior type based on statistical thresholds
| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    bf_type = CASE(
        // Many users, relatively few distinct login errors, distributed over multiple IPs (but not too many),
        // and happens quickly. Often bots using leaked credentials.
        unique_users >= 10 AND total_attempts >= 30 AND unique_login_errors <= 3
            AND unique_ips >= 5
            AND duration_seconds <= 600
            AND unique_users > unique_ips,
        "credential_stuffing",

        // One password against many users. Single error (e.g., "InvalidPassword"), not necessarily fast.
        unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 15 AND duration_seconds <= 1800,
        "password_spraying",

        // One user targeted repeatedly (same error), OR extremely noisy pattern from many IPs.
        (unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300)
            OR (unique_users <= 3 AND unique_ips > 30 AND total_attempts >= 100),
        "password_guessing",

        // everything else
        "other"
    )

// Only keep columns necessary for detection output/reporting
| KEEP
    time_window, bf_type, duration_seconds, total_attempts, first_seen, last_seen,
    unique_users, user_id_list, login_errors, unique_login_errors,
    unique_error_codes, error_codes, request_types, app_names,
    ip_list, unique_ips, source_orgs, countries,
    unique_country_count, unique_asn_orgs,
    authentication_requirement, client_app_id, client_app_display_name,
    target_resource_id, target_resource_display_name, conditional_access_status,
    device_detail_browser, device_detail_device_id, device_detail_operating_system,
    incoming_token_type, risk_state, session_id, user_id,
    user_principal_name, result_description, result_signature, result_type

// Remove anything not classified as credential attack activity
| WHERE bf_type != "other"

Install detection rules in Elastic Security

Detect Microsoft Entra ID Sign-In Brute Force Activity 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).