Threat Analysis

Tampering with Conditional Access Policies Using Azure AD Graph API

Summary

Azure Active Directory (Azure AD) is Microsoft's cloud-based identity and access management service, and it supports multiple authentication methods. The premium version of Azure AD also supports Conditional Access policies (CAPs) that grant or block access based on defined criteria, such as device compliance or user location. Azure AD stores the settings for the authentication methods and CAPs. CAPs can be modified via the Azure AD portal, PowerShell, and API calls.

In May 2022, Secureworks® Counter Threat Unit™ (CTU) researchers investigated which APIs allow editing of CAP settings and identified three: the legacy Azure AD Graph (also known as AADGraph), Microsoft Graph, and an undocumented Azure IAM API. AADGraph was the only API that allowed modification of all CAP settings, including the metadata. This capability lets administrators tamper with all CAP settings, including the creation and modification timestamps. Modifications made using AADGraph are not properly logged, endangering integrity and non-repudiation of Azure AD policies.

CTU™ researchers shared these findings with Microsoft on May 26, 2022. Microsoft confirmed the findings a month later but stated that it is expected behavior. On May 11, 2023, Microsoft notified CTU researchers of planned changes to improve audit logs and restrict CAP updates via AADGraph.

Azure AD CAPs

Azure AD CAPs allow organizations to grant or block access to services protected by Azure AD. They can also be used for session monitoring and limiting a session lifetime. CAPs are enforced during the Azure AD authentication process. Azure AD uses the following common signals to make a policy decision:

  • User or group membership
  • IP location information
  • Device
  • Application

Only users with specific roles can access CAPs in the Azure AD portal (see Table 1).

Access type Azure AD role
Read Global Administrator
Global Reader
Security Administrator
Conditional Access Administrator
Security Reader
Modify Global Administrator
Security Administrator
Conditional Access Administrator

Table 1. Azure AD roles required to access CAPs.

Figure 1 shows an example CAP that requires all users to perform multi-factor authentication (MFA). The policy is not enabled in this example; it is set to Report-only mode. This mode allows organizations to assess the impact of the CAP before enforcing it.


Figure 1. Sample CAP. (Source: Secureworks)

The Azure AD portal displays the name, state, and creation and modification timestamps (see Figure 2).


Figure 2. List of CAPs. (Source: Secureworks)

The Azure AD portal reflects changes whenever the CAP is modified (see Figure 3).


Figure 3. Modified CAP. (Source: Secureworks)

Azure AD audit logs captures CAP creation and modification events (see Figure 4).


Figure 4. Azure AD audit logs listing CAP creation events (highlighted in red (bottom)) and CAP modification events (highlighted in green (top)). (Source: Secureworks)

Both the 'Add conditional access policy' and 'Update conditional access policy' events include details of the modified properties (see Figure 5). This feature provides a full audit trail and includes modified settings.


Figure 5. Audit log details for the 'Update conditional access policy' event. (Source: Secureworks)

Modifying Conditional Access with API calls

The Azure AD portal is a graphical user interface (GUI) that allows administrators to create and maintain CAPs via a browser. GUIs can perform ad-hoc tasks but not automation and programmatic access. To address those needs, Microsoft provides three APIs that can interact with CAPs:

  • Azure AD IAM
  • MS Graph
  • Azure AD Graph (AADGraph)

Azure AD IAM API

The Azure AD portal uses an undocumented Azure AD IAM API to create, view, and edit CAPs. The API is available at https ://main . iam . ad . ext . azure . com/api/Policies/Policies. Because the Azure AD portal uses Azure AD IAM APIs, access requires the permissions listed in Table 1. The API returns a list of CAPs as a JSON object (see Figure 6).


Figure 6. Azure AD IAM API response. (Source: Secureworks)

When the API opens a CAP for editing, it returns the CAP details as a JSON object (see Figure 7). This returned JSON object has many fields, which correspond to the CAP settings available in the Azure AD portal. The response also includes creation and modification timestamps.


Figure 7. Response returned by the Azure AD IAM API call. (Source: Secureworks)

Modifying a CAP sends a JSON object to https: //main . iam . ad . ext . azure . com/api/Policies/ConvertPolicyMsGraph as an HTTP POST request. Figure 8 shows a JSON object where the CAP state was changed from Off to Report-only. Only the modified data and not the metadata is sent to Azure AD.


Figure 8. Azure AD IAM API CAP modification request. (Source: Secureworks)

MS Graph API

MS Graph API support for conditional access is well-documented, Microsoft also published examples for creating and editing CAPs. Table 2 lists the required permissions to access CAPs via MS Graph API.

Access type Permissions
Modify (all three required) Policy.Read.All Policy.ReadWrite.ConditionalAccess Application.Read.All
Read Policy.Read.All

Table 2. MS Graph API permissions required for CAPs.

Users or applications with these permissions can list CAPs by calling the API at https: //graph . Microsoft . com/v1.0/identity/conditionalAccess/policies. The API returns all CAPs and details as a JSON object (see Figure 9).


Figure 9. MS Graph API response. (Source: Secureworks)

Creating or modifying a CAP uses the same API endpoint:

  • Creating a CAP makes an HTTP POST with a JSON object (see Figure 10).


    Figure 10. Creating Conditional Access policies via MS Graph API. (Source: Secureworks)

  • Modifying a CAP makes an HTTP PATCH request with a JSON object.

Only the modified data is sent to Azure AD. The metadata is not included.

Azure AD Graph API (AADGraph)

Microsoft has attempted to deprecate the AADGraph API for years. As of this publication, its retirement is scheduled to occur sometime after June 30, 2023. Microsoft has removed public AADGraph API documentation to discourage its use.

CAPs can be accessed using the AADGraph API at https: //graph . windows . net/<tenant>/policies?api-version=<api version>, where <tenant> is the Azure AD tenant and <api version> is the desired AADGraph API version. Using 1.6 as the API version returns some Azure AD policies that the user can access if they have appropriate permissions, but CAPs are not listed. However, using 1.61-internal as the version returns all Azure AD policies, including CAPs, regardless of the user's permissions. As a result, any user of the tenant can list CAPs and bypass the role requirements.

The API returns all policies as JSON objects. Figure 11 shows a CAP policy (indicated by the policyType of 18).


Figure 11. AADGraph API response. (Source: Secureworks)

The CAP settings and metadata are stored in the policyDetail attribute as a JSON object (see Figure 12). Administrators with permissions to modify CAPs can edit this attribute, enabling them to tamper with the CAP conditions and metadata.


Figure 12. CAP settings in policyDetail attribute. (Source: Secureworks)

Updating an existing CAP with the AADGraph API involves an HTTP PATCH request to https: //graph . windows . net/<tenant>/policies/<objectid>?api-version=1.61-internal, where <objectid> is the object ID of the CAP to be modified. The content of the request is a JSON object that only includes the policyDetail attribute (see Figure 13).


Figure 13. Updating CAP using the AADGraph API. (Source: Secureworks)

Tampering with Conditional Access policies

CTU researchers used the AADInternals toolkit to tamper with CAPs. Administrators or threat actors can leverage the AADGraph API to make changes that are not properly logged.

  1. We retrieved the current policyDetail value of the example CAP:
    1. Acquired an access token for an administrator with permissions to modify CAPs
    2. Saved the example CAP to a variable
    3. Extracted the policyDetail value and copy the data to the clipboard (see Figure 14)


      Figure 14. Getting current CAP policyDetail using AADInternals. (Source: Secureworks)

  2. We pasted the policyDetail value into a text editor and reformatted the JSON for readability (see Figure 15). We then emptied the ModifiedDateTime attribute (see line 4) and changed the State attribute from Reporting to Disabled. The modified JSON was flattened and copied to the clipboard.


    Figure 15. Modified CAP policyDetail. (Source: Secureworks)

  3. We used the modified policyDetail from the clipboard to update the CAP (see Figure 16).


    Figure 16. Updating the CAP policyDetail attribute via AADInternals. (Source: Secureworks)

    The Azure AD portal updated the modifications within a minute (see Figure 17).


    Figure 17. Modified CAP in Azure AD portal. (Source: Secureworks)

When CAPs are updated via the AADGraph API, the 'Update conditional access policy' event is not generated in the audit logs (see Figure 18). As a result, there is an incomplete audit trail on what modifications were made.


Figure 18. CAP modification via AADGraph does not create the Update conditional access event. (Source: Secureworks)

Threat actors with administrator permissions can leverage this omission to obscure CAPs. For instance, the PowerShell script in Figure 19 removes the timestamps and display names of all CAPs.


Figure 19. PowerShell scipt to remove CAP display name and timestamps. (Source: Secureworks)

After running the script, CAPs are fully functional. However, the Azure AD portal cannot open or edit them (see Figure 20).


Figure 20. Azure AD portal after removing CAP display names and timestamps. (Source: Secureworks)

Administrators can still delete CAPs and make duplicates to view existing CAP settings. If organizations keep audit logs for a longer period of time, they may be able to restore CAP names and timestamps based on historical audit log data.

Communication with Microsoft

CTU researchers reported the metadata editing and logging issues to the Microsoft Security Response Center (MSRC) on May 20, 2022. These issues were reported as tampering and elevation of privilege, as administrators are also able to modify the metadata. The MSRC responded on June 26:

We confirm the following behaviors when a Conditional Access Policy is modified via Azure AD Graph APIs (or PowerShell modules based on AAD Graph APIs):

Only Core Directory service Audit Log item is present in the Audit Logs, while the corresponding Conditional Access service Audit Log item is missing.

Details of the changed properties and values are not present in the Core Directory service Audit Log item.

Modified Date information of the edited policy object is not updated on the Conditional Access Azure Portal page.

We analyzed the scenario, and established that:

There is no escalation of privileges: only users with the required permissions are allowed to access or modify policy objects.

Investigations of malicious Conditional Access Policies are not affected due to relevant information present in the sign-in logs.

Date, Activity, Target, and Actor information of policy changes are present in the Activity Logs, allowing admins to audit who changed a policy and when.

On August 23, 2022, CTU researchers notified the MSRC that all users can read conditional access. This issue was reported as elevation of privilege, as any user can read CAPs without administrator permissions. The MSRC responded on February 2, 2023:

There are several known experiences where an authenticated and authorized user is able to read specific data pertaining to an Azure AD configuration such as an authentication policy or other similar configurations.

These cases are by design: the user is authorized, the data is read-only and doesn't contain any specific user information.

While reading data such as an authentication policy is not perceived as a security breach, we do have optimizations in Azure AD to allow other data or configurations to only be read or changed based on admin roles with specific edit / create/ delete rights for security purposes.

On May 11, 2023, the MSRC informed the CTU research team of planned changes to address these issues:

  1. Improve audit logs to reflect the type of policy being updated when CA policies are updated through AAD Graph.
  2. We will prevent admins from using AAD Graph to make updates to CA policies.

In addition to these improvements, AAD Graph is set to be retired.

Conclusion

Administrators can use the AADGraph API to change CAPs. The API does not properly log changes, and the lack of an audit trail breaks integrity and non-repudiation of CAPs. As a result, organizations cannot trust CAP information shown in the Azure AD portal or in directory audit logs. In addition, any tenant user can view CAPs without administrator permissions. This ability allows low-privileged threat actors to identify gaps in CAPs or target them for future modification. Third-party tools such as ROADTools and TSxAzureADExport exploit this ability.

CTU researchers recommend that organizations store Azure AD audit logs in the Log Analytics workspace or in other storage solutions such as Secureworks Taegis™ XDR. Organizations can detect CAP modifications via the AADGraph API by monitoring audit logs for an 'Update policy' event that does not have a corresponding 'Update conditional access policy' event within two seconds.

Appendix

The following script can restore the names and modification dates of CAPs that have been created or modified using the Azure AD portal or the MS Graph API:

# Read legit CAP events from the audit log
$CAPEvents=Get-AADIntAzureAuditLog -Export                    `
            | Where-Object activityDisplayName -in            `
                "Add conditional access policy",              `
                "Update conditional access policy"            `
            | Select-Object "activityDateTime" -ExpandProperty "targetResources" `
            | Select-Object "id","displayName","activityDateTime"
 
# Loop through the events to get the first (latest) update
$CAPInfos=@{}
foreach($CAPEvent in $CAPEvents)
{
    if(!$CAPInfos.ContainsKey($CAPEvent.id))
    {
        $CAPInfos[$CAPEvent.id] = [pscustomobject]@{
                "displayName"      = $CAPEvent.displayName
                "modifiedDateTime" = $CAPEvent.activityDateTime
            }
    }
}
 
# Read current CAPs
$CAPs = Get-AADIntConditionalAccessPolicies
 
# Loop through CAPs
foreach($CAP in $CAPs)
{
    # Create the return value
    $retVal = [pscustomobject][ordered]@{
        "id"      = $CAP.objectId
        "isEmpty" = [string]::IsNullOrWhiteSpace($CAP.displayName)
        "success" = $null
        "name"    = $CAP.displayName
    }
 
    # Check whether the displayName is empty
    if($retVal.isEmpty)
    {
        # Check whether we found the old information
        if($CAPInfo = $CAPInfos[$retVal.id])
        {
            # Get policyDetails and fix Modified Date
            $policyDetail = $CAP.policyDetail[0] | ConvertFrom-Json 
            try
            {
                $policyDetail.ModifiedDateTime = $CAPInfo.modifiedDateTime
            }
            catch{}
 
            $newPolicyDetail = $policyDetail | ConvertTo-Json -Depth 10 -Compress
 
            # Replace name with the old displayName
            $retVal.name = $CAPInfo.displayName
            try
            {
                Set-AADIntAzureADPolicyDetails  -ObjectId     $retVal.id       `
                                                -PolicyDetail $newPolicyDetail `
                                                -DisplayName  $retVal.name     `
                                                | Out-Null
 
                $retVal.success = $true
            }
            catch
            {
                # Failed
                $retVal.success = $false
                $retVal.name = $CAP.displayName
            }
        }
    }
    # Return
    $retVal
} 

The following KQL query can be used to identify 'Update policy' events that do not have a corresponding 'Update conditional access policy' event within two seconds:

AuditLogs 
| where OperationName == "Update policy"
| mv-expand TargetResources
| where TargetResources.displayName != "Default Policy"
| mv-expand InitiatedBy
| project PolicyName = TargetResources.displayName, PolicyId = tostring(TargetResources.id), UserPrincipalName = InitiatedBy.user.userPrincipalName, UserId = tostring(InitiatedBy.user.id), OperationName, Time = bin(TimeGenerated, 2s), TimeGenerated, CorrelationId
|join kind=leftanti (AuditLogs
        | where OperationName == "Update conditional access policy"
        | mv-expand TargetResources
        | mv-expand InitiatedBy
        | project PolicyId = tostring(TargetResources.id), UserId = tostring(InitiatedBy.user.id), Time = bin(TimeGenerated, 2s)) on PolicyId,UserId,Time
| order by TimeGenerated

Back to more Threat Analyses and Advisories

Additional Resources

TRY TAEGIS TODAY!

See for yourself: Request your demo to see how Taegis can reduce risk, optimize existing security investments, and fill talent gaps.