Skip to content

Tutorials

Hunting suspicious PowerShell with KQL in Defender XDR

Five advanced hunting queries that surface encoded commands, Office spawning PowerShell, download cradles and rare scripts, and how to triage each hit.

Published 4 min readReviewed by The Ledger EditorIntermediate

PowerShell is on every Windows machine, it is trusted, and it can download and run code without writing a file to disk. That makes it one of the favorite tools of attackers who prefer living off the land, and MITRE ATT&CK tracks it as T1059.001. It is also used all day by administrators and management software, so "PowerShell ran" is never an alert by itself.

This tutorial shows how to separate the two with KQL in Microsoft Defender XDR advanced hunting. You will build five queries, each aimed at a pattern that is common in attacks and uncommon in normal administration, and learn what to check when one of them returns results.

Before you start

You need access to advanced hunting in the Microsoft Defender portal (Hunting → Advanced hunting) and devices onboarded to Microsoft Defender for Endpoint. Advanced hunting keeps up to 30 days of Defender data. The queries use two tables:

  • DeviceProcessEvents: one row per process creation. The columns used here are Timestamp, DeviceName, AccountName, FileName (the process that started), ProcessCommandLine, InitiatingProcessFileName (its parent) and InitiatingProcessCommandLine.
  • DeviceNetworkEvents: network connections, with RemoteIP, RemoteUrl, RemoteIPType and the process that made them.

Microsoft documents every column of DeviceProcessEvents and DeviceNetworkEvents. The queries look for both powershell.exe (Windows PowerShell) and pwsh.exe (PowerShell 7).

The five hunts

  1. Encoded commands

    PowerShell accepts a whole script as a Base64 string through the -EncodedCommand parameter, which can be shortened to -enc or even -e. Administrators rarely need it; attackers use it to hide what they run and to avoid quoting problems.

    encoded-commands.kql
    DeviceProcessEvents
    | where Timestamp > ago(7d)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | where ProcessCommandLine matches regex @"(?i)\s[-/]e[a-z]*\s+[a-z0-9+/=]{20,}"
    | project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, ProcessCommandLine
    | order by Timestamp desc

    The highlighted line matches any parameter that starts with e, followed by a long Base64-looking string. Requiring at least 20 characters avoids matching -ExecutionPolicy Bypass, which also starts with e.

  2. Office and script hosts starting PowerShell

    Word, Excel and Outlook have no ordinary reason to start PowerShell. When they do, it is often a malicious macro or attachment. The same goes for mshta.exe and the Windows script hosts, which are common first stages.

    suspicious-parents.kql
    DeviceProcessEvents
    | where Timestamp > ago(7d)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | where InitiatingProcessFileName in~ ("winword.exe", "excel.exe", "powerpnt.exe",
        "outlook.exe", "mshta.exe", "wscript.exe", "cscript.exe")
    | project Timestamp, DeviceName, AccountName, InitiatingProcessFileName,
        InitiatingProcessCommandLine, ProcessCommandLine
    | order by Timestamp desc

    This query usually returns very few rows, which makes every hit worth reading.

  3. Download cradles

    A download cradle is a short command that fetches code from the internet and runs it, often in one line. MITRE calls the download part Ingress Tool Transfer (T1105). Look for the methods and cmdlets that cradles rely on:

    download-cradles.kql
    DeviceProcessEvents
    | where Timestamp > ago(7d)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | where ProcessCommandLine has_any ("DownloadString", "DownloadFile", "Net.WebClient",
        "Invoke-WebRequest", "Start-BitsTransfer", "FromBase64String", "Invoke-Expression", "IEX")
    | summarize Executions = count(), Devices = dcount(DeviceName),
        Example = any(ProcessCommandLine) by InitiatingProcessFileName, AccountName
    | order by Devices asc, Executions asc

    Software installers and management agents also download things, so expect some noise. Summarizing by parent process and account groups that noise together, and sorting with the fewest devices first puts the unusual cases on top.

  4. Rare command lines

    Legitimate PowerShell tends to repeat: the same management script runs on hundreds of machines every day. An attacker's commands are usually seen on one or two machines. Stacking, counting how often each command line appears, turns that difference into a list:

    rare-command-lines.kql
    DeviceProcessEvents
    | where Timestamp > ago(30d)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | summarize Devices = dcount(DeviceName), Executions = count(), LastSeen = max(Timestamp)
        by InitiatingProcessFileName, ProcessCommandLine
    | where Devices <= 2
    | order by Executions asc

    Scripts that put random file names or dates in their command lines will look rare every time. Once you know them, exclude them by parent process or by a stable part of the command line, and the list becomes much shorter.

  5. PowerShell talking to the internet

    The last hunt starts from the network side: PowerShell processes that connected to public IP addresses, with the command line that made the connection.

    powershell-network.kql
    DeviceNetworkEvents
    | where Timestamp > ago(7d)
    | where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
    | where RemoteIPType == "Public"
    | summarize Connections = count(), Destinations = make_set(coalesce(RemoteUrl, RemoteIP), 10)
        by DeviceName, InitiatingProcessAccountName, InitiatingProcessCommandLine
    | order by Connections asc

    Connections to Microsoft update and management services are normal. A connection to a newly registered domain, a paste site or a bare IP address deserves a closer look.

Triage a hit

A result from any of these hunts is a lead, not a verdict. Work through the same questions every time:

  • Decode before you judge. For encoded commands, copy the Base64 string and decode it as UTF-16LE text, which is what PowerShell uses. In PowerShell, [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($b64)) returns the script without running it. Never paste a decoded command back into a console.
  • Read the parent chain. Who started the process, and who started the parent? Explorer, a management agent and Word tell three very different stories.
  • Check the account and the device. Is this an administrator on a server they manage, or a sales user on a laptop at 3 a.m.?
  • Look before and after. In the device timeline, check what happened in the minutes around the event: files created, network connections, new services or scheduled tasks.
  • Search wider. If the command looks malicious, search for the same command line, domain or file hash across all devices.

Some attack frameworks load PowerShell scripts that never appear on the command line at all. On machines with Microsoft Defender Antivirus, AMSI lets the antivirus inspect script content at run time, and Windows can record script blocks in event 4104 when script block logging is enabled. Both see code that command line hunting misses, so check whether they are turned on in your environment.

From hunt to detection

When a query returns only real problems after tuning, promote it. In advanced hunting, Create detection rule turns a query into a custom detection that runs on a schedule and raises alerts. Do it only after you have run the query for a few weeks and recorded the false positives you excluded, so the team knows what the rule deliberately ignores.

Why it matters for a SOC analyst

PowerShell abuse appears in a large share of hands-on intrusions, from fileless malware to ransomware operators preparing their final stage. Commercial XDR products already alert on many of these patterns, but threat hunting with your own queries teaches you what normal looks like in your environment, and that is what lets you spot the one command line that does not belong.

Terms in this article

KQL
The read-only query language used by Microsoft Sentinel, Defender XDR advanced hunting, Azure Monitor and Azure Data Explorer.
Threat hunting
The proactive search for attackers who are already inside the environment but have not triggered any alert, usually starting from a hypothesis.
Living off the land
Attacking with tools already present on the victim's systems, such as PowerShell or WMI, so the activity blends in with normal administration.
Fileless malware
Malicious code that runs in memory or through legitimate tools like PowerShell, leaving few or no malicious files on disk to scan.
AMSI
A Windows interface that lets applications such as PowerShell send scripts to the installed antivirus for scanning just before they run.
XDR
A security platform that correlates detections from endpoints, identities, email, cloud apps and more into single incidents with a shared response console.
False positive
An alert that fires on activity that is not actually malicious, because the detection logic matched something legitimate.
MITRE ATT&CK
A free, public knowledge base of adversary tactics and techniques based on real-world observations, used as a common language for attack and defense.

Last reviewed on by The Ledger Editor, Security Engineer