Detection rules › Kusto

Azure VM Run Command operations executing a unique PowerShell script

Severity
medium
Time window
1d
Group by
Authorization, Caller, CorrelationId, FileName, PowershellFileName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

Event coverage

Rule body kusto

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 binding: totals

let totals = toscalar (RunCommandData
| summarize count());

Derived from RunCommandData.

Let binding: hashTotals

let hashTotals = RunCommandData
| summarize HashCount=count() by ScriptFingerprintHash;

Derived from RunCommandData.

The stages below define let RunCommandData (the rule's main pipeline source).

Stage 1: source

AzureActivity

Stage 2: where

| where OperationNameValue =~ "Microsoft.Compute/virtualMachines/runCommand/action"

Stage 3: where

| where Authorization has "virtualMachines"

Stage 4: summarize

| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), max(CallerIpAddress), make_list(ActivityStatusValue) by CorrelationId, Authorization, Caller

Stage 5: where

| where list_ActivityStatusValue has_any ("Succeeded", "Success")

Stage 6: extend (5 consecutive steps)

| 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])

Stage 7: project

| project StartTime, EndTime, Subscription, VirtualMachineName, CorrelationId, Caller, CallerIpAddress=max_CallerIpAddress, Scope

Stage 8: join

| 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

Stage 9: where

| where PowershellFileCreatedTimestamp between (StartTime .. EndTime)

Stage 10: project

| project StartTime, EndTime, PowershellFileCreatedTimestamp, VirtualMachineName, Caller, CallerIpAddress, FileName, FileSize, InitiatingProcessId, InitiatingProcessAccountDomain, InitiatingProcessFolderPath, Scope

Stage 11: join

| 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)
    | where isnotempty(PowershellFileName)
    | extend PSCommand = tostring(parse_json(AdditionalFields).Command)
    | where PSCommand != PowershellFileName 
    | 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

Stage 12: project

| project StartTime, EndTime, PowershellFileCreatedTimestamp, PowershellExecStartTime, PowershellExecEnd, PowershellFileName, PowershellScriptCommands=list_PSCommand, Caller, CallerIpAddress, InitiatingProcessCommandLine, PowershellFileSize=FileSize, VirtualMachineName, Scope

Stage 13: sort

| order by StartTime asc

Stage 14: extend

| extend TempFingerprintString = strcat(PowershellScriptCommands, PowershellFileSize)

Stage 15: extend

| extend ScriptFingerprintHash = hash_sha256(tostring(PowershellScriptCommands))

The stages below run on RunCommandData (the outer pipeline).

Stage 16: join

RunCommandData
| join kind=leftouter (
hashTotals
) on ScriptFingerprintHash

Stage 17: extend

| extend Prevalence = toreal(HashCount) / toreal(totals) * 100

References totals (defined above).

Stage 18: where

| where HashCount == 1

Stage 19: extend

| extend timestamp = StartTime

Stage 20: extend

| extend CallerName = tostring(split(Caller, "@")[0]), CallerUPNSuffix = tostring(split(Caller, "@")[1])

Stage 21: project

| project timestamp, StartTime, EndTime, PowershellFileName, VirtualMachineName, Caller, CallerName, CallerUPNSuffix, CallerIpAddress, PowershellScriptCommands, PowershellFileSize, ScriptFingerprintHash, Prevalence, Scope

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Authorizationmatch
  • virtualMachines transforms: term
HashCounteq
  • 1 transforms: cased
InitiatingProcessCommandLinematch
  • -File transforms: term
InitiatingProcessFileNameeq
  • RunCommandExtension.exe transforms: cased
OperationNameValueeq
  • Microsoft.Compute/virtualMachines/runCommand/action
PSCommandne
  • PowershellFileName transforms: cased
PowershellFileCreatedTimestampge
  • StartTime
PowershellFileCreatedTimestample
  • EndTime
PowershellFileNameis_not_null
  • (no value, null check)
list_ActivityStatusValuematch
  • Succeeded
  • Success

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Callerproject
CallerIpAddressproject
CallerNameproject
CallerUPNSuffixproject
EndTimeproject
PowershellFileNameproject
PowershellFileSizeproject
PowershellScriptCommandsproject
Prevalenceproject
Scopeproject
ScriptFingerprintHashproject
StartTimeproject
VirtualMachineNameproject
timestampproject