Azure VM Run Command operations executing a unique PowerShell script
Id | 5239248b-abfb-4c6a-8177-b104ade5db56 |
Rulename | 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 |
Tactics | LateralMovement Execution |
Techniques | T1570 T1059.001 |
Required data connectors | AzureActivity MicrosoftThreatProtection |
Kind | Scheduled |
Query frequency | 1d |
Query period | 1d |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Detections/AzureActivity/RareRunCommandPowerShellScript.yaml |
Version | 1.0.8 |
Arm template | 5239248b-abfb-4c6a-8177-b104ade5db56.json |
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
name: Azure VM Run Command operations executing a unique PowerShell script
requiredDataConnectors:
- dataTypes:
- AzureActivity
connectorId: AzureActivity
- dataTypes:
- DeviceFileEvents
- DeviceEvents
connectorId: MicrosoftThreatProtection
entityMappings:
- fieldMappings:
- identifier: FullName
columnName: Caller
- identifier: Name
columnName: CallerName
- identifier: UPNSuffix
columnName: CallerUPNSuffix
entityType: Account
- fieldMappings:
- identifier: Address
columnName: CallerIpAddress
entityType: IP
- fieldMappings:
- identifier: HostName
columnName: VirtualMachineName
- identifier: AzureID
columnName: Scope
entityType: Host
triggerThreshold: 0
id: 5239248b-abfb-4c6a-8177-b104ade5db56
tactics:
- LateralMovement
- Execution
version: 1.0.8
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/AzureActivity/RareRunCommandPowerShellScript.yaml
queryPeriod: 1d
kind: Scheduled
metadata:
categories:
domains:
- Security - Others
- Identity
author:
name: Microsoft Security Research
support:
tier: Community
source:
kind: Community
queryFrequency: 1d
severity: Medium
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.'
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
triggerOperator: gt
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"apiVersion": "2024-01-01-preview",
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/5239248b-abfb-4c6a-8177-b104ade5db56')]",
"kind": "Scheduled",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/5239248b-abfb-4c6a-8177-b104ade5db56')]",
"properties": {
"alertRuleTemplateName": "5239248b-abfb-4c6a-8177-b104ade5db56",
"customDetails": null,
"description": "'Identifies when Azure Run command is used to execute a PowerShell script on a VM that is unique.\nThe 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.'\n",
"displayName": "Azure VM Run Command operations executing a unique PowerShell script",
"enabled": true,
"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"
}
]
}
],
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Detections/AzureActivity/RareRunCommandPowerShellScript.yaml",
"query": "let RunCommandData = materialize ( AzureActivity\n// Isolate run command actions\n| where OperationNameValue =~ \"Microsoft.Compute/virtualMachines/runCommand/action\"\n// Confirm that the operation impacted a virtual machine\n| where Authorization has \"virtualMachines\"\n// Each runcommand operation consists of three events when successful, StartTimeed, Accepted (or Rejected), Successful (or Failed).\n| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), max(CallerIpAddress), make_list(ActivityStatusValue) by CorrelationId, Authorization, Caller\n// Limit to Run Command executions that Succeeded\n| where list_ActivityStatusValue has_any (\"Succeeded\", \"Success\")\n// Extract data from the Authorization field, allowing us to later extract the Caller (UPN) and CallerIpAddress\n| extend Authorization_d = parse_json(Authorization)\n| extend Scope = Authorization_d.scope\n| extend Scope_s = split(Scope, \"/\")\n| extend Subscription = tostring(Scope_s[2])\n| extend VirtualMachineName = tostring(Scope_s[-1])\n| project StartTime, EndTime, Subscription, VirtualMachineName, CorrelationId, Caller, CallerIpAddress=max_CallerIpAddress, Scope\n| join kind=leftouter (\n DeviceFileEvents\n | where InitiatingProcessFileName == \"RunCommandExtension.exe\"\n | extend VirtualMachineName = tostring(split(DeviceName, \".\")[0])\n | project VirtualMachineName, PowershellFileCreatedTimestamp=TimeGenerated, FileName, FileSize, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFolderPath, InitiatingProcessId\n) on VirtualMachineName\n// We need to filter by time sadly, this is the only way to link events\n| where PowershellFileCreatedTimestamp between (StartTime .. EndTime)\n| project StartTime, EndTime, PowershellFileCreatedTimestamp, VirtualMachineName, Caller, CallerIpAddress, FileName, FileSize, InitiatingProcessId, InitiatingProcessAccountDomain, InitiatingProcessFolderPath, Scope\n| join kind=inner(\n DeviceEvents\n | extend VirtualMachineName = tostring(split(DeviceName, \".\")[0])\n | where InitiatingProcessCommandLine has \"-File\"\n // Extract the script name based on the structure used by the RunCommand extension\n | extend PowershellFileName = extract(@\"\\-File\\s(script[0-9]{1,9}\\.ps1)\", 1, InitiatingProcessCommandLine)\n // Discard results that didn't successfully extract, these are not run command related\n | where isnotempty(PowershellFileName)\n | extend PSCommand = tostring(parse_json(AdditionalFields).Command)\n // The first execution of PowerShell will be the RunCommand script itself, we can discard this as it will break our hash later\n | where PSCommand != PowershellFileName \n // Now we normalise the cmdlets, we're aiming to hash them to find scripts using rare combinations\n | extend PSCommand = toupper(PSCommand)\n | order by PSCommand asc\n | summarize PowershellExecStartTime=min(TimeGenerated), PowershellExecEnd=max(TimeGenerated), make_list(PSCommand) by PowershellFileName, InitiatingProcessCommandLine\n) on $left.FileName == $right.PowershellFileName\n| project StartTime, EndTime, PowershellFileCreatedTimestamp, PowershellExecStartTime, PowershellExecEnd, PowershellFileName, PowershellScriptCommands=list_PSCommand, Caller, CallerIpAddress, InitiatingProcessCommandLine, PowershellFileSize=FileSize, VirtualMachineName, Scope\n| order by StartTime asc \n// We generate the hash based on the cmdlets called and the size of the powershell script\n| extend TempFingerprintString = strcat(PowershellScriptCommands, PowershellFileSize)\n| extend ScriptFingerprintHash = hash_sha256(tostring(PowershellScriptCommands)));\nlet totals = toscalar (RunCommandData\n| summarize count());\nlet hashTotals = RunCommandData\n| summarize HashCount=count() by ScriptFingerprintHash;\nRunCommandData\n| join kind=leftouter (\nhashTotals\n) on ScriptFingerprintHash\n// 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\n| extend Prevalence = toreal(HashCount) / toreal(totals) * 100\n// Where the hash was only ever seen once.\n| where HashCount == 1\n| extend timestamp = StartTime\n| extend CallerName = tostring(split(Caller, \"@\")[0]), CallerUPNSuffix = tostring(split(Caller, \"@\")[1])\n| project timestamp, StartTime, EndTime, PowershellFileName, VirtualMachineName, Caller, CallerName, CallerUPNSuffix, CallerIpAddress, PowershellScriptCommands, PowershellFileSize, ScriptFingerprintHash, Prevalence, Scope\n",
"queryFrequency": "P1D",
"queryPeriod": "P1D",
"severity": "Medium",
"subTechniques": [
"T1059.001"
],
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"Execution",
"LateralMovement"
],
"techniques": [
"T1059",
"T1570"
],
"templateVersion": "1.0.8",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0
},
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
}
]
}