Microsoft Entra ID Sign-In Brute Force Activity

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

Microsoft Entra ID Sign-In Brute Force Activity

edit

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.

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: 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: 2

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

Investigating Microsoft Entra ID Sign-In Brute Force Activity

This rule detects brute-force authentication activity in Entra ID sign-in logs. It classifies failed sign-in attempts into behavior types such as password spraying, credential stuffing, or password guessing. The classification (bf_type) helps prioritize triage and incident response.

Possible investigation steps

  • Review bf_type: Determines the brute-force technique being used (password_spraying, credential_stuffing, or password_guessing).
  • Examine user_id_list: Identify if high-value accounts (e.g., administrators, service principals, federated identities) are being targeted.
  • Review login_errors: Repetitive error types like "Invalid Grant" or "User Not Found" suggest automated attacks.
  • Check ip_list and source_orgs: Investigate if the activity originates from suspicious infrastructure (VPNs, hosting providers, etc.).
  • Validate unique_ips and countries: Geographic diversity and IP volume may indicate distributed or botnet-based attacks.
  • Compare total_attempts vs duration_seconds: High rate of failures in a short time period implies automation.
  • Analyze user_agent.original and device_detail_browser: User agents like curl, Python, or generic libraries may indicate scripting tools.
  • Investigate client_app_display_name and incoming_token_type: Detect potential abuse of legacy or unattended login mechanisms.
  • Inspect target_resource_display_name: Understand what application or resource the attacker is trying to access.
  • Pivot using session_id and device_detail_device_id: Determine if a device is targeting multiple accounts.
  • Review conditional_access_status: If not enforced, ensure Conditional Access policies are scoped correctly.

False positive analysis

  • Legitimate automation (e.g., misconfigured scripts, sync processes) can trigger repeated failures.
  • Internal red team activity or penetration tests may mimic brute-force behaviors.
  • Certain service accounts or mobile clients may generate repetitive sign-in noise if not properly configured.

Response and remediation

  • Notify your identity security team for further analysis.
  • Investigate and lock or reset impacted accounts if compromise is suspected.
  • Block offending IPs or ASNs at the firewall, proxy, or using Conditional Access.
  • Confirm MFA and Conditional Access are enforced for all user types.
  • Audit targeted accounts for credential reuse across services.
  • Implement account lockout or throttling for failed sign-in attempts where possible.

Rule query

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

Framework: MITRE ATT&CKTM