id: 55fbc363-6cc9-4201-bd68-d980b612082b name: Azure VM Run Command linked with MDE description: | 'Identifies any Azure VM Run Command operations and links these operations with MDE host logging. Linking these two data sources provides hunting opportunities. Logging from AzureActivity provides the IP address and UPN of the account that invoked the command. Joining this with logging from MDE provides insights into what cmdlets were loaded by the command.' requiredDataConnectors: - connectorId: AzureActivity dataTypes: - AzureActivity - connectorId: MicrosoftThreatProtection dataTypes: - DeviceFileEvents - DeviceEvents tactics: - LateralMovement - CredentialAccess relevantTechniques: - T1570 - T1078.004 query: | 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, Started, 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 "Succeeded" // 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 | 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 | 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) | extend PSCommand = tostring(parse_json(AdditionalFields).Command) | order by TimeGenerated asc | where PSCommand != PowershellFileName | summarize PowershellExecStart=min(TimeGenerated), PowershellExecEnd=max(TimeGenerated), make_list(PSCommand) by PowershellFileName, InitiatingProcessCommandLine ) on $left.FileName == $right.PowershellFileName | project StartTime, EndTime, PowershellFileCreatedTimestamp, PowershellExecStart, PowershellExecEnd, PowershellFileName, PowershellScriptCommands=list_PSCommand, Caller, CallerIpAddress, InitiatingProcessCommandLine, PowershellFileSize=FileSize, VirtualMachineName | order by StartTime asc | extend ScriptFingerprintHash = hash_sha256(tostring(PowershellScriptCommands)) entityMappings: - entityType: Account fieldMappings: - identifier: FullName columnName: AccountCustomEntity - entityType: IP fieldMappings: - identifier: Address columnName: IPCustomEntity