Detection rules › Elastic

Entra ID MFA TOTP Brute Force Attempted

Status
production
Severity
medium
Time window
9m
Group by
user.id
Author
Elastic
Source
github.com/elastic/detection-rules

Identifies brute force attempts against Azure Entra multi-factor authentication (MFA) Time-based One-Time Password (TOTP) verification codes. This rule detects high frequency failed TOTP code attempts for a single user in a short time-span with a high number of distinct session IDs. Adversaries may programmatically attemopt to brute-force TOTP codes by generating several sessions and attempt to guess the correct code.

MITRE ATT&CK coverage

TacticTechniques
Credential AccessT1110.001 Brute Force: Password Guessing

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body elastic

[metadata]
creation_date = "2024/12/11"
integration = ["azure"]
maturity = "production"
updated_date = "2026/04/10"

[rule]
author = ["Elastic"]
description = """
Identifies brute force attempts against Azure Entra multi-factor authentication (MFA) Time-based One-Time Password
(TOTP) verification codes. This rule detects high frequency failed TOTP code attempts for a single user in a short
time-span with a high number of distinct session IDs. Adversaries may programmatically attemopt to brute-force TOTP
codes by generating several sessions and attempt to guess the correct code.
"""
false_positives = [
    """
    Based on the high-frequency threshold, it would be unlikely for a legitimate user to exceed the threshold for failed
    TOTP code attempts in a short time-span over multiple sessions.
    """,
]
from = "now-9m"
language = "esql"
license = "Elastic License v2"
name = "Entra ID MFA TOTP Brute Force Attempted"
note = """## Triage and analysis

### Investigating Entra ID MFA TOTP Brute Force Attempted

This rule detects brute force attempts against Azure Entra multi-factor authentication (MFA) Time-based One-Time Password (TOTP) verification codes. It identifies high-frequency failed TOTP code attempts for a single user in a short time-span with a high number of distinct session IDs. Adversaries may programmatically attempt to brute-force TOTP codes by generating several sessions and attempting to guess the correct code.

#### Possible Investigation Steps:

    - Check the source addresses associated with the failed TOTP attempts.
    - Determine if the source IP address is consistent with the user’s typical login locations.
    - Look for unusual geographic patterns or anomalous IP addresses (e.g., proxies, VPNs, or locations outside the user’s normal activity).
    - Review the error code associated with the failed attempts. This can help identify if the failures are due to incorrect TOTP codes or other issues.
    - Verify that that auth metho reported is `OAth` as it indicates the use of TOTP codes.
    - Pivot into signin logs for the target user and check if auth via TOTP was successful which would indicate a successful brute force attempt.
    - Review conditional access policies applied to the user or group as reported by the sign-in logs.
    - Analyze the client application ID and display name to determine if the attempts are coming from a legitimate application or a potentially malicious script.
        - Adversaries may use legitimate FOCI applications to bypass security controls or make login attempts appear legitimate.
    - Review the resource ID access is being attempted against such as MyApps, Microsoft Graph, or other resources. This can help identify if the attempts are targeting specific applications or services.
    - The correlation IDs or session IDs can be used to trace the authentication attempts across different logs or systems. Note that for this specific behavior, unique session ID count is high and could be challenging to correlate.

#### False Positive Analysis:

    - Verify if the failed attempts could result from the user’s unfamiliarity with TOTP codes or issues with device synchronization.
    - Check if the user recently switched MFA methods or devices, which could explain multiple failures.
    - Determine if this is whitebox testing or a developer testing MFA integration.

#### Response and Remediation:

    - If proven malicious, lock the affected account temporarily to prevent further unauthorized attempts.
    - Notify the user of suspicious activity and validate their access to the account.
    - Reset passwords and MFA settings for the affected user to prevent unauthorized access while communicating with the user.
    - Ensure conditional access policies are configured to monitor and restrict anomalous login behavior.
    - Consider a different MFA method or additional security controls to prevent future bypass attempts.
    - Implement additional monitoring to track high-frequency authentication failures across the environment.
    - Audit historical logs for similar patterns involving other accounts to identify broader threats.
    - Provide guidance on the secure use of MFA and the importance of recognizing and reporting suspicious activity.
"""
references = [
    "https://www.oasis.security/resources/blog/oasis-security-research-team-discovers-microsoft-azure-mfa-bypass",
    "https://learn.microsoft.com/en-us/entra/identity/",
    "https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-ins",
]
risk_score = 47
rule_id = "3fac01b2-b811-11ef-b25b-f661ea17fbce"
setup = """#### Required Entra ID Sign-In Logs
This rule requires the Entra ID sign-in logs via the Azure integration be enabled. In Entra ID, sign-in logs must be enabled and streaming to the Event Hub used for the Entra ID logs integration.
"""
severity = "medium"
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",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-azure.signinlogs-* metadata _id, _version, _index

| where
    // filter for Entra Sign-in Logs
    data_stream.dataset == "azure.signinlogs"
    and azure.signinlogs.operation_name == "Sign-in activity"
    and azure.signinlogs.properties.user_type == "Member"

    // filter for MFA attempts with OATH conditional access attempts or TOTP
    and azure.signinlogs.properties.mfa_detail.auth_method == "OATH verification code"

    // filter on failures only from brute-force attempts
    and (
            (
                azure.signinlogs.result_signature == "FAILURE" and
                azure.signinlogs.result_description == "Authentication failed during strong authentication request."
            ) or azure.signinlogs.properties.status.error_code == 500121
        )

| stats
    Esql.event_count = count(*),
    Esql.azure_signinlogs_properties_session_id_count_distinct = count_distinct(azure.signinlogs.properties.session_id),
    Esql.source_address_values = values(source.address),
    Esql.azure_tenant_id_valuues = values(azure.tenant_id),
    Esql_priv.azure_identity_values = values(azure.signinlogs.identity),
    Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
    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_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
    Esql.azure_signinlogs_properties_authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),
    Esql.azure_signinlogs_properties_client_app_used_values = values(azure.signinlogs.properties.client_app_used),
    Esql.azure_signinlogs_properties_client_credential_type_values = values(azure.signinlogs.properties.client_credential_type),
    Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
    Esql.azure_signinlogs_properties_correlation_id_values = values(azure.signinlogs.properties.correlation_id),
    Esql.azure_signinlogs_properties_is_interactive_values = values(azure.signinlogs.properties.is_interactive),
    Esql.azure_signinlogs_properties_mfa_detail_auth_method_values = values(azure.signinlogs.properties.mfa_detail.auth_method),
    Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
    Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
    Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
    Esql.azure_signinlogs_properties_risk_detail_values = values(azure.signinlogs.properties.risk_detail),
    Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
    Esql.azure_signinlogs_properties_original_request_id_values = values(azure.signinlogs.properties.original_request_id),
    Esql.user_id_values = values(user.id)
    by user.id

| where Esql.event_count >= 20 and Esql.azure_signinlogs_properties_session_id_count_distinct >= 10

| keep
    Esql.event_count,
    Esql.azure_signinlogs_properties_session_id_count_distinct,
    Esql.source_address_values,
    Esql.azure_tenant_id_valuues,
    Esql_priv.azure_identity_values,
    Esql_priv.azure_signinlogs_properties_user_principal_name_values,
    Esql.azure_signinlogs_properties_app_id_values,
    Esql.azure_signinlogs_properties_app_display_name_values,
    Esql.azure_signinlogs_properties_authentication_requirement_values,
    Esql.azure_signinlogs_properties_authentication_protocol_values,
    Esql.azure_signinlogs_properties_client_app_used_values,
    Esql.azure_signinlogs_properties_client_credential_type_values,
    Esql.azure_signinlogs_properties_conditional_access_status_values,
    Esql.azure_signinlogs_properties_correlation_id_values,
    Esql.azure_signinlogs_properties_is_interactive_values,
    Esql.azure_signinlogs_properties_mfa_detail_auth_method_values,
    Esql.azure_signinlogs_properties_resource_display_name_values,
    Esql.azure_signinlogs_properties_resource_id_values,
    Esql.azure_signinlogs_properties_risk_state_values,
    Esql.azure_signinlogs_properties_risk_detail_values,
    Esql.azure_signinlogs_properties_status_error_code_values,
    Esql.azure_signinlogs_properties_original_request_id_values,
    Esql.user_id_values
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[[rule.threat.technique.subtechnique]]
id = "T1110.001"
name = "Password Guessing"
reference = "https://attack.mitre.org/techniques/T1110/001/"



[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"

Stages and Predicates

Stage 1: from

from logs-azure.signinlogs-* metadata _id, _version, _index

Stage 2: where

| where
    data_stream.dataset == "azure.signinlogs"
    and azure.signinlogs.operation_name == "Sign-in activity"
    and azure.signinlogs.properties.user_type == "Member"
    and azure.signinlogs.properties.mfa_detail.auth_method == "OATH verification code"
    and (
            (
                azure.signinlogs.result_signature == "FAILURE" and
                azure.signinlogs.result_description == "Authentication failed during strong authentication request."
            ) or azure.signinlogs.properties.status.error_code == 500121
        )

Stage 3: stats

| stats
    Esql.event_count = count(*),
    Esql.azure_signinlogs_properties_session_id_count_distinct = count_distinct(azure.signinlogs.properties.session_id),
    Esql.source_address_values = values(source.address),
    Esql.azure_tenant_id_valuues = values(azure.tenant_id),
    Esql_priv.azure_identity_values = values(azure.signinlogs.identity),
    Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
    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_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
    Esql.azure_signinlogs_properties_authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),
    Esql.azure_signinlogs_properties_client_app_used_values = values(azure.signinlogs.properties.client_app_used),
    Esql.azure_signinlogs_properties_client_credential_type_values = values(azure.signinlogs.properties.client_credential_type),
    Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
    Esql.azure_signinlogs_properties_correlation_id_values = values(azure.signinlogs.properties.correlation_id),
    Esql.azure_signinlogs_properties_is_interactive_values = values(azure.signinlogs.properties.is_interactive),
    Esql.azure_signinlogs_properties_mfa_detail_auth_method_values = values(azure.signinlogs.properties.mfa_detail.auth_method),
    Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
    Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
    Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
    Esql.azure_signinlogs_properties_risk_detail_values = values(azure.signinlogs.properties.risk_detail),
    Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
    Esql.azure_signinlogs_properties_original_request_id_values = values(azure.signinlogs.properties.original_request_id),
    Esql.user_id_values = values(user.id)
    by user.id

Stage 4: where

| where Esql.event_count >= 20 and Esql.azure_signinlogs_properties_session_id_count_distinct >= 10

Stage 5: keep

| keep
    Esql.event_count,
    Esql.azure_signinlogs_properties_session_id_count_distinct,
    Esql.source_address_values,
    Esql.azure_tenant_id_valuues,
    Esql_priv.azure_identity_values,
    Esql_priv.azure_signinlogs_properties_user_principal_name_values,
    Esql.azure_signinlogs_properties_app_id_values,
    Esql.azure_signinlogs_properties_app_display_name_values,
    Esql.azure_signinlogs_properties_authentication_requirement_values,
    Esql.azure_signinlogs_properties_authentication_protocol_values,
    Esql.azure_signinlogs_properties_client_app_used_values,
    Esql.azure_signinlogs_properties_client_credential_type_values,
    Esql.azure_signinlogs_properties_conditional_access_status_values,
    Esql.azure_signinlogs_properties_correlation_id_values,
    Esql.azure_signinlogs_properties_is_interactive_values,
    Esql.azure_signinlogs_properties_mfa_detail_auth_method_values,
    Esql.azure_signinlogs_properties_resource_display_name_values,
    Esql.azure_signinlogs_properties_resource_id_values,
    Esql.azure_signinlogs_properties_risk_state_values,
    Esql.azure_signinlogs_properties_risk_detail_values,
    Esql.azure_signinlogs_properties_status_error_code_values,
    Esql.azure_signinlogs_properties_original_request_id_values,
    Esql.user_id_values

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Esql.event_countKEEP Esql.event_count
Esql.azure_signinlogs_properties_session_id_count_distinctKEEP Esql.azure_signinlogs_properties_session_id_count_distinct
Esql.source_address_valuesKEEP Esql.source_address_values
Esql.azure_tenant_id_valuuesKEEP Esql.azure_tenant_id_valuues
Esql_priv.azure_identity_valuesKEEP Esql_priv.azure_identity_values
Esql_priv.azure_signinlogs_properties_user_principal_name_valuesKEEP Esql_priv.azure_signinlogs_properties_user_principal_name_values
Esql.azure_signinlogs_properties_app_id_valuesKEEP Esql.azure_signinlogs_properties_app_id_values
Esql.azure_signinlogs_properties_app_display_name_valuesKEEP Esql.azure_signinlogs_properties_app_display_name_values
Esql.azure_signinlogs_properties_authentication_requirement_valuesKEEP Esql.azure_signinlogs_properties_authentication_requirement_values
Esql.azure_signinlogs_properties_authentication_protocol_valuesKEEP Esql.azure_signinlogs_properties_authentication_protocol_values
Esql.azure_signinlogs_properties_client_app_used_valuesKEEP Esql.azure_signinlogs_properties_client_app_used_values
Esql.azure_signinlogs_properties_client_credential_type_valuesKEEP Esql.azure_signinlogs_properties_client_credential_type_values
Esql.azure_signinlogs_properties_conditional_access_status_valuesKEEP Esql.azure_signinlogs_properties_conditional_access_status_values
Esql.azure_signinlogs_properties_correlation_id_valuesKEEP Esql.azure_signinlogs_properties_correlation_id_values
Esql.azure_signinlogs_properties_is_interactive_valuesKEEP Esql.azure_signinlogs_properties_is_interactive_values
Esql.azure_signinlogs_properties_mfa_detail_auth_method_valuesKEEP Esql.azure_signinlogs_properties_mfa_detail_auth_method_values
Esql.azure_signinlogs_properties_resource_display_name_valuesKEEP Esql.azure_signinlogs_properties_resource_display_name_values
Esql.azure_signinlogs_properties_resource_id_valuesKEEP Esql.azure_signinlogs_properties_resource_id_values
Esql.azure_signinlogs_properties_risk_state_valuesKEEP Esql.azure_signinlogs_properties_risk_state_values
Esql.azure_signinlogs_properties_risk_detail_valuesKEEP Esql.azure_signinlogs_properties_risk_detail_values
Esql.azure_signinlogs_properties_status_error_code_valuesKEEP Esql.azure_signinlogs_properties_status_error_code_values
Esql.azure_signinlogs_properties_original_request_id_valuesKEEP Esql.azure_signinlogs_properties_original_request_id_values
Esql.user_id_valuesKEEP Esql.user_id_values