Azure VM Run Command operations, when linked with MDE host logs, may indicate adversary attempts to execute arbitrary commands on virtual machines. SOC teams should proactively hunt for this behavior to detect potential lateral movement or persistence tactics leveraging cloud infrastructure.
KQL Query
AzureActivity
// Isolate run command actions
| where OperationNameValue == "Microsoft.Compute/virtualMachines/runCommand/action"
// Confirm that the operation impacted a virtual machine
| where Authorization has "virtualMachines"
// Each runcommand operation consists of three events when successful, Started, Accepted (or Rejected), Successful (or Failed).
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), max(CallerIpAddress), make_list(ActivityStatusValue) by CorrelationId, Authorization, Caller
// Limit to Run Command executions that Succeeded
| where list_ActivityStatusValue has "Succeeded"
// Extract data from the Authorization field, allowing us to later extract the Caller (UPN) and CallerIpAddress
| extend Authorization_d = parse_json(Authorization)
| extend Scope = Authorization_d.scope
| extend Scope_s = split(Scope, "/")
| extend Subscription = tostring(Scope_s[2])
| extend VirtualMachineName = tostring(Scope_s[-1])
| project StartTime, EndTime, Subscription, VirtualMachineName, CorrelationId, Caller, CallerIpAddress=max_CallerIpAddress
| join kind=leftouter (
DeviceFileEvents
| where InitiatingProcessFileName == "RunCommandExtension.exe"
| extend VirtualMachineName = tostring(split(DeviceName, ".")[0])
| project VirtualMachineName, PowershellFileCreatedTimestamp=TimeGenerated, FileName, FileSize, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFolderPath, InitiatingProcessId
) on VirtualMachineName
// We need to filter by time sadly, this is the only way to link events
| where PowershellFileCreatedTimestamp between (StartTime .. EndTime)
| project StartTime, EndTime, PowershellFileCreatedTimestamp, VirtualMachineName, Caller, CallerIpAddress, FileName, FileSize, InitiatingProcessId, InitiatingProcessAccountDomain, InitiatingProcessFolderPath
| join kind=inner(
DeviceEvents
| extend VirtualMachineName = tostring(split(DeviceName, ".")[0])
| where InitiatingProcessCommandLine has "-File"
| extend PowershellFileName = extract(@"\-File\s(script[0-9]{1,9}\.ps1)", 1, InitiatingProcessCommandLine)
| extend PSCommand = tostring(parse_json(AdditionalFields).Command)
| order by TimeGenerated asc
| where PSCommand != PowershellFileName
| summarize PowershellExecStart=min(TimeGenerated), PowershellExecEnd=max(TimeGenerated), make_list(PSCommand) by PowershellFileName, InitiatingProcessCommandLine
) on $left.FileName == $right.PowershellFileName
| project StartTime, EndTime, PowershellFileCreatedTimestamp, PowershellExecStart, PowershellExecEnd, PowershellFileName, PowershellScriptCommands=list_PSCommand, Caller, CallerIpAddress, InitiatingProcessCommandLine, PowershellFileSize=FileSize, VirtualMachineName
| order by StartTime asc
| extend ScriptFingerprintHash = hash_sha256(tostring(PowershellScriptCommands))
id: 55fbc363-6cc9-4201-bd68-d980b612082b
name: Azure VM Run Command linked with MDE
description: |
'Identifies any Azure VM Run Command operations and links these operations with
MDE host logging. Linking these two data sources provides hunting opportunities.
Logging from AzureActivity provides the IP address and UPN of the account that
invoked the command. Joining this with logging from MDE provides insights into
what cmdlets were loaded by the command.'
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceFileEvents
- DeviceEvents
tactics:
- LateralMovement
- CredentialAccess
relevantTechniques:
- T1570
- T1078.004
query: |
AzureActivity
// Isolate run command actions
| where OperationNameValue == "Microsoft.Compute/virtualMachines/runCommand/action"
// Confirm that the operation impacted a virtual machine
| where Authorization has "virtualMachines"
// Each runcommand operation consists of three events when successful, Started, Accepted (or Rejected), Successful (or Failed).
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), max(CallerIpAddress), make_list(ActivityStatusValue) by CorrelationId, Authorization, Caller
// Limit to Run Command executions that Succeeded
| where list_ActivityStatusValue has "Succeeded"
// Extract data from the Authorization field, allowing us to later extract the Caller (UPN) and CallerIpAddress
| extend Authorization_d = parse_json(Authorization)
| extend Scope = Authorization_d.scope
| extend Scope_s = split(Scope, "/")
| extend Subscription = tostring(Scope_s[2])
| extend VirtualMachineName = tostring(Scope_s[-1])
| project StartTime, EndTime, Subscription, VirtualMachineName, CorrelationId, Caller, CallerIpAddress=max_CallerIpAddress
| join kind=leftouter (
DeviceFileEvents
| where InitiatingProcessFileName == "RunCommandExtension.exe"
| extend VirtualMachineName = tostring(split(DeviceName, ".")[0])
| project VirtualMachineName, PowershellFileCreatedTimestamp=TimeGenerated, FileName, FileSize, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFolderPath, InitiatingProcessId
) on VirtualMachineName
// We need to filter by time sadly, this is the only way to link events
| where PowershellFileCreatedTimestamp between (StartTime .. EndTime)
| project StartTime, EndTime, PowershellFileCreatedTimestamp, VirtualMachineName, Caller, CallerIpAddress, FileName, FileSize, InitiatingProcessId, InitiatingProcessAccountDomain, InitiatingProcessFolderPath
| join kind=inner(
DeviceEvents
| extend VirtualMachineName = tostring(split(DeviceName, ".")[0])
| where InitiatingProcessCommandLine has "-File"
| extend PowershellFileName = extract(@"\-File\s(script[0-9]{1,9}\.ps1)", 1, InitiatingProcessCommandLine)
| extend PSCommand = tostring(parse_json(Additi
| Sentinel Table | Notes |
|---|---|
AzureActivity | Ensure this data connector is enabled |
DeviceEvents | Ensure this data connector is enabled |
DeviceFileEvents | Ensure this data connector is enabled |
Scenario: Scheduled VM Maintenance Task
Description: A legitimate scheduled task runs a Run Command on an Azure VM to perform routine maintenance, such as disk cleanup or log rotation.
Filter/Exclusion: Exclude commands that match known maintenance scripts (e.g., Cleanup-LogFiles.ps1, DiskCleanup.exe) or filter by command_id associated with scheduled tasks.
Scenario: Admin Performing Remote Troubleshooting
Description: An admin uses Azure VM Run Command to execute diagnostic scripts or tools like WinRM or PowerShell to troubleshoot a VM remotely.
Filter/Exclusion: Filter by user account (e.g., [email protected]) or include a command_name like Troubleshoot-VM.ps1 in the allowed command list.
Scenario: Automated Patching Job
Description: A patching job, such as Microsoft Update or Windows Server Update Services (WSUS), triggers a Run Command to install updates on multiple VMs.
Filter/Exclusion: Exclude commands that match known patching scripts (e.g., Install-Updates.ps1) or filter by command_id associated with patching tools.
Scenario: Integration with Azure DevOps Pipeline
Description: A CI/CD pipeline uses Azure VM Run Command to deploy code or configure environments, often involving tools like Azure DevOps CLI or Ansible.
Filter/Exclusion: Filter by command_name such as Deploy-Application.ps1 or include the pipeline’s service account (e.g., [email protected]) in the allowed list.
Scenario: Security Tool Configuration via Run Command
Description: A security tool like Microsoft Defender for Endpoint or CrowdStrike is configured via Run Command to install or update its