Azure Authentication Methods Policy OIDC Discovery URL Changed


Description

Detects modifications to the OIDC discovery URL in Azure Entra ID's Authentication Methods Policy. This technique enables attackers to federate the tenant with attacker-controlled identity providers, bypassing multi-factor authentication and enabling unauthorized access through bring-your-own IdP methods.

Query · python

def rule(event):
    operation_name = event.get("operationName", "")

    if "authentication methods policy update" not in operation_name.lower():
        return False

    old_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "oldValue", default=[]
    )
    new_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "newValue", default=[]
    )

    # Ensure we have lists
    if not isinstance(old_values, list):
        old_values = [old_values] if old_values else []
    if not isinstance(new_values, list):
        new_values = [new_values] if new_values else []

    if len(old_values) != len(new_values):
        # Lists have different lengths; check all values to be safe
        for value in old_values + new_values:
            if isinstance(value, str) and "discoveryUrl" in value:
                return True
        return False

    for old_value, new_value in zip(old_values, new_values):
        if (
            isinstance(old_value, str)
            and isinstance(new_value, str)
            and "discoveryUrl" in old_value
            and "discoveryUrl" in new_value
        ):
            if old_value != new_value:
                return True

    return False


def title(event):
    actor = event.deep_get(
        "properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN_ACTOR>"
    )
    return f"Authentication Methods Policy OIDC Discovery URL Changed by [{actor}]"


def alert_context(event):
    context = {}

    context["operation_name"] = event.get("operationName", "<NO_OPERATION>")
    context["activity_display_name"] = event.deep_get(
        "properties", "activityDisplayName", default="<NO_ACTIVITY>"
    )
    context["category"] = event.get("category", "<NO_CATEGORY>")

    context["initiator_user_id"] = event.deep_get(
        "properties", "initiatedBy", "user", "id", default="<NO_USER_ID>"
    )
    context["initiator_display_name"] = event.deep_get(
        "properties", "initiatedBy", "user", "displayName", default="<NO_DISPLAY_NAME>"
    )
    context["initiator_ip"] = event.deep_get(
        "properties", "initiatedBy", "user", "ipAddress", default="<NO_IP>"
    )

    # Extract OIDC discovery URL changes
    old_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "oldValue", default=[]
    )
    new_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "newValue", default=[]
    )

    if not isinstance(old_values, list):
        old_values = [old_values] if old_values else []
    if not isinstance(new_values, list):
        new_values = [new_values] if new_values else []

    for old_value, new_value in zip(old_values, new_values):
        if (isinstance(old_value, str) and "discoveryUrl" in old_value) or (
            isinstance(new_value, str) and "discoveryUrl" in new_value
        ):
            context["old_discovery_url"] = old_value
            context["new_discovery_url"] = new_value
            break

    return context

Analyst notes

  1. Query Azure.Audit logs for all authentication policy changes by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this change to determine if this modification was part of a broader attack campaign
  2. Immediately verify the new OIDC discovery URL with your identity team to confirm it points to a legitimate identity provider owned by your organization and not an attacker-controlled domain
  3. Query Azure.Audit and sign-in logs for all authentication events in the 24 hours after the policy change to identify any suspicious token issuance or unauthorized access attempts, and if the modification was unauthorized, immediately revert the authentication methods policy, revoke all active sessions, and review all API access and resource modifications that occurred during the compromise window
Raw source Azure Authentication Methods Policy OIDC Discovery URL Changed · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: azure_auth_methods_policy_oidc_change.py
RuleID: "Azure.Audit.OIDC.Changed"
DisplayName: "Azure Authentication Methods Policy OIDC Discovery URL Changed"
Enabled: true
LogTypes:
  - Azure.Audit
Severity: High
Description: >
  Detects modifications to the OIDC discovery URL in Azure Entra ID's Authentication Methods Policy. This technique enables attackers to federate the tenant with attacker-controlled identity providers, bypassing multi-factor authentication and enabling unauthorized access through bring-your-own IdP methods.
Tags:
  - Persistence
  - Modify Authentication Process
Reports:
  MITRE ATT&CK:
    - TA0003:T1556
    - TA0003:T1556.009
Runbook: |
  1. Query Azure.Audit logs for all authentication policy changes by properties:initiatedBy:user:userPrincipalName in the 7 days before and after this change to determine if this modification was part of a broader attack campaign
  2. Immediately verify the new OIDC discovery URL with your identity team to confirm it points to a legitimate identity provider owned by your organization and not an attacker-controlled domain
  3. Query Azure.Audit and sign-in logs for all authentication events in the 24 hours after the policy change to identify any suspicious token issuance or unauthorized access attempts, and if the modification was unauthorized, immediately revert the authentication methods policy, revoke all active sessions, and review all API access and resource modifications that occurred during the compromise window
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/persistence_entra_id_oidc_discovery_url_change.toml
SummaryAttributes:
  - properties:initiatedBy:user:userPrincipalName
  - properties:targetResources:displayName
  - properties:initiatedBy:user:ipAddress
Tests:
  - Name: Authentication Methods Policy OIDC Discovery URL Changed
    ExpectedResult: true
    Log:
      {
        "time": "2025-01-15 14:20:35.456",
        "resourceId": "/tenants/tenant-abc/providers/Microsoft.aadiam",
        "operationName": "Authentication Methods Policy Update",
        "operationVersion": "1.0",
        "category": "Policy",
        "tenantId": "tenant-abc",
        "resultSignature": "None",
        "durationMs": 0,
        "callerIpAddress": "1.2.3.4",
        "correlationId": "policy-update-001",
        "Level": "4",
        "properties":
          {
            "result": "success",
            "operationName": "Authentication Methods Policy Update",
            "activityDisplayName": "Update authentication methods policy",
            "activityDateTime": "2025-01-15T14:20:35.4567890Z",
            "loggedByService": "Core Directory",
            "operationType": "Update",
            "initiatedBy":
              {
                "user":
                  {
                    "id": "admin-compromised-001",
                    "displayName": "Compromised Global Admin",
                    "userPrincipalName": "denethor@lotr.com",
                    "ipAddress": "1.2.3.4",
                  },
              },
            "targetResources":
              [
                {
                  "id": "policy-auth-methods-123",
                  "displayName": "Authentication Methods Policy",
                  "type": "Policy",
                  "modifiedProperties":
                    [
                      {
                        "displayName": "OpenIdConnectConfiguration",
                        "oldValue": "{\"discoveryUrl\":\"https://login.microsoftonline.com/tenant-abc/.well-known/openid-configuration\"}",
                        "newValue": "{\"discoveryUrl\":\"https://attacker-idp.evil.com/.well-known/openid-configuration\"}",
                      },
                    ],
                },
              ],
          },
        "p_event_time": "2025-01-15 14:20:35.456",
        "p_log_type": "Azure.Audit",
      }
  - Name: Non-OIDC Policy Change
    ExpectedResult: false
    Log:
      {
        "time": "2025-01-15 15:30:40.789",
        "resourceId": "/tenants/tenant-xyz/providers/Microsoft.aadiam",
        "operationName": "Authentication Methods Policy Update",
        "operationVersion": "1.0",
        "category": "Policy",
        "tenantId": "tenant-xyz",
        "resultSignature": "None",
        "durationMs": 0,
        "callerIpAddress": "203.0.113.50",
        "correlationId": "policy-update-002",
        "Level": "4",
        "properties":
          {
            "result": "success",
            "operationName": "Authentication Methods Policy Update",
            "activityDisplayName": "Update authentication methods policy",
            "activityDateTime": "2025-01-15T15:30:40.7890123Z",
            "loggedByService": "Core Directory",
            "operationType": "Update",
            "initiatedBy":
              {
                "user":
                  {
                    "id": "admin-legitimate-002",
                    "displayName": "Legitimate Admin",
                    "userPrincipalName": "admin@company.com",
                    "ipAddress": "203.0.113.50",
                  },
              },
            "targetResources":
              [
                {
                  "id": "policy-auth-methods-456",
                  "displayName": "Authentication Methods Policy",
                  "type": "Policy",
                  "modifiedProperties":
                    [
                      {
                        "displayName": "PasswordlessMFAEnabled",
                        "oldValue": "false",
                        "newValue": "true",
                      },
                    ],
                },
              ],
          },
        "p_event_time": "2025-01-15 15:30:40.789",
        "p_log_type": "Azure.Audit",
      }

# ------ paired body: azure_auth_methods_policy_oidc_change.py ------

def rule(event):
    operation_name = event.get("operationName", "")

    if "authentication methods policy update" not in operation_name.lower():
        return False

    old_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "oldValue", default=[]
    )
    new_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "newValue", default=[]
    )

    # Ensure we have lists
    if not isinstance(old_values, list):
        old_values = [old_values] if old_values else []
    if not isinstance(new_values, list):
        new_values = [new_values] if new_values else []

    if len(old_values) != len(new_values):
        # Lists have different lengths; check all values to be safe
        for value in old_values + new_values:
            if isinstance(value, str) and "discoveryUrl" in value:
                return True
        return False

    for old_value, new_value in zip(old_values, new_values):
        if (
            isinstance(old_value, str)
            and isinstance(new_value, str)
            and "discoveryUrl" in old_value
            and "discoveryUrl" in new_value
        ):
            if old_value != new_value:
                return True

    return False


def title(event):
    actor = event.deep_get(
        "properties", "initiatedBy", "user", "userPrincipalName", default="<UNKNOWN_ACTOR>"
    )
    return f"Authentication Methods Policy OIDC Discovery URL Changed by [{actor}]"


def alert_context(event):
    context = {}

    context["operation_name"] = event.get("operationName", "<NO_OPERATION>")
    context["activity_display_name"] = event.deep_get(
        "properties", "activityDisplayName", default="<NO_ACTIVITY>"
    )
    context["category"] = event.get("category", "<NO_CATEGORY>")

    context["initiator_user_id"] = event.deep_get(
        "properties", "initiatedBy", "user", "id", default="<NO_USER_ID>"
    )
    context["initiator_display_name"] = event.deep_get(
        "properties", "initiatedBy", "user", "displayName", default="<NO_DISPLAY_NAME>"
    )
    context["initiator_ip"] = event.deep_get(
        "properties", "initiatedBy", "user", "ipAddress", default="<NO_IP>"
    )

    # Extract OIDC discovery URL changes
    old_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "oldValue", default=[]
    )
    new_values = event.deep_walk(
        "properties", "targetResources", "modifiedProperties", "newValue", default=[]
    )

    if not isinstance(old_values, list):
        old_values = [old_values] if old_values else []
    if not isinstance(new_values, list):
        new_values = [new_values] if new_values else []

    for old_value, new_value in zip(old_values, new_values):
        if (isinstance(old_value, str) and "discoveryUrl" in old_value) or (
            isinstance(new_value, str) and "discoveryUrl" in new_value
        ):
            context["old_discovery_url"] = old_value
            context["new_discovery_url"] = new_value
            break

    return context

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.