Implement conditional access on Azure AD Apps : Using Workload Identities Premium

Azure AD app do not honor conditional access policies levaraging IP restrictions.

Suppose we have a conditional access policy which restricts access to any app from any IP except certain IP ranges via a named location (in this case using my ISP network).

Interactive user login – Blocked

You will notice the user interactive sign-in gets blocked when coming from an IP outside of what is allowed.

Login using a service principal for Azure AD app – Allowed

In this section, we will see if you have credentials for Azure AD app, you can access resources depending on what permissions the app has. In this example, we would read all the emails.

Setup

Lets setup an Azure AD app with mail.read permission and a credential.

Code to get emails from all the mailboxes

Prerequisites : Install and import ExchangeOnlinemanagement and Microsoft.Graph modules

Install-Module ExchangeOnlineManagement
Import-Module ExchangeOnlineManagement
Install-Module Microsoft.Graph
Import-Module Microsoft.Graph

Replace the clientId and tenantId with the clientId of the app and the tenant id for your tenant respectively. When the script is run, please supply the credential created for the app.

# Import the required module
Import-Module Microsoft.Graph
$err_string= ''
# Set the necessary variables
$clientId = "7477abb4-xxxx-xxxx-xxxx-xxxxxx"
$tenantId = "c2b84b0b-xxxx-xxxx-xxxx-xxxxxxx"
$ClientSecretCredential = Get-Credential -Credential $clientId
 
# Connect to Microsoft Graph
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome
 
# Get all users in the tenant
$users = Get-MgUser
 
# Loop through each user
foreach ($user in $users) {
    # Get the user's mailbox
    try {
        $mailbox = Get-MgUserMailFolderMessage -UserId $user.Id -MailFolderId 'Inbox' -ErrorAction Stop
        $test = $user.Mail
        write-host "####### Reading emails for mailbox " -nonewline
        write-host $test -foreground red -nonewline
        write-host " ##########" 
        write-host "Found " -nonewline
        write-host $mailbox.Length -foreground red -nonewline
        write-host " email(s) " 
        foreach ($message in $mailbox) {
            # Print the message subject and received date
            Write-Output (" ----------------------------------------------------")
            Write-Output ("Subject: " + $message.Subject)
            Write-Output ("Received: " + $message.ReceivedDateTime)
            $body = $message.Body.Content -replace '<[^>]+>',''
            $body = $body.trim()
            Write-Output ("Body: " + $body)
        }
    write-host "`n"
    }
    catch
    { 
        $err_string = $_ | Out-String
    }
    if ($err_string -inotmatch "The mailbox is either inactive, soft-deleted, or is hosted on-premise")
    {
        Write-Host $err_string
    }
}
# Disconnect from Microsoft Graph
Disconnect-MgGraph

Running the above code by supplying the secret for the below and we can see we are still able to access all the emails. The service principle sign-in logs clearly note the access is from outside the IP address (from a foreign country) but the conditional access policy didn’t apply.

Using Microsoft Entra Workload ID to implement the conditional access

To address this, Microsoft has a new feature named Microsoft Entra Workload ID. Bad thing here is It needs Its own premium license. Good thing is you can try it out to see if this even works!
Login to the Entra ID portal as a global admininstrator and search for Workload Identities and activate the trial of 200 licenses for 90 days.

Then login to Microsoft Admin portal, and assign the users with “Micsoroft Entra Workload ID” license.

Once the license is assigned, login as the GA and licensed user to the the Entra portal.
Go to Protection > Conditional Access > Create.
There we see “Workload Identities” under “What does this policy apply to”.
Now, we can select the app we want to apply the conditional access with IP restriction.

Running the same code above would now show error noting the access has been blocked by the conditional access policy

Service principal sign-in logs would show the failure as well

Conclusion

Microsoft Entra Workload ID premium looks promising and goes much beyond the conditional access. Its worth looking at its capabilities.

https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-workload-id

Getting a handle on Azure AD/ Entra ID apps and their permissions

Midnight blizzard attack on Microsoft involved abuse of permissions on Azure AD/OAuth apps. Therefore, Its important to take stock of all the apps and their permissions and evaluate if we need those permissions and reduce them if we can.

Per the post, the attacker abused Office 365 Exchange Online full_access_as_app role, which allows access to mailbox. However, Microsoft Graph API also allows an app to use privileged mail.read/mail.write/mail.readwrite which can be abused to have similar effect.

This post has details on how to get all the apps and their permissions and potential way to prevent/detect.

What are Azure AD / Entra ID apps

On a high level, you can use Azure AD app to access any resources in Azure and M365 and that includes emails as well.

When you create an Azure AD application, you’re essentially registering your application with Azure AD, obtaining an application ID (also known as client ID) and optionally a client secret or certificate for authentication purposes and permissions to authorize them to access resources. This allows your application to authenticate users against Azure AD and access resources on behalf of those users.

Because attackers can abuse the high privileged permissions on Azure AD app to access Azure/M365 , It’s important to govern the apps and their permissions and below are few ways :

  • Get all the Azure AD apps and their permissions
  • Do we even need that “prod” Azure AD app?
  • Do we really need those permissions on the “prod” Azure AD app?
  • Apply conditional access policy on the apps e.g. IP restriction
  • Apply restrictions on domain users to register Azure AD/Entra apps
  • Understand roles and users in those roles which can manage Azure AD applications
  • Splunk monitoring and detection

Get all the Azure AD apps and their permissions

Powershell script to export all the azure AD apps and their permissions

Install the Azure AD module.
install-module azuread

# Connect to Azure AD
Connect-AzureAD

# Get all Azure AD applications
$allApps = Get-AzureADApplication -All $true
$array = @()
# Loop through each application
foreach ($app in $allApps) {
    Write-Host "Application Name: $($app.DisplayName)"

    # Get the required resource access (application permissions)
    $appPermissions = $app.RequiredResourceAccess | ForEach-Object {
        $resourceAppId = $_.ResourceAppId
        $resourceSP = Get-AzureADServicePrincipal -Filter "AppId eq '$resourceAppId'"
        $_.ResourceAccess | ForEach-Object {
            $permissionId = $_.Id
            $permissionType = $_.Type
            $permission = $null
			#$resourceSP
            if ($permissionType -eq 'Role') {
                $permission = $resourceSP.AppRoles | Where-Object { $_.Id -eq $permissionId }
            } elseif ($permissionType -eq 'Scope') {
                $permission = $resourceSP.Oauth2Permissions | Where-Object { $_.Id -eq $permissionId }
            }

            if ($permission) {
                [PSCustomObject]@{
                    'Application Name' = $app.DisplayName
					'API' = $resourceSP.DisplayName
                    'Permission Name' = $permission.Value
                    'Permission Description' = $permission.Description
                    'Permission Type' = $permissionType
                }
            }
        }
    }
	$array+=$appPermissions
    # Output the permissions
    #$appPermissions | Format-Table
}
$array | Export-Csv "output.csv"

The CSV file generating the below output :

  • Application Name
  • API
  • Permission Name
  • Permission Description
  • Permission Type (“Role” means application permissions and “Scope” means delegated permissions

Splunk output

If you are using Splunk and using ingesting the activity logs from M365 using Splunk Add-On for Microsoft 365, you can use below query to get all the app role assignments.

 index="o365" Operation="Add app role assignment to service principal."
| spath path=ModifiedProperties{}.NewValue output=NewValues
| spath path=Target{}.ID output=NewTargetValues
| eval _time = strptime(CreationTime, "%Y-%m-%dT%H:%M")
| eval AppName = mvindex(NewValues, 6)
| eval perm = mvindex(NewValues, 1)
| eval permdesc = mvindex(NewValues, 2)
| eval target = mvindex(NewTargetValues, 3)
| table _time, AppName, perm, target
| stats values(perm) as AllAPIPermissions, values(target) as API by AppName

Using MSIdentityTools

Mr. Merill Fernando [Principal Product Manager, Entra ] released a fantastic video for the update in the MSIdentityTool to generate the apps and permissions. Works like a charm.

Do we even need that “prod” Azure app?

Now that you have the list of the apps from the script above, you want to chedk if the apps in the list are even being used.
Login to Microsoft Entra Admin Center > Monitoring & Health > Service Principal sign-ins > Filter for last 7 days
If its a production app, and if they are not in the sign-in events screen for last 7 days, you want to ask the app owners if this app is needed any more. Get the email confirmation and remove the app.

Do we really need those permissions on the “prod” Azure AD app?

Sometimes, apps are assigned permissions which they really dont need. For example, mail.send/mail.read/mail.readwrite are assigned to an app to work with couple of mailboxes. However, the permissions are meant to work with ALL mailboxes and can be abused by an attacker.

Implement Conditional Access for Azure AD apps

Azure AD apps do not honor the conditional access policies to enforce IP restriction, for example. A potential solution is to use Microsoft Entra Workload ID premium feature.

Apply restrictions on domain users to register Azure AD/Entra apps

Login to Azure portal > Microsoft Entra ID > User settings.
Ensure the “User can register applications” is set to “No”.

This takes out the risk of a domain user registering an app and giving it permissions – although an admin still needs to grant consent on it.
Having said that, even with the above setting in place there are roles which can register applications. An example below is role “Application developers”.

This is another reason why best security practices should need to be applied for the privileged roles.

https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/privileged-roles-permissions?tabs=admin-center

Understand roles and users in those roles which can manage Azure AD applications

Apart from the “Application developer” role which can register Azure AD apps, below two are privileged roles which can add/update credentials to an existing Azure AD apps as well. So, if the attacker compromises users in the below roles, they can quickly escalate privileges by adding credentials to an existing Azure AD app which has high privileges like  full_access_as_app role or mail.read/send and exfilter emails out of mailboxes.

Therefore, we should be careful assigning these roles and if absolutely needed ensure they arew cloud-only accounts with MFA turned on.

  • Application Administrator
  • Cloud Application Administrator

https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference

Splunk Detection and Monitoring

In the context of Azure AD apps, I find the below searches useful which may be used for detections to be monitored by SOC:

Detect when high privileged permissions are assigned to Azure AD apps

Lets create a lookup of high privileged permissions with perm as the column

Splunk query to get all the instance when the permissions are assigned to the app matching the ones in the lookup table.

index=”o365″ Operation=”Add app role assignment to service principal.” ResultStatus=Success
| spath path=ModifiedProperties{}.NewValue output=NewValues
| spath path=Target{}.ID output=NewTargetValues
| eval _time = strptime(CreationTime, “%Y-%m-%dT%H:%M”)
| eval appname = mvindex(NewValues, 6)
| eval perm = mvindex(NewValues, 1)
| eval permdesc = mvindex(NewValues, 2)
| eval appid = mvindex(NewValues, 7)
| eval target = mvindex(NewTargetValues, 3)
| join type=inner perm [ inputlookup azure_m365_permissions.csv | table perm ]
| table _time, UserId, appid, appname, perm, permdesc, target

A new credential has been added to an Azure AD app

index=o365 Operation=”Update application – Certificates and secrets management ” ResultStatus=”Success”
| table _time UserId OrganizationId Operation Target{}.ID ModifiedProperties{}.NewValue ModifiedProperties{}.Name ModifiedProperties{}.OldValue Target{}.Type