Azure Run Command attacks: investigating VM execution
How attackers use Azure VM Run Command and the Custom Script Extension, what the Activity Log records (and omits), and which on-VM artifacts hold the script.
TL;DR. Run Command lets anyone with Microsoft.Compute/virtualMachines/runCommand/action execute a script on a VM through the VM agent — as SYSTEM on Windows, as an elevated user on Linux — with no network path to the VM. The Activity Log records who, when, from where and on which VM, but generally not the script. The script lives on the VM: C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\<version>\Downloads\ on Windows, /var/lib/waagent/run-command/download/ on Linux. The Custom Script Extension (extensions/write) is the same idea with a download step. Correlate with outbound flows from the VM in the next minutes.
Run Command is the Azure feature I would miss most as an administrator and fear most as a defender. It turns a control-plane permission into code execution on the guest, which is exactly why intruders who obtain Contributor on a subscription use it within minutes.
Why attackers like Run Command
Microsoft describes Run Command as a way to run scripts "within an Azure Windows VM" using the VM agent, useful when RDP or SSH is closed (Windows, Linux). From an attacker's point of view:
| Property (per Microsoft Learn) | What it means for an attacker |
|---|---|
| Scripts run as System on Windows, as an elevated user on Linux | Immediate highest privilege on the guest |
Needs virtualMachines/runCommand/action; Virtual Machine Contributor and higher have it | Any stolen Contributor-level identity is enough |
| Works without RDP/SSH; the agent pulls the job | NSGs and firewalls do not stop it |
| Output limited to the last 4,096 bytes, max 90 minutes | Attackers run a downloader and leave |
The typical sequence: run RunShellScript or RunPowerShellScript with a one-liner that downloads a second stage, dumps credentials, or queries the Instance Metadata Service for the VM's managed identity token. That last one turns VM execution into a new cloud identity (managed identity abuse).
MITRE ATT&CK tracks this as T1651 Cloud Administration Command. Microsoft's own Azure Threat Research Matrix lists it as AZT301.1.
What the Activity Log shows
| Operation | Meaning |
|---|---|
Microsoft.Compute/virtualMachines/runCommand/action | "Action" Run Command on a VM (the classic one) |
Microsoft.Compute/virtualMachines/runCommands/write | Managed Run Command resource created or updated |
Microsoft.Compute/virtualMachineScaleSets/virtualMachines/runCommand/action | Run Command on a scale-set instance |
Microsoft.HybridCompute/machines/runCommands/write | Run Command on an Azure Arc-enabled server |
Microsoft.Compute/virtualMachines/extensions/write | Extension installed or updated (Custom Script Extension, among others) |
Microsoft.SerialConsole/serialPorts/connect/action | Serial console connection |
Each Run Command typically produces a Started, an Accepted and a Succeeded event sharing one correlationId. You get the caller, the claims, the caller IP and the target VM. What you usually do not get is the script. Mandiant's Azure Run Command for Dummies is explicit that the Activity Log shows the action but not the script contents; recovering them requires the VM.
Where the script lives on the VM
From Mandiant's research and Microsoft's troubleshooting notes:
| OS | Artifact | Path |
|---|---|---|
| Windows | Script body | C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\<version>\Downloads\script<n>.ps1 |
| Windows | Status and output | C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\<version>\Status\<n>.status |
| Windows | Extension log | C:\WindowsAzure\Logs\Plugins\Microsoft.CPlat.Core.RunCommandWindows\<version>\RunCommandExtension.log |
| Linux | Script, stdout, stderr | /var/lib/waagent/run-command/download/<n>/ (script.sh, stdout, stderr) |
| Linux | Handler log (timestamps, no script) | /var/log/azure/run-command/handler.log |
On Windows, RunPowerShellScript also leaves PowerShell telemetry (script block logging, event ID 4104) if it was enabled. Collect these files before rebuilding the VM: snapshot the OS disk, then image or mount it read-only.
The Custom Script Extension variant
The Custom Script Extension downloads files from fileUris and runs commandToExecute. It is installed with Microsoft.Compute/virtualMachines/extensions/write; the publisher and type are Microsoft.Compute / CustomScriptExtension on Windows and Microsoft.Azure.Extensions / CustomScript on Linux (Windows, Linux).
Forensic notes:
- The command can sit in
settings(clear text in the request body) orprotectedSettings(encrypted to the VM). Attackers who know this use protected settings. - On Windows it runs as
LocalSystem; downloads go toC:\Packages\Plugins\Microsoft.Compute.CustomScriptExtension\1.*\Downloads\<n>and logs toC:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension. - On Linux, downloads and
stdout/stderrgo to/var/lib/waagent/custom-script/download/<n>/, and the handler log is/var/log/azure/custom-script/handler.log. az vm extension listshows the current public settings, includingcommandToExecuteif it was not protected. It shows only the current state, not history.
What the analyzer flags
| Rule | Detects | Level |
|---|---|---|
AZ-VM-001 | Run Command on a VM, scale-set instance or Arc machine | High |
AZ-VM-002 | Custom Script Extension deployed (extensions/write with a custom-script type) | High |
AZ-VM-003 | Serial console connection | Medium |
AZ-VM-004 | VM created in a region not used during the learning period | Medium |
AZ-VM-005 | Disk or snapshot exported with a SAS URL (beginGetAccess) | High |
AZ-VM-006 | Disk snapshot created | Low |
AZ-NET-004 | Flow logs show traffic with an IP address involved in other findings | High |
AZ-VM-001 is noisy in environments that use Run Command for administration. That is fine: the point is to put each execution in front of a human, with its caller and IP, and the Azure Forensics analyzer timeline shows what happened right after.
Triage checklist
- Who ran it? A human administrator from the corporate IP range during working hours, or a service principal that normally deploys web apps?
- Is the caller's IP new?
AZ-ID-001flags service principals used from IPs never seen before. - What did the VM do next? In VNet or NSG flow logs, look for outbound connections from the VM's private IP to a public IP in the next few minutes. In the fictional sample, the VM connects out to the attacker IP on port 80 roughly 25 seconds after the Run Command starts.
- Did the VM's managed identity appear elsewhere? A managed identity token used from an IP other than the VM's is the signature of IMDS token theft (
AZ-ID-002). - Get the script from the disk snapshot, at the paths above.
- Check for disk export.
beginGetAccesson a disk or snapshot returns a download URL for the whole disk (MITRE T1537).
Remediation
- Isolate the VM (deny-all NSG), snapshot its disks for forensics, rebuild it from a known-good image.
- Remove unknown extensions and managed run commands; remove the RunCommand extension if you do not use it.
- Rotate credentials that were on the VM or reachable by its managed identity.
- Limit
runCommand/actionwith custom roles; Virtual Machine Contributor grants it.