Detection rules › Elastic

Entra ID OAuth Flow by Microsoft Authentication Broker to Device Registration Service (DRS)

Status
production
Severity
high
Time window
61m
Group by
Esql.time_window_date_trunc, azure.signinlogs.properties.session_id, azure.signinlogs.properties.user_principal_name
Author
Elastic
Source
github.com/elastic/detection-rules

Identifies separate OAuth authorization flows in Microsoft Entra ID where the same user principal and session ID are observed across multiple IP addresses within a 5-minute window. These flows involve the Microsoft Authentication Broker (MAB) as the client application and the Device Registration Service (DRS) as the target resource. This pattern is highly indicative of OAuth phishing activity, where an adversary crafts a legitimate Microsoft login URL to trick a user into completing authentication and sharing the resulting authorization code, which is then exchanged for an access and refresh token by the attacker.

MITRE ATT&CK coverage

Rule body elastic

[metadata]
creation_date = "2025/04/30"
integration = ["azure"]
maturity = "production"
updated_date = "2026/04/10"

[rule]
author = ["Elastic"]
description = """
Identifies separate OAuth authorization flows in Microsoft Entra ID where the same user principal and session ID are
observed across multiple IP addresses within a 5-minute window. These flows involve the Microsoft Authentication Broker
(MAB) as the client application and the Device Registration Service (DRS) as the target resource. This pattern is highly
indicative of OAuth phishing activity, where an adversary crafts a legitimate Microsoft login URL to trick a user into
completing authentication and sharing the resulting authorization code, which is then exchanged for an access and
refresh token by the attacker.
"""
false_positives = [
    """
    Legitimate device registrations using Microsoft Authentication Broker may occur during corporate enrollment
    scenarios or bulk provisioning, but it is uncommon for multiple source IPs to register the same identity across
    Microsoft Graph, Device Registration Service (DRS), and Azure Active Directory (AAD) in a short time span.
    """,
]
from = "now-61m"
interval = "60m"
language = "esql"
license = "Elastic License v2"
name = "Entra ID OAuth Flow by Microsoft Authentication Broker to Device Registration Service (DRS)"
note = """## Triage and analysis

### Investigating Entra ID OAuth Flow by Microsoft Authentication Broker to Device Registration Service (DRS)

This rule identifies potential OAuth phishing behavior in Microsoft Entra ID where two OAuth authorization flows are observed in quick succession, sharing the same user principal and session ID but originating from different IP addresses. The client application is the Microsoft Authentication Broker, and the target resource is the Device Registration Service (DRS). This pattern is indicative of adversaries attempting to phish targets for OAuth sessions by tricking users into authenticating through a crafted URL, which then allows the attacker to obtain an authorization code and exchange it for access and refresh tokens.

### Possible Investigation Steps:

- `target`: The user principal name targeted by the authentication broker. Investigate whether this user has recently registered a device, signed in from new IPs, or had password resets or MFA changes.
- `session_id`: Used to correlate all events in the OAuth flow. All sign-ins in the alert share the same session, suggesting shared or hijacked state.
- `unique_token_id`: Lists tokens generated in the flow. If multiple IDs exist in the same session, this indicates token issuance from different locations.
- `source_ip`, `city_name`, `country_name`, `region_name`: Review the IPs and geolocations involved. A mismatch in geographic origin within minutes can signal adversary involvement.
- `user_agent`: Conflicting user agents (e.g., `python-requests` and `Chrome`) suggest one leg of the session was scripted or automated.
- `os`: If multiple operating systems are observed in the same short session (e.g., macOS and Windows), this may suggest activity from different environments.
- `incoming_token_type`: Look for values like `"none"` or `"refreshToken"` that can indicate abnormal or re-authenticated activity.
- `token_session_status`: A value of `"unbound"` means the issued token is not tied to a device or CAE session, making it reusable from another IP.
- `conditional_access_status`: If this is `"notApplied"`, it may indicate that expected access policies were not enforced.
- `auth_count`: Number of events in the session. More than one indicates the session was reused within the time window.
- `target_time_window`: Use this to pivot into raw sign-in logs to review the exact sequence and timing of the activity.
- Search `azure.auditlogs` for any device join or registration activity around the `target_time_window`.
- Review `azure.identityprotection` logs for anonymized IPs, impossible travel, or token replay alerts.
- Search for other activity from the same IPs across all users to identify horizontal movement.

### False Positive Analysis

- A legitimate device join from a user switching networks (e.g., mobile hotspot to Wi-Fi) could explain multi-IP usage.
- Some identity management agents or EDR tools may use MAB for background device registration flows.
- Developers or IT administrators may access DRS across environments when testing.

### Response and Remediation

- If confirmed unauthorized, revoke all refresh tokens for the user and disable any suspicious registered devices.
- Notify the user and verify if the authentication or device join was expected.
- Review Conditional Access policies for the Microsoft Authentication Broker (`29d9ed98-a469-4536-ade2-f981bc1d605e`) to ensure enforcement of MFA and device trust.
- Consider restricting token-based reauthentication from anonymized infrastructure or unusual user agents.
- Continue monitoring for follow-on activity, such as privilege escalation, token misuse, or lateral movement.
"""
references = [
    "https://www.volexity.com/blog/2025/04/22/phishing-for-codes-russian-threat-actors-target-microsoft-365-oauth-workflows/",
    "https://github.com/dirkjanm/ROADtools",
    "https://dirkjanm.io/phishing-for-microsoft-entra-primary-refresh-tokens/",
]
risk_score = 73
rule_id = "375132c6-25d5-11f0-8745-f661ea17fbcd"
setup = """#### Required Microsoft Entra ID Sign-In Logs
This rule requires the Microsoft Entra ID Sign-In Logs integration be enabled and configured to collect sign-in logs. In Entra ID, sign-in logs must be enabled and streaming to the Event Hub used for the Azure integration.
"""
severity = "high"
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",
    "Resources: Investigation Guide",
    "Tactic: Initial Access",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-azure.signinlogs-* metadata _id, _version, _index
| where
    data_stream.dataset == "azure.signinlogs" and
    event.outcome == "success" and
    azure.signinlogs.properties.user_type == "Member" and
    azure.signinlogs.identity is not null and
    azure.signinlogs.properties.user_principal_name is not null and
    source.address is not null and
    azure.signinlogs.properties.app_id == "29d9ed98-a469-4536-ade2-f981bc1d605e" and  // MAB
    azure.signinlogs.properties.resource_id == "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9"  // DRS

| eval
    Esql.time_window_date_trunc = date_trunc(30 minutes, @timestamp),
    Esql.azure_signinlogs_properties_session_id = azure.signinlogs.properties.session_id,
    Esql.is_browser_case = case(
        to_lower(azure.signinlogs.properties.device_detail.browser) rlike "(chrome|firefox|edge|safari).*", 1, 0
    )

| stats
    Esql_priv.azure_signinlogs_properties_user_display_name_values = values(azure.signinlogs.properties.user_display_name),
    Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
    Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id),
    Esql.azure_signinlogs_properties_unique_token_identifier_values = values(azure.signinlogs.properties.unique_token_identifier),

    Esql.source_geo_city_name_values = values(source.geo.city_name),
    Esql.source_geo_country_name_values = values(source.geo.country_name),
    Esql.source_geo_region_name_values = values(source.geo.region_name),
    Esql.source_address_values = values(source.address),
    Esql.source_address_count_distinct = count_distinct(source.address),
    Esql.source_as_organization_name_values = values(source.`as`.organization.name),

    Esql.azure_signinlogs_properties_authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),
    Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
    Esql.azure_signinlogs_properties_is_interactive_values = values(azure.signinlogs.properties.is_interactive),

    Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type),
    Esql.azure_signinlogs_properties_token_protection_status_details_sign_in_session_status_values = values(azure.signinlogs.properties.token_protection_status_details.sign_in_session_status),
    Esql.azure_signinlogs_properties_session_id_count_distinct = count_distinct(azure.signinlogs.properties.session_id),
    Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name),
    Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
    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_app_owner_tenant_id_values = values(azure.signinlogs.properties.app_owner_tenant_id),
    Esql.azure_signinlogs_properties_resource_owner_tenant_id_values = values(azure.signinlogs.properties.resource_owner_tenant_id),

    Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
    Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
    Esql.azure_signinlogs_properties_risk_level_aggregated_values = values(azure.signinlogs.properties.risk_level_aggregated),

    Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser),
    Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.is_browser_case_max = max(Esql.is_browser_case),

    Esql.event_count = count(*)
  by
    Esql.time_window_date_trunc,
    azure.signinlogs.properties.user_principal_name,
    azure.signinlogs.properties.session_id

| keep
    Esql.time_window_date_trunc,
    Esql_priv.azure_signinlogs_properties_user_display_name_values,
    Esql_priv.azure_signinlogs_properties_user_principal_name_values,
    Esql.azure_signinlogs_properties_session_id_values,
    Esql.azure_signinlogs_properties_unique_token_identifier_values,
    Esql.source_geo_city_name_values,
    Esql.source_geo_country_name_values,
    Esql.source_geo_region_name_values,
    Esql.source_address_values,
    Esql.source_address_count_distinct,
    Esql.source_as_organization_name_values,
    Esql.azure_signinlogs_properties_authentication_protocol_values,
    Esql.azure_signinlogs_properties_authentication_requirement_values,
    Esql.azure_signinlogs_properties_is_interactive_values,
    Esql.azure_signinlogs_properties_incoming_token_type_values,
    Esql.azure_signinlogs_properties_token_protection_status_details_sign_in_session_status_values,
    Esql.azure_signinlogs_properties_session_id_count_distinct,
    Esql.azure_signinlogs_properties_app_display_name_values,
    Esql.azure_signinlogs_properties_app_id_values,
    Esql.azure_signinlogs_properties_resource_id_values,
    Esql.azure_signinlogs_properties_resource_display_name_values,
    Esql.azure_signinlogs_properties_app_owner_tenant_id_values,
    Esql.azure_signinlogs_properties_resource_owner_tenant_id_values,
    Esql.azure_signinlogs_properties_conditional_access_status_values,
    Esql.azure_signinlogs_properties_risk_state_values,
    Esql.azure_signinlogs_properties_risk_level_aggregated_values,
    Esql.azure_signinlogs_properties_device_detail_browser_values,
    Esql.azure_signinlogs_properties_device_detail_operating_system_values,
    Esql.user_agent_original_values,
    Esql.is_browser_case_max,
    Esql.event_count

| where
    Esql.source_address_count_distinct >= 2 and
    Esql.azure_signinlogs_properties_session_id_count_distinct == 1 and
    Esql.is_browser_case_max >= 1 and
    Esql.event_count >= 2
'''


[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1078"
name = "Valid Accounts"
reference = "https://attack.mitre.org/techniques/T1078/"

[[rule.threat.technique.subtechnique]]
id = "T1078.004"
name = "Cloud Accounts"
reference = "https://attack.mitre.org/techniques/T1078/004/"

[[rule.threat.technique]]
id = "T1566"
name = "Phishing"
reference = "https://attack.mitre.org/techniques/T1566/"

[[rule.threat.technique.subtechnique]]
id = "T1566.002"
name = "Spearphishing Link"
reference = "https://attack.mitre.org/techniques/T1566/002/"

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

[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1528"
name = "Steal Application Access Token"
reference = "https://attack.mitre.org/techniques/T1528/"

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

[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1550"
name = "Use Alternate Authentication Material"
reference = "https://attack.mitre.org/techniques/T1550/"

[[rule.threat.technique.subtechnique]]
id = "T1550.001"
name = "Application Access Token"
reference = "https://attack.mitre.org/techniques/T1550/001/"

[rule.threat.tactic]
id = "TA0005"
name = "Defense Evasion"
reference = "https://attack.mitre.org/tactics/TA0005/"

Stages and Predicates

Stage 1: from

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

Stage 2: where

| where
    data_stream.dataset == "azure.signinlogs" and
    event.outcome == "success" and
    azure.signinlogs.properties.user_type == "Member" and
    azure.signinlogs.identity is not null and
    azure.signinlogs.properties.user_principal_name is not null and
    source.address is not null and
    azure.signinlogs.properties.app_id == "29d9ed98-a469-4536-ade2-f981bc1d605e" and
    azure.signinlogs.properties.resource_id == "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9"

Stage 3: eval

| eval
    Esql.time_window_date_trunc = date_trunc(30 minutes, @timestamp),
    Esql.azure_signinlogs_properties_session_id = azure.signinlogs.properties.session_id,
    Esql.is_browser_case = case(
        to_lower(azure.signinlogs.properties.device_detail.browser) rlike "(chrome|firefox|edge|safari).*", 1, 0
    )
Esql.is_browser_case =
ifto_lower(azure.signinlogs.properties.device_detail.browser) rlike "(chrome|firefox|edge|safari).*"1
else0

Stage 4: stats

| stats
    Esql_priv.azure_signinlogs_properties_user_display_name_values = values(azure.signinlogs.properties.user_display_name),
    Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
    Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id),
    Esql.azure_signinlogs_properties_unique_token_identifier_values = values(azure.signinlogs.properties.unique_token_identifier),

    Esql.source_geo_city_name_values = values(source.geo.city_name),
    Esql.source_geo_country_name_values = values(source.geo.country_name),
    Esql.source_geo_region_name_values = values(source.geo.region_name),
    Esql.source_address_values = values(source.address),
    Esql.source_address_count_distinct = count_distinct(source.address),
    Esql.source_as_organization_name_values = values(source.`as`.organization.name),

    Esql.azure_signinlogs_properties_authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),
    Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
    Esql.azure_signinlogs_properties_is_interactive_values = values(azure.signinlogs.properties.is_interactive),

    Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type),
    Esql.azure_signinlogs_properties_token_protection_status_details_sign_in_session_status_values = values(azure.signinlogs.properties.token_protection_status_details.sign_in_session_status),
    Esql.azure_signinlogs_properties_session_id_count_distinct = count_distinct(azure.signinlogs.properties.session_id),
    Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name),
    Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
    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_app_owner_tenant_id_values = values(azure.signinlogs.properties.app_owner_tenant_id),
    Esql.azure_signinlogs_properties_resource_owner_tenant_id_values = values(azure.signinlogs.properties.resource_owner_tenant_id),

    Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
    Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
    Esql.azure_signinlogs_properties_risk_level_aggregated_values = values(azure.signinlogs.properties.risk_level_aggregated),

    Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser),
    Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.is_browser_case_max = max(Esql.is_browser_case),

    Esql.event_count = count(*)
  by
    Esql.time_window_date_trunc,
    azure.signinlogs.properties.user_principal_name,
    azure.signinlogs.properties.session_id

Stage 5: keep

| keep
    Esql.time_window_date_trunc,
    Esql_priv.azure_signinlogs_properties_user_display_name_values,
    Esql_priv.azure_signinlogs_properties_user_principal_name_values,
    Esql.azure_signinlogs_properties_session_id_values,
    Esql.azure_signinlogs_properties_unique_token_identifier_values,
    Esql.source_geo_city_name_values,
    Esql.source_geo_country_name_values,
    Esql.source_geo_region_name_values,
    Esql.source_address_values,
    Esql.source_address_count_distinct,
    Esql.source_as_organization_name_values,
    Esql.azure_signinlogs_properties_authentication_protocol_values,
    Esql.azure_signinlogs_properties_authentication_requirement_values,
    Esql.azure_signinlogs_properties_is_interactive_values,
    Esql.azure_signinlogs_properties_incoming_token_type_values,
    Esql.azure_signinlogs_properties_token_protection_status_details_sign_in_session_status_values,
    Esql.azure_signinlogs_properties_session_id_count_distinct,
    Esql.azure_signinlogs_properties_app_display_name_values,
    Esql.azure_signinlogs_properties_app_id_values,
    Esql.azure_signinlogs_properties_resource_id_values,
    Esql.azure_signinlogs_properties_resource_display_name_values,
    Esql.azure_signinlogs_properties_app_owner_tenant_id_values,
    Esql.azure_signinlogs_properties_resource_owner_tenant_id_values,
    Esql.azure_signinlogs_properties_conditional_access_status_values,
    Esql.azure_signinlogs_properties_risk_state_values,
    Esql.azure_signinlogs_properties_risk_level_aggregated_values,
    Esql.azure_signinlogs_properties_device_detail_browser_values,
    Esql.azure_signinlogs_properties_device_detail_operating_system_values,
    Esql.user_agent_original_values,
    Esql.is_browser_case_max,
    Esql.event_count

Stage 6: where

| where
    Esql.source_address_count_distinct >= 2 and
    Esql.azure_signinlogs_properties_session_id_count_distinct == 1 and
    Esql.is_browser_case_max >= 1 and
    Esql.event_count >= 2

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.time_window_date_truncKEEP Esql.time_window_date_trunc
Esql_priv.azure_signinlogs_properties_user_display_name_valuesKEEP Esql_priv.azure_signinlogs_properties_user_display_name_values
Esql_priv.azure_signinlogs_properties_user_principal_name_valuesKEEP Esql_priv.azure_signinlogs_properties_user_principal_name_values
Esql.azure_signinlogs_properties_session_id_valuesKEEP Esql.azure_signinlogs_properties_session_id_values
Esql.azure_signinlogs_properties_unique_token_identifier_valuesKEEP Esql.azure_signinlogs_properties_unique_token_identifier_values
Esql.source_geo_city_name_valuesKEEP Esql.source_geo_city_name_values
Esql.source_geo_country_name_valuesKEEP Esql.source_geo_country_name_values
Esql.source_geo_region_name_valuesKEEP Esql.source_geo_region_name_values
Esql.source_address_valuesKEEP Esql.source_address_values
Esql.source_address_count_distinctKEEP Esql.source_address_count_distinct
Esql.source_as_organization_name_valuesKEEP Esql.source_as_organization_name_values
Esql.azure_signinlogs_properties_authentication_protocol_valuesKEEP Esql.azure_signinlogs_properties_authentication_protocol_values
Esql.azure_signinlogs_properties_authentication_requirement_valuesKEEP Esql.azure_signinlogs_properties_authentication_requirement_values
Esql.azure_signinlogs_properties_is_interactive_valuesKEEP Esql.azure_signinlogs_properties_is_interactive_values
Esql.azure_signinlogs_properties_incoming_token_type_valuesKEEP Esql.azure_signinlogs_properties_incoming_token_type_values
Esql.azure_signinlogs_properties_token_protection_status_details_sign_in_session_status_valuesKEEP Esql.azure_signinlogs_properties_token_protection_status_details_sign_in_session_status_values
Esql.azure_signinlogs_properties_session_id_count_distinctKEEP Esql.azure_signinlogs_properties_session_id_count_distinct
Esql.azure_signinlogs_properties_app_display_name_valuesKEEP Esql.azure_signinlogs_properties_app_display_name_values
Esql.azure_signinlogs_properties_app_id_valuesKEEP Esql.azure_signinlogs_properties_app_id_values
Esql.azure_signinlogs_properties_resource_id_valuesKEEP Esql.azure_signinlogs_properties_resource_id_values
Esql.azure_signinlogs_properties_resource_display_name_valuesKEEP Esql.azure_signinlogs_properties_resource_display_name_values
Esql.azure_signinlogs_properties_app_owner_tenant_id_valuesKEEP Esql.azure_signinlogs_properties_app_owner_tenant_id_values
Esql.azure_signinlogs_properties_resource_owner_tenant_id_valuesKEEP Esql.azure_signinlogs_properties_resource_owner_tenant_id_values
Esql.azure_signinlogs_properties_conditional_access_status_valuesKEEP Esql.azure_signinlogs_properties_conditional_access_status_values
Esql.azure_signinlogs_properties_risk_state_valuesKEEP Esql.azure_signinlogs_properties_risk_state_values
Esql.azure_signinlogs_properties_risk_level_aggregated_valuesKEEP Esql.azure_signinlogs_properties_risk_level_aggregated_values
Esql.azure_signinlogs_properties_device_detail_browser_valuesKEEP Esql.azure_signinlogs_properties_device_detail_browser_values
Esql.azure_signinlogs_properties_device_detail_operating_system_valuesKEEP Esql.azure_signinlogs_properties_device_detail_operating_system_values
Esql.user_agent_original_valuesKEEP Esql.user_agent_original_values
Esql.is_browser_case_maxKEEP Esql.is_browser_case_max
Esql.event_countKEEP Esql.event_count