Hunting Playbook: PowerShell Abuse in Microsoft Defender XDR.
The process was signed by Microsoft.
The command window opened for only a moment. No malware name appeared. No ransomware note was created. Nothing obvious exploded on the endpoint.
But PowerShell had executed with unusual arguments, encoded content and a parent process that did not fit normal administration.
Agent Foskett did not investigate the file name alone.
He investigated the behaviour around it.
Lesson overview
Learn how to hunt for PowerShell abuse by analysing process telemetry, command-line arguments, encoded commands, parent-child process relationships and follow-on activity in Microsoft Defender XDR.
Why PowerShell abuse matters
The PowerShell abuse hunting workflow
Step 1 — Find recent PowerShell execution
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
let TimeFrame = 7d;
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| project Timestamp,
DeviceName,
AccountName,
FileName,
ProcessCommandLine,
InitiatingProcessFileName,
InitiatingProcessCommandLine
| order by Timestamp descStep 2 — Hunt for suspicious PowerShell arguments
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let TimeFrame = 7d;
let SuspiciousArguments = dynamic(["-enc", "-encodedcommand", "-nop", "-w hidden", "bypass", "downloadstring", "iex", "invoke-expression"]);
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any (SuspiciousArguments)
| project Timestamp,
DeviceName,
AccountName,
ProcessCommandLine,
InitiatingProcessFileName,
InitiatingProcessCommandLine
| order by Timestamp descStep 3 — Identify unusual parent processes
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let TimeFrame = 14d;
let SuspiciousParents = dynamic(["winword.exe", "excel.exe", "outlook.exe", "chrome.exe", "msedge.exe", "wscript.exe", "cscript.exe", "mshta.exe"]);
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where InitiatingProcessFileName in~ (SuspiciousParents)
| project Timestamp,
DeviceName,
AccountName,
InitiatingProcessFileName,
InitiatingProcessCommandLine,
ProcessCommandLine
| order by Timestamp descStep 4 — Summarise PowerShell activity by device
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
let TimeFrame = 7d;
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize PowerShellEvents = count(),
Accounts = make_set(AccountName, 20),
ParentProcesses = make_set(InitiatingProcessFileName, 20),
ExampleCommands = make_set(ProcessCommandLine, 10),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by DeviceName
| order by PowerShellEvents descStep 5 — Correlate PowerShell with network activity
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
let TimeFrame = 7d;
let PowerShellProcesses =
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| project DeviceId,
DeviceName,
ProcessTime = Timestamp,
AccountName,
ProcessCommandLine,
ProcessId;
PowerShellProcesses
| join kind=inner (
DeviceNetworkEvents
| where Timestamp > ago(TimeFrame)
| project DeviceId,
NetworkTime = Timestamp,
InitiatingProcessId,
RemoteUrl,
RemoteIP,
RemotePort,
ActionType
) on DeviceId
| where NetworkTime between (ProcessTime .. ProcessTime + 10m)
| project ProcessTime,
NetworkTime,
DeviceName,
AccountName,
ProcessCommandLine,
RemoteUrl,
RemoteIP,
RemotePort,
ActionType
| order by NetworkTime descStep 6 — Build a PowerShell investigation timeline
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
let TimeFrame = 7d;
let TargetDevice = "DEVICE-NAME";
union
(
DeviceProcessEvents
| where Timestamp > ago(TimeFrame)
| where DeviceName =~ TargetDevice
| where FileName in~ ("powershell.exe", "pwsh.exe") or InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
| project Timestamp, EventType = "Process", DeviceName, AccountName, Evidence = ProcessCommandLine
),
(
DeviceNetworkEvents
| where Timestamp > ago(TimeFrame)
| where DeviceName =~ TargetDevice
| where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
| project Timestamp, EventType = "Network", DeviceName, AccountName = InitiatingProcessAccountName, Evidence = strcat(RemoteUrl, " ", RemoteIP)
),
(
DeviceFileEvents
| where Timestamp > ago(TimeFrame)
| where DeviceName =~ TargetDevice
| where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
| project Timestamp, EventType = "File", DeviceName, AccountName = InitiatingProcessAccountName, Evidence = FileName
)
| order by Timestamp ascCommon false positives
Real-world investigation
Investigation checklist
Related Agent Foskett Academy lessons
Coming next
Final thought
Hunting PowerShell Abuse in Microsoft Defender XDR
Agent Foskett Academy Lesson 105 teaches defenders how to investigate PowerShell abuse, encoded commands, process telemetry and command-line activity in Microsoft Defender XDR.
Microsoft Defender XDR PowerShell abuse investigation playbook
This hunting playbook explains how to review PowerShell execution, suspicious arguments, parent-child process relationships, network activity and endpoint timelines during Windows compromise investigations.
Learn to hunt malicious PowerShell with KQL
Defenders can use KQL, DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents and Microsoft Defender XDR telemetry to detect suspicious PowerShell execution and post-compromise attacker behaviour.
