Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Azure VM Run Command operations executing a unique PowerShell script

Back
Id5239248b-abfb-4c6a-8177-b104ade5db56
RulenameAzure VM Run Command operations executing a unique PowerShell script
DescriptionIdentifies 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.
SeverityMedium
TacticsLateralMovement
Execution
TechniquesT1570
T1059.001
Required data connectorsAzureActivity
MicrosoftThreatProtection
KindScheduled
Query frequency1d
Query period1d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Detections/AzureActivity/RareRunCommandPowerShellScript.yaml
Version1.0.8
Arm template5239248b-abfb-4c6a-8177-b104ade5db56.json
Deploy To Azure
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
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.'  
kind: Scheduled
tactics:
- LateralMovement
- Execution
requiredDataConnectors:
- connectorId: AzureActivity
  dataTypes:
  - AzureActivity
- connectorId: MicrosoftThreatProtection
  dataTypes:
  - DeviceFileEvents
  - DeviceEvents
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/AzureActivity/RareRunCommandPowerShellScript.yaml
severity: Medium
name: Azure VM Run Command operations executing a unique PowerShell script
metadata:
  support:
    tier: Community
  author:
    name: Microsoft Security Research
  categories:
    domains:
    - Security - Others
    - Identity
  source:
    kind: Community
triggerThreshold: 0
queryPeriod: 1d
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  
relevantTechniques:
- T1570
- T1059.001
id: 5239248b-abfb-4c6a-8177-b104ade5db56
queryFrequency: 1d
entityMappings:
- entityType: Account
  fieldMappings:
  - columnName: Caller
    identifier: FullName
  - columnName: CallerName
    identifier: Name
  - columnName: CallerUPNSuffix
    identifier: UPNSuffix
- entityType: IP
  fieldMappings:
  - columnName: CallerIpAddress
    identifier: Address
- entityType: Host
  fieldMappings:
  - columnName: VirtualMachineName
    identifier: HostName
  - columnName: Scope
    identifier: AzureID
triggerOperator: gt
version: 1.0.8