Detection rules › Kusto
Azure VM Run Command operations executing a unique PowerShell script
Identifies when Azure Run command is used to execute a PowerShell script on a VM that is unique. The uniqueness of the PowerShell script is determined by taking a combined hash of the cmdLets it imports and the file size of the PowerShell script. Alerts from this detection indicate a unique PowerShell was executed in your environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Lateral Movement |
Telemetry coverage
Rule body
id: 5239248b-abfb-4c6a-8177-b104ade5db56
name: Azure VM Run Command operations executing a unique PowerShell script
description: |
'Identifies when Azure Run command is used to execute a PowerShell script on a VM that is unique.
The uniqueness of the PowerShell script is determined by taking a combined hash of the cmdLets it imports and the file size of the PowerShell script. Alerts from this detection indicate a unique PowerShell was executed in your environment.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceFileEvents
- DeviceEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
- Execution
relevantTechniques:
- T1570
- T1059.001
query: |
let RunCommandData = materialize ( 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, StartTimeed, 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_any ("Succeeded", "Success")
// 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, Scope
| 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, Scope
| join kind=inner(
DeviceEvents
| extend VirtualMachineName = tostring(split(DeviceName, ".")[0])
| where InitiatingProcessCommandLine has "-File"
// Extract the script name based on the structure used by the RunCommand extension
| extend PowershellFileName = extract(@"\-File\s(script[0-9]{1,9}\.ps1)", 1, InitiatingProcessCommandLine)
// Discard results that didn't successfully extract, these are not run command related
| where isnotempty(PowershellFileName)
| extend PSCommand = tostring(parse_json(AdditionalFields).Command)
// The first execution of PowerShell will be the RunCommand script itself, we can discard this as it will break our hash later
| where PSCommand != PowershellFileName
// Now we normalise the cmdlets, we're aiming to hash them to find scripts using rare combinations
| extend PSCommand = toupper(PSCommand)
| order by PSCommand asc
| summarize PowershellExecStartTime=min(TimeGenerated), PowershellExecEnd=max(TimeGenerated), make_list(PSCommand) by PowershellFileName, InitiatingProcessCommandLine
) on $left.FileName == $right.PowershellFileName
| project StartTime, EndTime, PowershellFileCreatedTimestamp, PowershellExecStartTime, PowershellExecEnd, PowershellFileName, PowershellScriptCommands=list_PSCommand, Caller, CallerIpAddress, InitiatingProcessCommandLine, PowershellFileSize=FileSize, VirtualMachineName, Scope
| order by StartTime asc
// We generate the hash based on the cmdlets called and the size of the powershell script
| extend TempFingerprintString = strcat(PowershellScriptCommands, PowershellFileSize)
| extend ScriptFingerprintHash = hash_sha256(tostring(PowershellScriptCommands)));
let totals = toscalar (RunCommandData
| summarize count());
let hashTotals = RunCommandData
| summarize HashCount=count() by ScriptFingerprintHash;
RunCommandData
| join kind=leftouter (
hashTotals
) on ScriptFingerprintHash
// Calculate prevalence, while we don't need this, it may be useful for responders to know how rare this script is in relation to normal activity
| extend Prevalence = toreal(HashCount) / toreal(totals) * 100
// Where the hash was only ever seen once.
| where HashCount == 1
| extend timestamp = StartTime
| extend CallerName = tostring(split(Caller, "@")[0]), CallerUPNSuffix = tostring(split(Caller, "@")[1])
| project timestamp, StartTime, EndTime, PowershellFileName, VirtualMachineName, Caller, CallerName, CallerUPNSuffix, CallerIpAddress, PowershellScriptCommands, PowershellFileSize, ScriptFingerprintHash, Prevalence, Scope
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Caller
- identifier: Name
columnName: CallerName
- identifier: UPNSuffix
columnName: CallerUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: VirtualMachineName
- identifier: AzureID
columnName: Scope
version: 1.0.8
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
let RunCommandData is inlined into the numbered stages below.
Let binding: totals
let totals = toscalar (RunCommandData
| summarize count());
Let binding: hashTotals
let hashTotals = RunCommandData
| summarize HashCount=count() by ScriptFingerprintHash;
Stage 1: source
let RunCommandData
Stage 2: source
let hashTotals
Stage 3: source
AzureActivity
Stage 4: where
where OperationNameValue =~ "Microsoft.Compute/virtualMachines/runCommand/action"
Stage 5: where
where Authorization contains "virtualMachines"
Stage 6: summarize
summarize EndTime, StartTime by CorrelationId, Authorization, Caller
Stage 7: where
where (list_ActivityStatusValue contains "Succeeded" or list_ActivityStatusValue contains "Success")
Stage 8: extend (5 consecutive steps)
extend Authorization_d, Scope, Scope_s, Subscription, VirtualMachineName
Stage 9: project
project Caller, CallerIpAddress, CorrelationId, EndTime, Scope, StartTime, Subscription, VirtualMachineName
Stage 10: join
join kind=leftouter (DeviceFileEvents) on VirtualMachineName
Stage 11: where
where PowershellFileCreatedTimestamp >= "StartTime" and PowershellFileCreatedTimestamp <= "EndTime"
Stage 12: project
project Caller, CallerIpAddress, EndTime, FileName, FileSize, InitiatingProcessAccountDomain, InitiatingProcessFolderPath, InitiatingProcessId, PowershellFileCreatedTimestamp, Scope, StartTime, VirtualMachineName
Stage 13: join
join kind=inner (DeviceEvents) on FileName, PowershellFileName
Stage 14: project
project Caller, CallerIpAddress, EndTime, InitiatingProcessCommandLine, PowershellExecEnd, PowershellExecStartTime, PowershellFileCreatedTimestamp, PowershellFileName, PowershellFileSize, PowershellScriptCommands, Scope, StartTime, VirtualMachineName
Stage 15: sort
sort by StartTime
Stage 16: extend
extend TempFingerprintString
Stage 17: extend
extend ScriptFingerprintHash
Stage 18: join
join kind=leftouter (hashTotals) on ScriptFingerprintHash
Stage 19: extend
extend Prevalence
Stage 20: where
where HashCount == 1
Stage 21: extend
extend timestamp
Stage 22: extend
extend CallerName, CallerUPNSuffix
Stage 23: project
project Caller, CallerIpAddress, CallerName, CallerUPNSuffix, EndTime, PowershellFileName, PowershellFileSize, PowershellScriptCommands, Prevalence, Scope, ScriptFingerprintHash, StartTime, VirtualMachineName, timestamp
Indicators
These rows show field, operator, and value matches.
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Caller | project |
CallerIpAddress | project |
CallerName | project |
CallerUPNSuffix | project |
EndTime | project |
PowershellFileName | project |
PowershellFileSize | project |
PowershellScriptCommands | project |
Prevalence | project |
Scope | project |
ScriptFingerprintHash | project |
StartTime | project |
VirtualMachineName | project |
timestamp | project |