Defending Against PowerShell Attacks

It’s no secret that I am a big fan of PowerShell and recently I have been spending a considerable amount of time researching and testing it from a security perspective. While there is a lot of solid information out there, I have found it can still be a challenge to really get a solid grasp on what PowerShell is and to fully understand everything that goes along with it. This post is going to be a collection of all of my findings and thoughts with the end goal of hoping to help individuals and organizations learn how to better prepare themselves to effectively defend against PowerShell attacks at an enterprise scale.

I also have a few additional posts covering PowerShell related topics that will be useful along the way:
Everything You Need To Know To Get Started Logging PowerShell
Disabling PowerShell v2 with Group Policy
Invoke-Decoder – A PowerShell script to decode/deobfuscate malware samples

What Is PowerShell?

Before jumping into monitoring and securing PowerShell, I want to cover some of the basics so we have a clear understanding of what PowerShell is, what it looks like when it runs on a system, and some of the inherit risks associated with it.

PowerShell is…

  • A Command-line shell and scripting language designed for system administration
  • Core component of the Windows Operating System, cannot be removed
  • Designed to make remote access and automation easier
  • Includes many built in tools called cmdlets
  • Scripts do not need to be compiled and can be edited in a text editor
  • Provides direct access to the .Net framework
  • Available to all users by default
  • Includes an “Execution Policy” feature to limit script execution, but this is not intended to be a security feature and is easily bypassed

What Components Make Up PowerShell?

There are three main components that make up PowerShell:

  • System.Management.Automation.dll – The core of PowerShell’s functionality resides in this library
  • PowerShell.exe – The host process that loads the System.Management.Automation.dll
  • Antimalware Scanning Interface (AMSI) – Interface that allows applications like PowerShell to integrate with anti-malware products like Windows Defender

The separation of these components presents some interesting situations and challenges when looking at PowerShell from a security perspective. More on that in a bit…

PowerShell Versioning

PowerShell has a few gotchas associated with the various versions available and the information below can be useful when it comes to standardizing what versions of PowerShell are available in an environment.

PowerShell Version
Required .Net Version
Windows 10 & Windows Server 2016 (NT 10.0)
.Net 4
Windows 8.1 & Server 2012 R2 (NT 6.3)
.Net 4
Windows 8 & Server 2012 (NT 6.2)
.Net 4
Windows 7 & Server 2008 R2 (NT 6.1)
2.0 (Depracated)
.Net 2/3.5

Notes and gotchas:

  • PowerShell v2 cannot be upgraded beyond v2
  • PowerShell v3 can be directly upgraded to v5+
  • PowerShell v2 and PowerShell v5 can be installed “side by side”
  • PowerShell v2 cannot be removed from Windows 7/Windows Server 2008 (End of life)
  • PowerShell v2/.Net 3.5 are not installed by default on Windows 8+/Server 2012+
  • Microsoft Office requires .Net 3.5 and often enables PowerShell v2
  • All versions of PowerShell will use the following path for PowerShell.exe:
  • Ideally, all systems should be on PowerShell v5.1 with PowerShell v2 disabled

Why Is PowerShell A Risk?

System administration tools are also useful to attackers and PowerShell includes many tools called cmdlets by default. From an attacker perspective, this means there is less of a need to download and install additional software which helps decrease the likelihood of detection, aka living off the land. PowerShell is also extremely easy to use, making it attractive to both administrators and attackers.

It is important to note that PowerShell is not the only technology that can be abused on Windows, here are a few examples of others:

  • Compiled executables
  • DLLs
  • Python
  • VBScript
  • JScript
  • COM Objects
  • Macros/Visual Basic for Applications (VBA)
  • And many more…

For the scope of this post, I will only be talking about how to tackle the PowerShell problem.

Built In Tools

Built in tools like Invoke-WebRequest (PowerShell v3+) allow a user to easily download files (or malware) directly from the internet via HTTP:

Creating Custom Tooling With .Net

This is where things start to get really interesting, so what if tools like Invoke-WebRequest aren’t available but you still needed to download files? Well, with .Net you can easily write your own tool in a text editor by leveraging the .Net WebClient class:

And this is what it looks like when the script is executed and used to download a file:

The WebClient class can also be called directly on the command line as a one-liner, and in this example by using DownloadString, the downloaded script is loaded without touching the disk:

This is super basic stuff, but demonstrates how powerful PowerShell can really be.

Abusing PowerShell

So now that we know a little bit more about what PowerShell is and what it looks like, how can it be abused?

Let’s take a look at a typical phishing scenario:

  1. User launches Outlook, opens an email that has a Word doc attached that contains a malicious macro and runs it:

  2. The macros runs a PowerShell one-liner to download and execute the next stage in memory:

  3. The downloaded PowerShell script establishes command and control (C2) along with persistence on the system:

  4. Attacker now has control over the system.

This scenario is all to common and can be extremely effective even in environments that have a mature security model.

PowerShell Tools & Frameworks

The example provided above shows how simple and efficient PowerShell can be in attack scenarios while also being easy to leverage to creating custom tooling. There are many open source projects that take these concepts to the next level and demonstrate how PowerShell can be useful throughout the attack lifecycle. Below is a list of some of the more popular PowerShell based tools/frameworks:

PowerShell And The Visibility Problem

When I talk to other security analysts or engineers about PowerShell, I often get the response “Oh, we see PowerShell activity in our AntiVirus (AV) or Endpoint Detection & Response (EDR) product, we don’t have anything to worry about!”. The problem with this is that many security analysts/engineers don’t actually use or understand how PowerShell is executed on a system, so when reviewing PowerShell activity in security tooling, it may not be obvious what is being missed. With many of the EDR and antivirus products I have tested, PowerShell activity is only captured when the command is tied to a process creation by calling “PowerShell.exe <arguments>”.

I think the best way to understand this behavior is to manually test some PowerShell and then see what it looks like in the logs, or as I like to say flushing something down the pipe and seeing what comes out. When you are the one running commands or scripts, you know exactly what should be in the logs and this allows for easy identification of gaps.

For the rest of this post, I am going to use a demo script that I created named “Invoke-BaseConfig.ps1”, the intent here is to show how the same exact script may appear differently in various tools. The script contains a few different scenarios including custom functions, calls to local executables, obfuscation, and calls to additional PowerShell scripts.


In this section, I am going to be using Sysmon to emulate what is typically captured by an AV/EDR solution. These events are also logged in the same way if Process Creation Events with Command Line Logging (Event ID 4688) are enabled.

Here is what it looks like when a cmdlet is executed by calling PowerShell.exe first:

When PowerShell is executed like this, most command line tools will log the entire command. However, if you are already in a PowerShell session, there is no need to call PowerShell.exe first, so let’s see what it looks like when running the cmdlet by itself:

Since the cmdlet is executed within the PowerShell process that was started when the session started, no command line is captured. This is only the beginning of the visibility issue, but it is critical to acknowledge that this gap exists.

Now let’s take a look at what is captured when executing something a little more complicated like a script:

As you can see in the image above, none of the script source itself is captured, but any command directly tied to an executable/process creation (powershell.exe, net.exe) is captured with the associated command line. Other commands like the obfuscated ones, are missed entirely. There is also another command that was logged calling a shell.ps1 that executed, but that script does not appear to be in the original source code of the Invoke-BaseConfig.ps1 script. All things considered, it’s not looking very promising at this point.

PowerShell Logging

If EDR and AV aren’t the answer to the PowerShell problem, then what is? That answer is simple and it’s also built in to Windows, PowerShell Logging.

The table below shows what PowerShell logs are available, the version level required, and some additional notes that may be useful to help you decide what logs to target:

Event IDs
PowerShell Version
Windows PowerShell
(Event Viewer > Application and Service Logs)
  • 800 – Pipeline Execution
  • 400 – Engine Version
  • Very high in volume
  • EID 800 events log one command per event
  • EID 400 useful for detecting downgrade attacks
(Event Viewer > Application and Service Logs > Microsoft > Windows > PowerShell)
  • 4104 – Scriptblock *
  • 4103 – Module *
  • 4100 – Errors
  • Scriptblock captures commands and script source
  • PowerShell v5+ required for “deep scriptblock logging” and malicious auto logging
  • Module logs show command/code expansion across the pipeline
  • Module logs can be very high in volume
Transcription *
(Default – C:\Users\%USERNAME%\Documents)
  • Captures commands and output as they are seen at the console
  • PowerShell v3+ required for Transcription logging to be enabled via GPO
  • Logs saved as flat text files, which can create parsing problems when ingesting to a SIEM

* Not enabled by default

Event ID 4104 – Scriptblock

Scriptblock logs are one of the most useful logs that can be enabled. In the screenshot below, running the same command from earlier, we are now able to see the initial command along with the entire source of the script that executed:

From a defensive perspective, this an amazing level of visibility. There is more to come around this particular event ID, but we will come back to that in just a few…

Event ID 4103 – Module

With module logging, each command will log an individual event, sometimes there will even be multiple events per command. This means the volume can be very high, but there is a lot of value when combining these events with the scriptblock events as they will show how the commands expanded as they were processed through the PowerShell pipeline. This means that you will see the command itself, but also the values of variables/output related to that command as it executed. Because of the variable expansion, there is a higher chance that passwords that are hardcoded or input via console will be captured in these logs. These events will also include the version of the PowerShell engine that was loaded.

In the example below, we can begin to see what the first obfuscated command looked like as it was executed. In this case, the command is using Set-ItemProperty to create a registry run key that will download and execute another obfuscated command every time the system starts up:

You can see in this example that multiple events were logged as the command executed down the pipeline going from the initial Invoke-Expression to Set-ItemProperty.

Transcription Logs

Transcription logs are my least favorite of the PowerShell logs, but they can be extremely useful when trying to determine if activity successfully executed and what data was output to the session. These logs should be saved to a centralized share and will be in a flat text file format which can then be ingested into a SIEM. Due to the format, they present some challenges in getting the data parsed out correctly.

In the screenshot below, you can see the initial command along with everything that was output to the screen:


At this point, it is becoming a little more clear what the script that executed was doing, but there are still some questions around the obfuscated sections. So let’s take a quick pause and talk about PowerShell and obfuscation…


PowerShell is extremely flexible in how the language itself can be written, making it ideal for obfuscation. If you are interested in obfuscating PowerShell, Daniel Bohannon has tool called Invoke-Obfuscation that is incredibly useful.

Here are some of the techniques that can be used:

  • Token – String, command, variable, etc
  • Abstract Syntax Tree (AST) – Scriptblock, command, etc
  • String – Concatenation, reorder, reverse
  • Encoding – ASCII, hex, binary, SecureString, whitespace, etc
  • Compression – Native Windows, Gzip
  • Escape Characters – “`”

Note: PowerShell includes an -encode option that enables the execution of base64 encoded commands. This is not intended for obfuscation, but to allow administrators to write complex one-liners that include brackets ({}). Please do not be the person that writes PowerShell detections that are solely based off of the presence of base64…;)

PowerShell v5.1 Deep Scriptblock Logging – Event ID 4104

Earlier we saw that the Scriptblock 4104 event captured the entire source of Invoke-BaseConfig.ps1, but the obfuscated commands were still obfuscated. Since we have the entire source, we could start to manually deobfuscate the code, but that can be time consuming and tricky. Luckily, with PowerShell v5.1 a feature called “Deep Scriptblock Logging” was introduced. As the obfuscated command is processed by PowerShell, additional events will be logged as each layer is stripped away.

In the example below, you can see that the CHAR encoded command is piped to a For-EachObject loop and then the decoded command is logged in the events that follow:

Manual Deobfuscation With PowerShell

There will still be times where not everything is automatically decoded, and you will need to go back to doing things manually. Luckily, PowerShell makes this pretty easy and will actually do the majority of the work for us. This can get a little risky, so I would suggest to always play it safe and do this on a offline/sandboxed VM. The biggest thing to remember here is that you only want to decode the obfuscated string, anything that will execute the command, like Invoke-Expression, needs to be removed.

You may have noticed that in the deobfuscated command in the section above, the IEX Net.WebClient command was not deobfuscated. To manually deobfuscate it, copy only what is contained between the parentheses (again we do not want any IEX here!), paste it into a PowerShell session and voila!

Backdoor.ps1 & Shell.ps1

We now have a pretty good idea of what Invoke-BaseConfig.ps1 is doing, but we still don’t know anything about the shell.ps1 that was caught way earlier in the EDR section, and now we also have a backdoor.ps1.

So let’s circle back to the Scriptblock 4104 events and look at the events that follow the execution of Invoke-BaseConfig.ps1. Sure enough, we can see the entire source of backdoor.ps1 which disables Windows Defender and then calls shell.ps1 for execution. Shell.ps1 contains a massive chunk of base64 encoded data that also appears to be compressed:

There are a couple things to note here, like the fact that backdoor.ps1 was initially executed from within Invoke-BaseConfig.ps1 without calling PowerShell.exe, so again the EDR/Sysmon tooling missed this entirely. However, backdoor.ps1 called shell.ps1 by calling “PowerShell.exe -Exec Bypass”, so this event was captured via EDR/Sysmon. This explains the gap in execution that was observed in the Sysmon logs where shell.ps1 seemed to come out of no where.

Deobfuscating Shell.ps1 Payload With Invoke-Decoder

There is really only one question left at this point, what is this big chunk of base64/compressed data in the shell.ps1 and why doesn’t it get deobfuscated by the PowerShell Deep Scriptblock logging? Probably because it is not actually PowerShell at all…

To help simplify working with these types of encoded payloads, I’ve written a tool called Invoke-Decoder.

To begin breaking this payload down, we first need to copy only the base64/compressed data and load it into Invoke-Decoder by pressing L, then S for string, and finally pasting in the string:

Use option 2 to decode the base64 and decompress the data in one shot:

Now it becomes obvious why the payload was not decoded, it’s a Portable Executable as indicated by the MZ header. Scrolling through the file in notepad, looking for interesting strings, there are some indications that this may be a Covenant “Grunt” and the C2 server is located at

PowerShell Security Controls

In addition to gaining more visibility into PowerShell, it is good idea to also take some steps to harden PowerShell and limit how it can be used. Below are few additional controls that are worth looking into to help increase your overall security posture.

Constrained Language Mode (CLM)

Constrained Language Mode is designed to support day-to-day administrative tasks, yet restricts access to sensitive language elements that can be used to invoke arbitrary Windows API’s. Enabling this control alone will prevent/break most malicious PowerShell based tools from running.

You can find more information about Constrained Language Mode here:
PowerShell Constrained Language Mode

Just Enough Administration (JEA)

Just Enough Administration takes things a step further from Contrained Language Mode. It’s a role based access control that limits which cmdlets, functions, and external commands a user can run. Sessions are also sandboxed.

You can find more information about Just Enough Administration here:
Just Enough Administration: Windows PowerShell security controls help protect enterprise data


By restricting what an attacker can do in PowerShell with something like CLM or JEA, attackers are forced to use alternate methods like executables, VBScript, JScript, and so on. AppLocker is a great way to restrict this type of activity by controlling what a user is able to launch based off of defined policies. With AppLocker, rules can be assigned to groups or individuals and it is also deeply integrated into the Windows OS and PowerShell.

AppLocker has the ability to restrict the following:

  • Executable files (.exe, .com)
  • Scripts (.js, .ps1, .vbs, .cmd, .bat)
  • Windows Installer files (.mst, .msi, .msp)
  • DLL files (.dll, .ocx)
  • Packaged apps/installers (appx)

You can find more information about AppLocker here:

PowerShell Bypasses

PowerShell v2 Downgrade Attacks

PowerShell v2 lacks all of the newer security features that Microsoft has implemented, including AMSI support. The following command can be used to “downgrade” a PowerShell session to v2 to not only evade AMSI but also most of the logging features:

As you can see in the PowerShell logs above, the downgrade command itself is detectable in the scriptblock (4104) events, you just won’t see anything after that. Removing PowerShell v2 prevents “Downgrade attacks”, I have a blog post that details all the steps required to do this via Group Policy that can be found here:
Disabling PowerShell v2 with Group Policy

AMSI And Logging Bypasses

AMSI and PowerShell logging are not perfect solutions, researchers/attackers spend a considerable amount of time and resources looking for ways to bypass these technologies. Microsoft is getting better at detecting and defending against these types of attacks, but this will almost always be a cat and mouse game.

Unmanaged PowerShell

Earlier it was mentioned that the core of PowerShell is actually the System.Management.Automation.dll, but why is this important? Because this means you can run PowerShell without PowerShell.exe. This is one of the reasons why blocking PowerShell.exe is generally not effective or recommended. This technique is referred to as “unmanaged” PowerShell, and these tools often implement AMSI and logging bypasses in the binary, making detection of the bypasses a little more difficult.

Here are a few unmanaged PowerShell projects worth checking out:

Defending Against PowerShell Attacks

It should be clear by now that defending against PowerShell attacks is best handled using a layered approach. The table below is a summary of the concepts outlined in this post and can serve as a great baseline for monitoring and limiting malicious PowerShell based activity:

Increasing Visibility
  • Endpoint Detection & Response (EDR) *
  • AntiVirus (AV) *
  • PowerShell Logging
    • Scriptblock (4104)
    • Module (4103)
    • Transcription
Attack Surface Reduction
  • Remove PowerShell v2
  • Constrained Language Mode (CLM)
  • Just Enough Administration (JEA)
  • Application Whitelisting (AppLocker)

* EDR and AV products that support AMSI will generally provide enhanced visibility into PowerShell.


Detecting malicious PowerShell activity is actually fairly easy with the right data:

  • PowerShell v5 Malicious Auto Logging Events
    • Malicious auto log events are 4104/Scriptblock events flagged via AMSI with a Level of “Warning”, these events are a great place to start monitoring PowerShell activity but will require some baselining to remove normal production noise
  • Known bad
    • Create signature based alerts for known bad language, tooling, and downgrade attacks
  • Obfuscated
    • Character frequency analysis can be very useful (`,{},(), whitespace)
    • Strip out escape characters (`) as data is indexed
    • Encoding techniques other than base64
  • Bypasses and Unmanaged PowerShell
    • PowerShell based bypasses will be logged in the PowerShell logs
    • Compiled bypasses and unmanaged PowerShell require additional executables/dlls, so you’ll need to utilize additional data sources/controls like the following:
      • Antivirus and EDR Tooling
      • Proxy and network data
      • Application whitelisting/AppLocker


Knowing what logs and controls are available is only half the battle, here are few additional challenges to keep in mind as they will likely come up during the implementation phase:

  • Cleartext Passwords
    • Logging command line and script activity means there will be a chance of logging cleartext passwords, keep this in mind when it comes to your confidential data storage policy
    • User education of best practices can greatly reduce the likelihood of this occuring
  • Restricting Local Event Viewer Log Access
    • PowerShell logs in the Event Viewer are readable by interactive users (IU) by default and because the logs can contain sensitive information (passwords), this is a less than ideal setting
  • Centralized Logging
    • Consider the following features when choosing a solution:
      • Transport Encryption (SSL, Kerberos, etc)
      • Compression – Network bandwidth is usually at a premium, while compression will eat more CPU cycles, the bandwidth savings may be worth it at scale
      • Filtering – Filter out noisy events at the client
      • Deployment method – For example Windows Event Collector (WEC) can be enabled via GPO, Splunk Universal Forwarder requires a full on agent installation with configuration file to be deployed/maintained
      • Scalability – Something like Splunk is highly scalable and available, but Window Event Collector is not and the collector itself becomes a single point of failure
  • Volume
    • This is going to be expensive at scale!
    • PowerShell log sizing
      • Filtering out specific event ID’s and noisy events will be critical – target EID’s 4104, 4103
      • Windows uses PowerShell to maintain Windows, expect the unexpected – logs can and will balloon especially during update cycles
      • On average, I would say expect at least 5-10 MB of logs per host per day in an enterprise environment
      • I wrote a tool that can help with sizing out the PowerShell logs called Get-PSLogSizeEstimate, check out the blog post and “Estimating PowerShell log storage requirements” section here:
        Everything You Need To Know To Get Started Logging PowerShell
    • Network bandwidth
      • When monitoring network utilization, make sure to account for any overhead created by the agent responsible for forwarding the log data
        • The Splunk Universal Forwarder can create anywhere from 30-50MB of overhead per host per day, while this data will require storage space on the Splunk instance, it does not count against your license


If you are still reading this, then hopefully by now you have a better understanding of what it takes to defend against PowerShell attacks. Even armed with this information, many organizations will still struggle to get a project like this off the ground when it comes to implementation and then dealing with the cost of storage and network capacity at scale. Once the right data is in the right place, having proper staffing with the right expertise to be able to review the data and create efficient content around that data becomes the next challenge. This is not going to be easy, but the overall increase in security posture greatly outweighs the cost.

So if you have ever heard a pentester or red teamer say that “PowerShell is dead”, that isn’t quite true. The logging and controls for PowerShell are finally where they should be, but the truth of the matter is that very few orgs have actually taken steps to implement these features.

Comments are closed.