Azure ROPC Login Attempt Without MFA


Description

Detects Resource Owner Password Credentials (ROPC) OAuth 2.0 authentication attempts in Microsoft Entra ID using single-factor authentication without MFA enforcement. ROPC is a deprecated legacy flow that allows applications to directly collect user credentials to obtain access tokens, bypassing modern authentication and MFA requirements. Adversaries commonly exploit ROPC during credential enumeration and password spraying campaigns using tools like TeamFiltration and MSOLSpray.

Query · python

from panther_azuresignin_helpers import (
    azure_signin_alert_context,
    azure_signin_success,
    is_sign_in_event,
)


def rule(event):
    # Check for sign-in events
    if not is_sign_in_event(event) or not azure_signin_success(event):
        return False

    # Check for ROPC authentication protocol
    auth_protocol = event.deep_get("properties", "authenticationProtocol", default="").lower()
    if auth_protocol != "ropc":
        return False

    auth_requirement = event.deep_get("properties", "authenticationRequirement", default="").lower()

    # Only alert on no MFA
    if auth_requirement != "singlefactorauthentication":
        return False

    # Check user type for Member account
    user_type = event.deep_get("properties", "userType", default="").lower()
    if user_type and user_type != "member":
        return False

    return True


def title(event):
    user_principal_name = event.deep_get(
        "properties", "userPrincipalName", default="<UNKNOWN_USER>"
    )
    source_ip = event.deep_get("properties", "ipAddress", default="<UNKNOWN_IP>")
    app_display_name = event.deep_get("properties", "appDisplayName", default="<UNKNOWN_APP>")

    return (
        f"ROPC Login Without MFA: User [{user_principal_name}] authenticated via "
        f"ROPC protocol from IP [{source_ip}] to app [{app_display_name}]"
    )


def alert_context(event):
    context = azure_signin_alert_context(event)

    # Add ROPC-specific fields
    fields = {
        "authentication_protocol": ("properties", "authenticationProtocol", "<NO_PROTOCOL>"),
        "authentication_requirement": (
            "properties",
            "authenticationRequirement",
            "<NO_REQUIREMENT>",
        ),
        "user_type": ("properties", "userType", "<NO_USER_TYPE>"),
        "app_display_name": ("properties", "appDisplayName", "<NO_APP>"),
        "app_id": ("properties", "appId", "<NO_APP_ID>"),
        "client_app_used": ("properties", "clientAppUsed", "<NO_CLIENT_APP>"),
        "user_agent": ("properties", "userAgent", "<NO_USER_AGENT>"),
        "is_interactive": ("properties", "isInteractive", None),
        "conditional_access_status": ("properties", "conditionalAccessStatus", "<NO_CA_STATUS>"),
        "device_detail_browser": ("properties", "deviceDetail", "browser", "<NO_BROWSER>"),
        "device_detail_os": ("properties", "deviceDetail", "operatingSystem", "<NO_OS>"),
    }

    for key, (*path, default) in fields.items():
        context[key] = event.deep_get(*path, default=default)

    return context

Analyst notes

  1. Query Azure.Audit logs for all ROPC authentication attempts by properties:userPrincipalName in the 24 hours surrounding this event to determine if this represents isolated testing or part of a broader password spraying or credential enumeration campaign with multiple failed attempts across different accounts
  2. Identify the application using ROPC by reviewing properties:appDisplayName and properties:appId, and verify with application owners whether this application has a legitimate business requirement for ROPC authentication or if it should be migrated to modern OAuth flows with interactive authentication
  3. Review the source IP address callerIpAddress and user agent properties:userAgent for indicators of automated attack tools (e.g., python-requests, curl, TeamFiltration signatures), check if the IP is associated with known malicious infrastructure, and if suspicious activity is confirmed, block the application from using ROPC through Azure AD application policies and force password reset for the affected user
Raw source Azure ROPC Login Attempt Without MFA · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: azure_ropc_login_no_mfa.py
RuleID: "Azure.Audit.ROPCLoginNoMFA"
DisplayName: "Azure ROPC Login Attempt Without MFA"
Enabled: true
Status: Experimental
LogTypes:
  - Azure.Audit
Severity: Medium
DedupPeriodMinutes: 60
Description: >
  Detects Resource Owner Password Credentials (ROPC) OAuth 2.0 authentication attempts in Microsoft
  Entra ID using single-factor authentication without MFA enforcement. ROPC is a deprecated legacy
  flow that allows applications to directly collect user credentials to obtain access tokens, bypassing
  modern authentication and MFA requirements. Adversaries commonly exploit ROPC during credential
  enumeration and password spraying campaigns using tools like TeamFiltration and MSOLSpray.
Reports:
  MITRE ATT&CK:
    - TA0001:T1078
    - TA0001:T1078.004
Runbook: |
  1. Query Azure.Audit logs for all ROPC authentication attempts by properties:userPrincipalName in the 24 hours surrounding this event to determine if this represents isolated testing or part of a broader password spraying or credential enumeration campaign with multiple failed attempts across different accounts
  2. Identify the application using ROPC by reviewing properties:appDisplayName and properties:appId, and verify with application owners whether this application has a legitimate business requirement for ROPC authentication or if it should be migrated to modern OAuth flows with interactive authentication
  3. Review the source IP address callerIpAddress and user agent properties:userAgent for indicators of automated attack tools (e.g., python-requests, curl, TeamFiltration signatures), check if the IP is associated with known malicious infrastructure, and if suspicious activity is confirmed, block the application from using ROPC through Azure AD application policies and force password reset for the affected user
Reference: https://github.com/elastic/detection-rules/blob/main/rules/integrations/azure/initial_access_entra_id_unusual_ropc_login_attempt.toml
SummaryAttributes:
  - properties:userPrincipalName
  - callerIpAddress
  - properties:appDisplayName
Tests:
  - Name: ROPC Login Without MFA
    ExpectedResult: true
    Log:
      {
        "time": "2025-01-15 09:30:25.123",
        "resourceId": "/tenants/tenant-123/providers/Microsoft.aadiam",
        "operationName": "Sign-in activity",
        "operationVersion": "1.0",
        "category": "SignInLogs",
        "tenantId": "tenant-123",
        "resultType": "0",
        "resultSignature": "SUCCESS",
        "durationMs": 0,
        "callerIpAddress": "2.2.2.2",
        "correlationId": "ropc-signin-001",
        "Level": "4",
        "properties":
          {
            "createdDateTime": "2025-01-15T09:30:25.1234567Z",
            "userPrincipalName": "gandalf@lotr.com",
            "userId": "user-123",
            "ipAddress": "2.2.2.2",
            "authenticationProtocol": "ropc",
            "authenticationRequirement": "singleFactorAuthentication",
            "userType": "Member",
            "appDisplayName": "Legacy Application",
            "appId": "app-legacy-456",
            "clientAppUsed": "Other clients",
            "userAgent": "python-requests/2.28.1",
            "isInteractive": false,
            "authenticationDetails":
              [{ "authenticationMethod": "Password", "authenticationStepDateTime": "2025-01-15T09:30:25.1234567Z" }],
            "conditionalAccessStatus": "notApplied",
            "deviceDetail": { "browser": "Unknown", "operatingSystem": "Unknown" },
            "resourceDisplayName": "Microsoft Graph",
            "resourceId": "00000003-0000-0000-c000-111111111111",
          },
        "p_event_time": "2025-01-15 09:30:25.123",
        "p_log_type": "Azure.Audit",
      }
  - Name: Failed ROPC Login
    ExpectedResult: false
    Log:
      {
        "time": "2025-01-15 15:05:50.678",
        "resourceId": "/tenants/tenant-ghi/providers/Microsoft.aadiam",
        "operationName": "Sign-in activity",
        "operationVersion": "1.0",
        "category": "SignInLogs",
        "tenantId": "tenant-ghi",
        "resultType": "50126",
        "resultSignature": "None",
        "durationMs": 0,
        "callerIpAddress": "192.0.2.250",
        "correlationId": "ropc-fail-001",
        "Level": "4",
        "properties":
          {
            "createdDateTime": "2025-01-15T15:05:50.6789012Z",
            "userPrincipalName": "attacker@company.com",
            "userId": "user-attacker-999",
            "ipAddress": "192.0.2.250",
            "authenticationProtocol": "ropc",
            "authenticationRequirement": "singleFactorAuthentication",
            "userType": "Member",
            "appDisplayName": "Office Application",
            "appId": "app-office-555",
            "clientAppUsed": "Other clients",
            "userAgent": "curl/7.68.0",
            "isInteractive": false,
            "status":
              {
                "errorCode": 50126,
                "failureReason": "Invalid username or password",
              },
            "conditionalAccessStatus": "notApplied",
            "deviceDetail": { "browser": "Unknown", "operatingSystem": "Unknown" },
          },
        "p_event_time": "2025-01-15 15:05:50.678",
        "p_log_type": "Azure.Audit",
      }

# ------ paired body: azure_ropc_login_no_mfa.py ------

from panther_azuresignin_helpers import (
    azure_signin_alert_context,
    azure_signin_success,
    is_sign_in_event,
)


def rule(event):
    # Check for sign-in events
    if not is_sign_in_event(event) or not azure_signin_success(event):
        return False

    # Check for ROPC authentication protocol
    auth_protocol = event.deep_get("properties", "authenticationProtocol", default="").lower()
    if auth_protocol != "ropc":
        return False

    auth_requirement = event.deep_get("properties", "authenticationRequirement", default="").lower()

    # Only alert on no MFA
    if auth_requirement != "singlefactorauthentication":
        return False

    # Check user type for Member account
    user_type = event.deep_get("properties", "userType", default="").lower()
    if user_type and user_type != "member":
        return False

    return True


def title(event):
    user_principal_name = event.deep_get(
        "properties", "userPrincipalName", default="<UNKNOWN_USER>"
    )
    source_ip = event.deep_get("properties", "ipAddress", default="<UNKNOWN_IP>")
    app_display_name = event.deep_get("properties", "appDisplayName", default="<UNKNOWN_APP>")

    return (
        f"ROPC Login Without MFA: User [{user_principal_name}] authenticated via "
        f"ROPC protocol from IP [{source_ip}] to app [{app_display_name}]"
    )


def alert_context(event):
    context = azure_signin_alert_context(event)

    # Add ROPC-specific fields
    fields = {
        "authentication_protocol": ("properties", "authenticationProtocol", "<NO_PROTOCOL>"),
        "authentication_requirement": (
            "properties",
            "authenticationRequirement",
            "<NO_REQUIREMENT>",
        ),
        "user_type": ("properties", "userType", "<NO_USER_TYPE>"),
        "app_display_name": ("properties", "appDisplayName", "<NO_APP>"),
        "app_id": ("properties", "appId", "<NO_APP_ID>"),
        "client_app_used": ("properties", "clientAppUsed", "<NO_CLIENT_APP>"),
        "user_agent": ("properties", "userAgent", "<NO_USER_AGENT>"),
        "is_interactive": ("properties", "isInteractive", None),
        "conditional_access_status": ("properties", "conditionalAccessStatus", "<NO_CA_STATUS>"),
        "device_detail_browser": ("properties", "deviceDetail", "browser", "<NO_BROWSER>"),
        "device_detail_os": ("properties", "deviceDetail", "operatingSystem", "<NO_OS>"),
    }

    for key, (*path, default) in fields.items():
        context[key] = event.deep_get(*path, default=default)

    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.