If you’ve been in Systems Administration for more than a few weeks, you’re probably aware of Microsoft’s PowerShell. At over 10 years of age now, PowerShell has evolved to v5.0 RTM with 6.0 in public testing via the new(ish) PowerShell Core.
Each version gets new features, functions, and system hooks and integration (and risks that come along with each of those) so it’s grown considerably in usage and capabilities and is commonly relied upon in systems administration tasks major and minor. It comes built-in with Windows desktop and server installs, it gives direct access to system functions, and as of Server 2012 has remote functions enabled out of the box! Version 5 also includes “Desired State Configuration” that lets you manage your environment’s IT configuration as code, which is a super-cool feature for IT server & desktop admins but not our focus for today.
All of that is great when you’re managing a fleet of computing assets and you need to enable features and functions, get and use management information, or automate administrative tasks; for this, PowerShell is a remarkable toolset. But let’s briefly turn the table around and see what this looks like to an attacker on your network:
And one of my favorite aspects of PowerShell is that blocking powershell.exe with AppLocker or another application whitelisting platform doesn’t block PowerShell from running. In fact, our Director of Offensive Security recently put together a write-up about how to launch a PowerShell Remote Access Tool (RAT) on a system where PowerShell.exe isn’t allowed to run – yes, PowerShell is so powerful you can run PS code on a system that has PowerShell.exe blocked.
For a bad actor, PowerShell is a goldmine combination of pre-written and widely available tools and scripts, deep system access, and a lightweight footprint.
If you’re looking at PowerShell and asking yourself “How do I ensure we’re only running known or approved scripts and not malware?” or “I like some of what PowerShell does but I want to give it some limits” …good news! Microsoft included a few safeguards that IT Operations and Blue Teams can use to lock down an environment. Some popular PS safeguards include requiring signed code (set-execution policy), or setting up Just-Enough-Administration (JEA) or Just-In-Time-Administration (JITA), but what we’re looking at today is Constrained Language Mode.
PowerShell has options for “language modes” that define what features and functionality you have in the session. Some of the major allowable functions include COM access, API calls, and module & library loading. These language modes include:
The Microsoft documentation about PowerShell Language Modes has a great example illustrating the differences: If you’re in a PowerShell session and want to find what language mode you’re currently in, you could run:
PS C:\Users\Default> $ExecutionContext.SessionState.LanguageMode
Which would generally return:
FullLanguage
Telling you you’re in “FullLanguage” mode, which is unrestricted. But per Microsoft (emphasis added):
“However, in sessions with RestrictedLanguage and NoLanguage language modes, you cannot use the dot method to get property values. Instead, the error message reveals the language mode.”
So with that example, we see early on how these language modes can differ in function. In FullLanguage mode we can run “$ExecutionContext.SessionState.LanguageMode” normally, but in RestrictedLanguage or NoLanguage modes, we can’t use the .’s and have more limited functionality.
Much of PowerShell’s usage as an offensive tool relies on elements and functions that are blocked when PowerShell is set to ConstrainedLanguage mode. This is what makes this single environment change a powerful defensive tool.
So you’ve spent some time looking over the different language modes available and reading through the documentation and blogosphere, now you want to apply these policies to your environment. Some examples could include:
There are two official ways to set language modes: AppLocker, or Device Guard’s User Mode Code Integrity (UMCI). Best practices lean towards using Device Guard because it’s more difficult to disable by admin users and local admin user accounts, whereas AppLocker can be bypassed just by killing the service – though as Russell Smith at Petri.com notes, “AppLocker enforcement is better than nothing.” Read more about DeviceGuard on Microsoft’s page.
To build a full Device Guard policy, you’ll want to set up a clean reference device and run New-CIPolicy to build a template policy. If you’re new to Device Guard, setting it up for the first time is beyond today’s scope, but the Microsoft Deployment Guide is a great starting point, and Petri also has a great primer.
I mentioned two official ways to set PS Language Modes. There is a third method using a registry key that has been documented by a Microsoft employee. The policy is fairly easy to set, and per the post:
“I decided to ProcMon it and the procmon logs show that when PoSH starts it always reads in the system environment variable from the reg key HKLM\System\CurrentControlSet\Control\SESSION MANAGER\Environment\__PSLockdownPolicy that defines it and never queries for a User Environment variable with this name. So the end story here is that this absolutely works and that it cannot be overridden easily by an attacking piece of code in user mode space.”
So it looks effective, and from our tests it has held up. But this method is unsupported by Microsoft. Microsoft documentation doesn’t have anything on this function, and MS Support likely won’t help you if you get in a bind. So test this out extensively first, and ultimately use at your own risk!
Here’s a caveat to know before you roll out policy changes: There’s a known workaround to some of these security features within PowerShell 5.0. Thanks to backwards compatibility, by invoking PowerShell 2.0 in a script or cmd, an attacker can easily sidestep around some of these PowerShell 5.0 security features, including Script Block Logging (which we’ll discuss in another post soon) and including ConstrainedLanguage mode.
But the good news is this requires several components to be installed in place already, including:
So to block this workaround and prevent invoking PowerShell 2.0, you can:
We expect disabling the PowerShell 2.0 feature will be the easiest path for most environments, since so much relies on older .NET Frameworks, especially in healthcare and legal environments.
PowerShell’s become a very capable and well-integrated management framework that has seen usage and adoption grow steadily. But it’s a powerful double-edged sword: For all the ease it brings IT Operations staff and environment managers, it also opens up options and avenues to network attackers & intruders, avenues with deep system access. With some precautions and configuration, you can make life difficult for an attacker, if not prevent an attack from gaining a foothold in your environment altogether. Stay tuned for more about Script Block Logging and other security controls PowerShell has baked in.
Further reading:
https://www.wired.com/story/microsoft-powershell-security/
https://www.petri.com/protect-malware-enforcing-powershell-constrained-language-mode
https://pentestn00b.wordpress.com/2017/03/20/simple-bypass-for-powershell-constrained-language-mode/
https://www.petri.com/enabling-windows-10-device-guard
https://bneg.io/2017/07/26/empire-without-powershell-exe/