← Blog · · df00tech

How to Detect PowerShell Execution Attacks: T1059.001 Complete Guide

PowerShell T1059.001 KQL SPL Microsoft Sentinel Splunk detection engineering

PowerShell is used in roughly 75% of fileless malware attacks. It’s signed by Microsoft, pre-installed on every modern Windows host, and exposes the entire .NET API to whoever can type a command. For an adversary, that is a turnkey offensive framework that never needs to touch disk.

This guide walks through how to build a practical PowerShell detection rule for MITRE ATT&CK T1059.001, with KQL for Microsoft Sentinel and SPL for Splunk pulled directly from the df00tech detection library.

1. Why PowerShell Is the #1 Windows Attack Surface

Native scripting environments are attractive targets because they bypass most traditional application controls. PowerShell is the worst offender:

  • It’s signed by Microsoft, so allowlisting tools trust it by default.
  • It can execute directly from memory via Invoke-Expression, avoiding on-disk detection.
  • It has direct access to the Win32 and .NET APIs, so anything a native binary can do, a one-liner can do too.
  • Remoting (WinRM) turns it into a built-in lateral movement tool — see T1021.006.

Every major ransomware crew and APT uses PowerShell somewhere in their chain — often for the initial loader, the credential dumper, or the lateral movement stage. It’s the parent technique for a reason. The broader T1059 detection covers its siblings like Windows Command Shell (T1059.003) and Visual Basic (T1059.005), but PowerShell is where you should invest first.

2. Encoded Commands and Download Cradles

Two patterns dominate malicious PowerShell usage in the wild: -EncodedCommand and the download cradle.

Encoded commands (-EncodedCommand, -enc, -e, -ec) accept a Base64-encoded UTF-16LE string and execute it. Attackers use them to hide command-line content from casual log review, cram a full payload into a single scheduled task, and defeat signature-based detection that looks for plaintext tokens.

Download cradles use Net.WebClient, Invoke-WebRequest, Start-BitsTransfer, or Invoke-RestMethod to pull a second-stage payload from a remote URL and pipe it straight to Invoke-Expression. Nothing touches disk. This is the canonical Cobalt Strike / Metasploit / Empire stager pattern.

Here is the production KQL from the df00tech T1059.001 detection:

let SuspiciousPatterns = dynamic([
  "-EncodedCommand", "-enc ", "-e ", "-ec ",
  "Invoke-WebRequest", "IWR ", "Invoke-RestMethod",
  "Net.WebClient", "DownloadString", "DownloadFile", "DownloadData",
  "Start-BitsTransfer",
  "AmsiUtils", "amsiInitFailed", "SetProtectionLevel",
  "Invoke-Expression", "IEX(", "IEX ",
  "-ExecutionPolicy Bypass", "-ep bypass", "-ep unrestricted",
  "-WindowStyle Hidden", "-w hidden", "-windowstyle h",
  "[Convert]::FromBase64String", "[System.Convert]::FromBase64String",
  "Invoke-Mimikatz", "Invoke-Shellcode",
  "New-Object IO.MemoryStream", "IO.Compression",
  "bitsadmin", "certutil -urlcache"
]);
DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName =~ "powershell.exe" or FileName =~ "pwsh.exe"
| where ProcessCommandLine has_any (SuspiciousPatterns)
| extend EncodedCmd = ProcessCommandLine has_any ("-EncodedCommand", "-enc ", "-e ", "-ec ")
| extend DownloadCradle = ProcessCommandLine has_any ("Invoke-WebRequest", "Net.WebClient", "DownloadString", "DownloadFile", "IWR ", "Start-BitsTransfer")
| extend AmsiBypass = ProcessCommandLine has_any ("AmsiUtils", "amsiInitFailed", "SetProtectionLevel")
| extend PolicyBypass = ProcessCommandLine has_any ("-ExecutionPolicy Bypass", "-ep bypass")
| extend HiddenWindow = ProcessCommandLine has_any ("-WindowStyle Hidden", "-w hidden")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
         InitiatingProcessFileName, InitiatingProcessCommandLine,
         EncodedCmd, DownloadCradle, AmsiBypass, PolicyBypass, HiddenWindow
| sort by Timestamp desc

What this catches: Any PowerShell or pwsh.exe launch containing one of the high-signal patterns. The extend columns flag which indicator matched, so an analyst can see at a glance whether they’re looking at an encoded command, a download cradle, an AMSI bypass, or a policy bypass — usually several at once.

Why the patterns matter: -EncodedCommand is load-bearing for obfuscation. Net.WebClient/DownloadString is the download cradle signature. AmsiUtils and amsiInitFailed are the names of the .NET fields used in the most common AMSI bypass technique (covered below). -ExecutionPolicy Bypass combined with -WindowStyle Hidden is the canonical malware dropper invocation.

False positives to expect: SCCM and Intune deployment scripts legitimately use -EncodedCommand. Monitoring agents (Datadog, SolarWinds) call Invoke-WebRequest to health-check URLs. IT automation platforms (Ansible WinRM, PDQ Deploy, Chef, Puppet) run PowerShell remotely. Baseline before alerting — track which parent processes are expected to spawn PowerShell in your environment, and exclude by exact parent + account, never by pattern.

3. AMSI Bypass: Disabling Defense from Inside the Process

The Antimalware Scan Interface (AMSI) is Windows’ in-memory script inspection layer. When PowerShell is about to execute a script block, AMSI hands the deobfuscated content to Defender for scanning. That’s why obfuscating an encoded Mimikatz payload isn’t enough — AMSI sees the decoded version.

So attackers disable it. The most common bypass patches System.Management.Automation.AmsiUtils.amsiInitFailed to true via .NET reflection, effectively telling PowerShell “don’t bother calling AMSI, it’s already broken.” Once patched, the rest of the session runs without script scanning.

The KQL query above flags this with the AmsiBypass column when it sees AmsiUtils, amsiInitFailed, or SetProtectionLevel on the command line. These strings are essentially never present in benign scripts — any hit is worth immediate triage.

For Splunk environments, the equivalent detection runs against Sysmon Event ID 1 and adds a cumulative suspicion score so analysts can prioritise multi-indicator events:

index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1
  (Image="*\\powershell.exe" OR Image="*\\pwsh.exe")
| eval CommandLine=lower(CommandLine)
| eval EncodedCmd=if(match(CommandLine, "(-encodedcommand|-enc\s|-e\s|-ec\s)"), 1, 0)
| eval DownloadCradle=if(match(CommandLine, "(invoke-webrequest|iwr\s|net\.webclient|downloadstring|downloadfile|downloaddata|start-bitstransfer|invoke-restmethod)"), 1, 0)
| eval AmsiBypass=if(match(CommandLine, "(amsiutils|amsiinitfailed|setprotectionlevel)"), 1, 0)
| eval PolicyBypass=if(match(CommandLine, "(-executionpolicy\s+bypass|-ep\s+bypass|-ep\s+unrestricted)"), 1, 0)
| eval HiddenWindow=if(match(CommandLine, "(-windowstyle\s+hidden|-w\s+hidden)"), 1, 0)
| eval InvokeExpression=if(match(CommandLine, "(invoke-expression|iex\(|iex\s)"), 1, 0)
| eval SuspicionScore=EncodedCmd + DownloadCradle + AmsiBypass + PolicyBypass + HiddenWindow + InvokeExpression
| where SuspicionScore > 0
| table _time, host, User, Image, CommandLine, ParentImage, ParentCommandLine, EncodedCmd, DownloadCradle, AmsiBypass, PolicyBypass, HiddenWindow, InvokeExpression, SuspicionScore
| sort - _time

What this catches: Same set of indicators as the KQL rule, but scored. A command line with EncodedCmd=1, DownloadCradle=1, AmsiBypass=1 has a suspicion score of 3 and should be treated as near-certainly malicious. A lone PolicyBypass=1 on an SCCM-parented process is almost always noise.

Why scoring matters: Single-indicator alerts from PowerShell are a false positive generator. Multi-indicator hits — encoded command and download cradle, or AMSI bypass and hidden window — are where real attacks live. The score lets you route high-signal events to the SOC and leave low-signal events for scheduled hunting.

False positives to expect: WSUS and third-party patch agents running hidden-window PowerShell, SOC scripts that legitimately use Invoke-Expression for log parsing, and RMM tools (ConnectWise Automate, NinjaRMM) running policy-bypass scripts for remote remediation.

4. Script Block Logging: Your Most Valuable Data Source

Command-line detection catches the launch. Script Block Logging catches the content — including the decoded version of any -EncodedCommand and the reconstructed script after obfuscation is unwrapped.

Turn it on. Always.

Via GPO: Administrative Templates > Windows Components > Windows PowerShell > Turn on Script Block Logging. This writes to Microsoft-Windows-PowerShell/Operational, Event ID 4104. Every script block that PowerShell compiles is logged — including Base64-decoded payloads, reflectively loaded assemblies, and fileless in-memory code.

When a command-line detection fires, the investigation always starts at the matching 4104 event. That is where you see the actual payload instead of -enc JABxAD0A.... The T1059.001 detection evidence-collection playbook specifies exactly which logs to pull, including Event ID 4103 (Module Logging), Event ID 1116 (Windows Defender AMSI detections), and Event ID 4104 (ScriptBlock).

5. Detection at Scale and Tuning

The single command-line rule above will produce noise in any environment with active PowerShell automation. Here is how to scale it without drowning your analysts.

Hunt for high-volume encoded command use

This hunting query groups encoded PowerShell launches by account and parent process over a seven-day window, surfacing compromised automation accounts and misbehaving deployment pipelines:

DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has "-enc" or ProcessCommandLine has "-EncodedCommand"
| summarize Count=count(), Devices=dcount(DeviceName), Earliest=min(Timestamp), Latest=max(Timestamp) by AccountName, InitiatingProcessFileName
| where Count > 3
| sort by Count desc

What this catches: Accounts or parent processes that consistently launch encoded PowerShell. In a clean environment, this should be a very short list — mostly SCCM, Intune, and your own managed automation. Anything unexpected on that list is worth a conversation. A user account running encoded PowerShell from Word or Outlook is a phishing payload. A new service account showing up is a compromised host or rogue install.

Why this pattern matters: Single-event detection misses slow-and-low automation abuse. Baselining who runs encoded commands over time catches attacker persistence that blends into your noise floor.

Tuning principles

Four rules that keep this detection usable:

  1. Require multi-indicator scoring for automated alerting. Route SuspicionScore >= 2 to the SOC queue. Keep SuspicionScore = 1 on a hunting dashboard.
  2. Exclude on exact parent + account, never on pattern. Excluding ccmexec.exe + SYSTEM is fine. Excluding anything with the word “update” in it is how you miss the next SolarWinds.
  3. Pipe suspicious PowerShell into credential-dumping detection. Mimikatz is still the #1 reason attackers run PowerShell — cross-reference hits against T1003.001 (LSASS dumping) detections on the same host within a 30-minute window.
  4. Track obfuscation trends separately. Heavy Base64, character replacement, and backtick obfuscation are their own signal. The T1027 (Obfuscated Files or Information) detection catches these patterns independently of what command is actually being run.

What to do when it fires

Triage a hit by decoding any Base64 payload with:

[System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('<encoded_string>'))

Check the parent process — PowerShell spawned by winword.exe, excel.exe, mshta.exe, or wscript.exe is almost always a phishing payload. Check for follow-on network connections from the PowerShell PID. Check for child processes spawned by the PowerShell PID — particularly cmd.exe, rundll32.exe, certutil.exe, or bitsadmin.exe, which indicate LOLBin staging. The full T1059.001 playbook covers escalation criteria and containment steps.

Wrapping Up

A PowerShell detection rule is not one rule — it’s a layered set of detections covering command-line patterns, script block content, network behaviour, and parent/child relationships. The KQL and SPL queries above are your starting point. Add ScriptBlock Logging, feed hits into your credential-theft and lateral-movement detections, and tune aggressively on parent + account combinations.

Browse the full df00tech detection library — 700+ KQL and SPL queries mapped to MITRE ATT&CK, free forever.