Last updated at Sat, 20 Jan 2024 23:22:05 GMT

This post is the first in a two-part series that covers defending your Windows environment against offensive PowerShell.

Hello! My name is Josh Frantz, and I am a security consultant for Rapid7. I’ve been helping companies better protect their networks for about seven years now and have seen a lot of things along the way, especially when it comes to defending networks against malicious PowerShell.

This series discusses how we can implement some basic controls to keep our data safe from potential PowerShell attacks and how to detect malicious behavior trying to circumvent said controls. As always, be sure to include necessary stakeholders before making these changes to your environment, especially when it comes to the logging changes we suggest for PowerShell.

“It isn’t easy being blue.” – Kermit the Frog, probably

As any Blue Teamer will tell you, defending your network against both criminals and third-party assessments is no easy task. Risk-Based Security reported that from January 2018 to the end of June, there have been over 10,000 discovered vulnerabilities (the highest number at the same point of any year, ever), increasing the surface areas for both criminals and non-criminal Red Teamers alike.

Windows is everywhere, dominating overall usage in both enterprise environments and among personal desktops/laptops. That makes it inherently difficult to defend. Our jobs as security professionals are ever-necessary in the current and future enterprise environment landscapes, and they’re not getting any easier.

So, why should I care about PowerShell attacks?

PowerShell is a built-in command line tool that has been included and enabled on every Windows operating system since Windows 7/Windows Server 2008 R2. It can be run in-memory where A/V software can’t see it, but we can often use PowerShell to download code and run it on our target. Several offensive tools exist based in PowerShell, including the following:

So, think about this: We have a scripting language that is enabled on 80% of all enterprise machines by default that can execute code downloaded from the internet, leverage .NET libraries, and has built-in remoting—and most companies don’t monitor what code endpoints are running.

Okay, I’m officially paying attention. What do I do?

Well, first realize that security (like onions and ogres) is all about layers. We can’t possibly implement one control and solely rely on that for protection. Below is the entirety of that onion, with each layer explained:

Update PowerShell to the latest stable version

Keeping things patched is something we so often overlook in security. The team behind PowerShell at Microsoft has done a really good job in the latest stable PowerShell version (Version 5.0, as of right now), adding several features to help us defend against malicious PowerShell attacks. Information on upgrading each version of PowerShell can be found here.

PowerShell V2

So, you’ve upgraded all your endpoints to PowerShell V5—great! But there is still the potential for “downgrade” attacks. These allow some exploit frameworks to use an older version of PowerShell without all the fancy security controls V5 affords us because Windows 8 and above have an optional feature that leaves the PowerShell V2 engine installed. Here’s how to disable this:

  • In Windows 8 and newer, run the following command on an endpoint (this could obviously be executed remotely or configured via Group Policy): Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root

Unfortunately, Windows doesn’t give us the ability to (easily) disable the V2 engine on Windows 7. However, as any good security program does, we have layers and can apply them in-depth. You can use AppLocker (discussed below) to disallow the V2’s DLLs from being used, rendering attacks such as PowerShell Empire’s ps-inject module ineffective:

  • C:\windows\assembly\GAC_MSIL\System.Management.Automation\1.0.0.0__31bf3856ad364e35\System.Management.Automation.dll
  • C:\windows\assembly\NativeImages_v2.0.50727_64\System.Management.A#\8b1355a03394301941edcbb9190e165b\System.Management.Automation.ni.dll
  • C:\windows\assembly\NativeImages_v4.0.30319_32\System.Manaa57fc8cc#\08d9ad8b895949d2a5f247b63b94a9cd\System.Management.Automation.ni.dll
  • C:\windows\assembly\NativeImages_v4.0.30319_64\System.Manaa57fc8cc#\4072bc1c91e324a1f680e9536b50bad4\System.Management.Automation.ni.dll
  • C:\windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll

AppLocker and Constrained Language Mode

So, we’ve updated PowerShell on all of our computers and disabled older versions to mitigate potential downgrade attacks. Now, we need to control access to PowerShell. A way underutilized tool for Windows is AppLocker, which allows us to whitelist executables and binaries to be run on machines controlled via Group Policy. I highly recommend taking this action in every environment where Windows is present, as it is one of the most effective ways to make offensive PowerShell harder to execute.

PowerShell Constrained Language (CL) is a language mode of PowerShell designed to support day-to-day administrative tasks yet restrict access to sensitive language elements that can be used to invoke arbitrary Windows APIs. Essentially, we are doing the following when enabling CL:

  • Disabling the use of COM Objects
  • Disabling .NET types
  • Disabling the use of Add-Type (Usually allows the creation of arbitrary types defined in different languages)
  • Disabling PowerShell Classes (which are C# type definitions)
  • Blocking XML-based workflows
  • Disabling Start-Job cmdlet

The above are the major points of CL mode, which greatly reduces an attacker’s ability to execute offensive PowerShell in your environment. Setting this language mode is fairly straightforward:

  • If using Windows 8 (and up) and PowerShell V5 in combination with AppLocker’s default allow policies, CL mode is the default language mode.
  • If using Windows 7 or lower, you can set the environment variable via Group Policy:
    • Computer Configuration > Preferences > Windows Settings > Environment

There is no one-size-fits-all solution for the language mode you wish to enable, but the above is one of the most effective ways to limit exposure to a number of offensive PowerShell attacks, as seen below with Invoke-Mimikatz trying to run in CL Mode:

Script Block Logging, Module Logging, and Transcription

Whoa, we’ve talked about potentially mitigating offensive PowerShell a lot now, but we’re still missing an important piece: incident detection. Having logs that tell us what people are doing with PowerShell (those who are allowed to run it, anyway) could be valuable for incident responders. If you need the .admx files for the policies we're about to go over, download those here. We can actually see what people are doing on the network by enabling three types of logging methods via Group Policy:

1. Script Block Logging

Script Block Logging records all blocks of code as they’re executed by PowerShell. We can even capture de-obfuscated code as it’s processed (if an attacker is using (Base64 or XOR), in addition to the obfuscated payload. The output of the executed script is not recorded (we will get there!), but it’s a powerful way to get the code people are running into your SIEM solution and give you the ability to respond. We also have the ability to “Log script block execution start/stop events,” which could be useful for scripts running over a long period of time. However, this generates a large number of events, and I wouldn’t recommend this for most environments. The event code to monitor for Script Block Logging is 4014, while the execution start/stop events are 4105 and 4106, respectively.

Follow these steps to enable this:

  • Enable the setting in Group Policy (Computer Configuration > Policies > Windows Components > Windows PowerShell)
  • You can also set the below registry value, which will do the same:
    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging\EnableScriptBlockLogging=1

2. Module logging

Module logging records all execution details as PowerShell processes them based on modules we include. In this example, we are going to simply log them all. We can record portions of scripts, de-obfuscated code, and formatted output. The event code used by module logging is 4103. To ensure we’re collecting everything, we recommend this option be enabled:

  • Enable the setting in Group Policy ((Computer Configuration > Policies > Windows Components > Windows Powershell) and make sure we enter a “*” to monitor all modules
  • You can also set the below registry values, which will do the same:
    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ModuleLogging > EnableModuleLogging=1
    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames= * = *

3. Transcription

Transcription will create a new log entry for each PowerShell session, including all input and output, exactly as it would appear if you had a PowerShell window open. It will also create a text file based on user account and session that contains timestamps and metadata for each command executed. However, it does not include the contents of external scripts/code executed by PowerShell.

The naming convention for these files is “PowerShell_transcriptxxx” to avoid overwriting other entries. By default, these files are logged under the user’s documents folder, but we configure them to be sent to a shared storage location for consumption by our SIEM. Here is a script you can use to set permissions on this shared storage directory. See below:

  • Enable the setting in Group Policy (Computer Configuration > Policies > Windows Components > Windows PowerShell)
    • Make sure we check the box for “Include invocation headers” so that we’re including timestamps per command.
    • Set the output directory to be a shared storage location. This location should be write-only, restricted to only be accessed by the security team.
  • You can also set the below registry values, which will do the same:
    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription > EnableTranscripting=1
    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription > EnableInvocationHeader=1
    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription > OutputDirectory=“\path\to\sharedstorage\” (Enter the path to the shared location. Empty = local documents)

PowerShell Remoting: Useful but dangerous

Powershell Remoting can be a system administrator’s best friend or worst nightmare, depending on how it is implemented. PSRemoting provides a way to execute code without using remote desktop protocol (RDP), and it’s based around WinRM. It is also encrypted regardless of protocol, always encrypting all communication after initial authentication with a per-session AES-256 symmetric key.

However, we’re looking at PowerShell with a new perspective after today and need to be careful about where we have this enabled. We should be disabling WinRM where applicable, such as on sensitive devices and servers. We should also monitor the logs we enabled before for PowerShell Remoting Commands (Invoke-Command, Enter-PSSession, and Exit-PSSession). We can also look at the ports used (5985 for HTTP, and 5986 for HTTPS).

Enabling PowerShell Remoting (in a safe way!) allows us to leverage even more control over PowerShell using Just Enough Administration (JEA), which lets you:

  • Allow the use of delegation using service accounts that perform administrative actions instead of individual user accounts
  • Use virtual accounts that have elevated privileges instead of a separate individual user account or admin account
  • Only allow specific users to run certain commands, functions and leverage frameworks

But wait, we’re not done!

Defending against PowerShell attacks is complicated, and is neither easy nor straightforward. If you’ve made it this far, congratulations! You’ve done a lot to ensure those very nice Red Teamers and actual cybercriminals aren’t able to leverage PowerShell to exploit your devices.

The actions above are a very good starting point for securing your environment against offensive PowerShell. Check back soon for Part 2, which will show you how to detect attackers who are savvy enough to bypass the newly implemented security controls and attack your network.