Detections/AzureActivity/RareRunCommandPowerShellScript.yaml (80 lines of code) (raw):

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