Detection rules › Kusto

Suspicious Powershell Commandlet Executed

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

This analytic rule detects when a suspicious PowerShell commandlet is executed on a host. Threat actors often use PowerShell to execute commands and scripts to move laterally, escalate privileges, and exfiltrate data.

MITRE ATT&CK coverage

Event coverage

Rule body kusto

id: b5153fb3-ada9-4ce4-9131-79c771efb50d
name: Suspicious Powershell Commandlet Executed 
description: |
  This analytic rule detects when a suspicious PowerShell commandlet is executed on a host. Threat actors often use PowerShell to execute commands and scripts to move laterally, escalate privileges, and exfiltrate data.
severity: Medium
status: Available 
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  // Adjust the list of suspicious commandlets as needed
  let SuspiciousPowerShellCommandList = dynamic(["Get-ADUserResultantPasswordPolicy",
    "Get-DomainPolicy",
    "Get-DomainUser",
    "Get-DomainComputer",
    "Get-DomainController",
    "Get-DomainGroup",
    "Get-DomainTrust",
    "Get-ADTrust",
    "Get-ForestTrust"
    ]);
  DeviceEvents
  | where ActionType == "PowerShellCommand"
  | extend Commandlet = tostring(parse_json(AdditionalFields).Command)
  | where Commandlet has_any (SuspiciousPowerShellCommandList)
  | project TimeGenerated, DeviceName, LocalIP, InitiatingProcessAccountUpn, InitiatingProcessId, InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine
  | extend Username = tostring(split(InitiatingProcessAccountUpn, '@')[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@')[1])
  | extend DvcHostname = tostring(split(DeviceName, '.')[0]), DvcDomain = tostring(strcat_array(array_slice(split(DeviceName, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: DvcHostname
      - identifier: DnsDomain
        columnName: DvcDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: LocalIP
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Username
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: InitiatingProcessId
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Suspicious PowerShell Commandlet Executed on {{DvcHostname}} ({{LocalIP}}) by ({{InitiatingProcessAccountUpn}})"
  alertDescriptionFormat: "Suspicious PowerShell Commandlet by Process '{{InitiatingProcessFileName}}' ProcessId: '{{InitiatingProcessId}}' with commandline {{InitiatingProcessCommandLine}} was executed."
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let binding: SuspiciousPowerShellCommandList

let SuspiciousPowerShellCommandList = dynamic(["Get-ADUserResultantPasswordPolicy",
  "Get-DomainPolicy",
  "Get-DomainUser",
  "Get-DomainComputer",
  "Get-DomainController",
  "Get-DomainGroup",
  "Get-DomainTrust",
  "Get-ADTrust",
  "Get-ForestTrust"
  ]);

Stage 1: source

DeviceEvents

Stage 2: where

| where ActionType == "PowerShellCommand"

Stage 3: extend

| extend Commandlet = tostring(parse_json(AdditionalFields).Command)

Stage 4: where

| where Commandlet has_any (SuspiciousPowerShellCommandList)

References SuspiciousPowerShellCommandList (defined above).

Stage 5: project

| project TimeGenerated, DeviceName, LocalIP, InitiatingProcessAccountUpn, InitiatingProcessId, InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine

Stage 6: extend

| extend Username = tostring(split(InitiatingProcessAccountUpn, '@')[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@')[1])

Stage 7: extend

| extend DvcHostname = tostring(split(DeviceName, '.')[0]), DvcDomain = tostring(strcat_array(array_slice(split(DeviceName, '.'), 1, -1), '.'))

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
ActionTypeeq
  • PowerShellCommand transforms: cased corpus 2 (kusto 2)
Commandletmatch
  • Get-ADTrust
  • Get-ADUserResultantPasswordPolicy
  • Get-DomainComputer
  • Get-DomainController
  • Get-DomainGroup
  • Get-DomainPolicy
  • Get-DomainTrust
  • Get-DomainUser
  • Get-ForestTrust

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
DeviceNameproject
InitiatingProcessAccountUpnproject
InitiatingProcessCommandLineproject
InitiatingProcessFileNameproject
InitiatingProcessFolderPathproject
InitiatingProcessIdproject
LocalIPproject
TimeGeneratedproject
UPNSuffixextend
Usernameextend
DvcDomainextend
DvcHostnameextend