Disabling PowerShell v2 with Group Policy

In this post I am going to tackle something that I have been wanting to play around with for awhile, disabling PowerShell v2 at an enterprise scale. As a former systems engineer and now a security engineer, I have a love/hate relationship with PowerShell since it is amazingly useful but also incredibly dangerous in the wrong hands. In my last post, Everything You Need To Know To Get Started Logging PowerShell, I covered how critical PowerShell logging can be from a defender perspective but also talked about some of the dangers of leaving the now deprecated PowerShell v2 lingering around in a Windows environment. Specifically, I am referring to PowerShell v2 downgrade attacks and how they can be used to evade many of the improvements made to the more recent versions of PowerShell like enhanced logging with AMSI(Antimalware Scanning Interface).

I couldn’t find what I would consider a practical/easy to deploy, off the shelf sort of solution online, so I decided to roll my own using PowerShell and Group Policy. There were a few gotcha’s that came up along the way, so I figured this would be a good project to share since I am all for everyone increasing their overall security posture especially when it comes to PowerShell.

It should go without saying that if you are thinking about trying something like this, always start by testing on a small scale to ensure that no unforeseen issues are encountered. Luckily, the roll back procedure is pretty straight forward since the PowerShell v2 engine can just be re-enabled, but note that it does require administrative privileges.

This should be simple, right?

Microsoft has a fantastic blog talking about Windows PowerShell v2 being deprecated that covers some of the details around PowerShell v2 along with some useful commands to check if the PowerShell v2 engine is currently installed on a system.

For example, on Windows desktops you can use the following command:
PS> Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

And on Windows server you can use this command:
PS> Get-WindowsFeature PowerShell-V2

Although, it does seem you can use the first command on both versions of Windows with no issues:

To disable PowerShell v2, it is a simple as running the following command:
PS> Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

So now that we know the commands to check if v2 is installed and then how to disable it if it is, this should be a pretty straight forward to script that can then be pushed via GPO as a startup script…right?

Setting up the GPO

To get started, the first thing to do is create a new GPO and then add a Startup Script under Computer Configuration > Policies > Windows Settings > Scripts (Startup/Shutdown):

Right click on Startup in the right pane and go to properties, then click on the PowerShell Scripts tab and click the “Show Files…” button to open up the Startup scripts directory on the domain controllers sysvol share. This is where the PowerShell script will be staged:

Since this a new script and I am not sure how it is going to behave when deployed with Group Policy, I generally start with something very basic and PowerShell Transcript logging enabled so that it is easy to see how the script actually executed and then proceed with troubleshooting if need be:

Start-Transcript -Path "C:\Windows\logs\Disable-PSv2.txt"

Write-Host "Checking current v2 state..."
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

Write-Host "Disabling v2..."
Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

Write-Host "Checking updated state..."
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

Stop-Transcript

Testing the script manually, everything seems to work as expected:

Now it’s time to add the script with the following parameters to ensure it runs without interference:
-ExecutionPolicy Bypass -NonInteractive -NoProfile

Ok out of all of the open windows, applying the updated settings. The GPO should now be ready to test. Rebooting a host is usually enough to pick up the policy, but you can also force an update with the following command:
PS> gpupdate /force

Unfortunately, the settings did not appear to be applied successfully and the following was logged in the Transcription log:

Transcript started, output file is C:\Windows\logs\Disable-PSv2.txt
Checking current v2 state...
PS>TerminatingError(Get-WindowsOptionalFeature): "DismOpenSession failed. Error code = 0x80040154"
Get-WindowsOptionalFeature : DismOpenSession failed. Error code = 0x80040154
At \\lab.ntwrk01.net\SysVol\lab.ntwrk01.net\Policies\{582F07EB-67C4-4930-B653-00385D4AEA20}\Machine\Scripts\Startup\Disa
ble-PSv2-Test.ps1:4 char:1
+ Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPower ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WindowsOptionalFeature], COMException
    + FullyQualifiedErrorId : Microsoft.Dism.Commands.GetWindowsOptionalFeatureCommand
Get-WindowsOptionalFeature : DismOpenSession failed. Error code = 0x80040154
At \\lab.ntwrk01.net\SysVol\lab.ntwrk01.net\Policies\{582F07EB-67C4-4930-B653-0
0385D4AEA20}\Machine\Scripts\Startup\Disable-PSv2-Test.ps1:4 char:1
+ Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPower ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WindowsOptionalFeature],
   COMException
    + FullyQualifiedErrorId : Microsoft.Dism.Commands.GetWindowsOptionalFeatur
   eCommand

Disabling v2...
PS>TerminatingError(Disable-WindowsOptionalFeature): "DismOpenSession failed. Error code = 0x80040154"
Disable-WindowsOptionalFeature : DismOpenSession failed. Error code = 0x80040154
At \\lab.ntwrk01.net\SysVol\lab.ntwrk01.net\Policies\{582F07EB-67C4-4930-B653-00385D4AEA20}\Machine\Scripts\Startup\Disa
ble-PSv2-Test.ps1:7 char:1
+ Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsP ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Disable-WindowsOptionalFeature], COMException
    + FullyQualifiedErrorId : Microsoft.Dism.Commands.DisableWindowsOptionalFeatureCommand
Disable-WindowsOptionalFeature : DismOpenSession failed. Error code =
0x80040154
At \\lab.ntwrk01.net\SysVol\lab.ntwrk01.net\Policies\{582F07EB-67C4-4930-B653-0
0385D4AEA20}\Machine\Scripts\Startup\Disable-PSv2-Test.ps1:7 char:1
+ Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsP ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Disable-WindowsOptionalFeatur
   e], COMException
    + FullyQualifiedErrorId : Microsoft.Dism.Commands.DisableWindowsOptionalFe
   atureCommand

Checking updated state...
PS>TerminatingError(Get-WindowsOptionalFeature): "DismOpenSession failed. Error code = 0x80040154"
Get-WindowsOptionalFeature : DismOpenSession failed. Error code = 0x80040154
At \\lab.ntwrk01.net\SysVol\lab.ntwrk01.net\Policies\{582F07EB-67C4-4930-B653-00385D4AEA20}\Machine\Scripts\Startup\Disa
ble-PSv2-Test.ps1:10 char:1
+ Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPower ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WindowsOptionalFeature], COMException
    + FullyQualifiedErrorId : Microsoft.Dism.Commands.GetWindowsOptionalFeatureCommand
Get-WindowsOptionalFeature : DismOpenSession failed. Error code = 0x80040154
At \\lab.ntwrk01.net\SysVol\lab.ntwrk01.net\Policies\{582F07EB-67C4-4930-B653-0
0385D4AEA20}\Machine\Scripts\Startup\Disable-PSv2-Test.ps1:10 char:1
+ Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPower ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WindowsOptionalFeature],
   COMException
    + FullyQualifiedErrorId : Microsoft.Dism.Commands.GetWindowsOptionalFeatur
   eCommand

**********************
Windows PowerShell transcript end

The critical portion of this error being “DismOpenSession failed. Error code = 0x80040154”. At first I was a little confused by this, startup scripts run under the local system account so it should have more than enough permissions to run the commands, so then I thought maybe it had something to do with how the command executes under this account and where it may need to store any temporary data.

To rule this out I used the following command to run PSExec to launch PowerShell as NT Authority\System and then ran the script manually:
PS> PSexec.exe -s -i PowerShell.exe
PS> PowerShell.exe -ExecutionPolicy Bypass .\Disable-PSv2.ps1

The script ran without issue, so I began to wonder if the issue was actually related to PowerShell and the *-WindowsOptionalFeature cmdlets. Since this is a startup script, there was a chance that whatever modules or dependencies were needed to run the command may not be loaded at the time of execution. I tried adding a delay to the script and that didn’t work, but then as I thought about it more, if the PS session was already started and then the delay timer is hit in the script, it really doesn’t do anything to solve the issue since the dependencies would be loaded as PowerShell starts, not when the script itself is executed.

Looking back at the errors, I noticed the reference to DISM, which I knew from my days as a Windows Admin was “Deployment Imaging Servicing and Management” which has its own set of very useful tools tied to dism.exe. This tool is mainly used to modify Windows images including adding and removing features, which is exactly what we are trying to do here. At this point, I realized that the PowerShell commands were likely just wrappers around the DISM command line tool, so I tried calling DISM directly using the following commands:
PS> dism.exe /Online /Get-Featureinfo /FeatureName:”MicrosoftWindowsPowerShellv2″
PS> dism.exe /Online /Disable-Feature /FeatureName:”MicrosoftWindowsPowerShellv2″ /NoRestart

As you can see the format of the commands are very similar to the PowerShell versions.

After making the updates to the script, it now completes successfully when deployed via GPO:

Transcript started, output file is C:\Windows\logs\Disable-PSv2.txt
Checking current v2 state...

Deployment Image Servicing and Management tool
Version: 10.0.14393.3241

Image Version: 10.0.14393.3241

Feature Information:

Feature Name : MicrosoftWindowsPowerShellV2
Display Name : Windows PowerShell 2.0 Engine
Description : Adds or Removes Windows PowerShell 2.0 Engine
Restart Required : Possible
State : Enabled

Custom Properties:

ServerComponent\Description : Windows PowerShell 2.0 Engine includes the core components from Windows PowerShell 2.0 for backward compatibility with existing Windows PowerShell host applications.
ServerComponent\DisplayName : Windows PowerShell 2.0 Engine
ServerComponent\Id : 411
ServerComponent\Parent : PowerShellRoot
ServerComponent\Type : Feature
ServerComponent\UniqueName : PowerShell-V2
ServerComponent\NonAncestorDependencies\ServerComponent\UniqueName : PowerShell
ServerComponent\NonAncestorDependencies\ServerComponent\UniqueName : NET-Framework-Core
ServerComponent\Deploys\Update\Name : MicrosoftWindowsPowerShellV2
ServerComponent\Version\Major : 2
ServerComponent\Version\Minor : 0

The operation completed successfully.
Disabling v2...

Deployment Image Servicing and Management tool
Version: 10.0.14393.3241

Image Version: 10.0.14393.3241

Disabling feature(s)
[==========================100.0%==========================]
The operation completed successfully.
Checking updated state...

Deployment Image Servicing and Management tool
Version: 10.0.14393.3241

Image Version: 10.0.14393.3241

Feature Information:

Feature Name : MicrosoftWindowsPowerShellV2
Display Name : Windows PowerShell 2.0 Engine
Description : Adds or Removes Windows PowerShell 2.0 Engine
Restart Required : Possible
State : Disable Pending

Custom Properties:

ServerComponent\Description : Windows PowerShell 2.0 Engine includes the core components from Windows PowerShell 2.0 for backward compatibility with existing Windows PowerShell host applications.
ServerComponent\DisplayName : Windows PowerShell 2.0 Engine
ServerComponent\Id : 411
ServerComponent\Parent : PowerShellRoot
ServerComponent\Type : Feature
ServerComponent\UniqueName : PowerShell-V2
ServerComponent\NonAncestorDependencies\ServerComponent\UniqueName : PowerShell
ServerComponent\NonAncestorDependencies\ServerComponent\UniqueName : NET-Framework-Core
ServerComponent\Deploys\Update\Name : MicrosoftWindowsPowerShellV2
ServerComponent\Version\Major : 2
ServerComponent\Version\Minor : 0

The operation completed successfully.

Taking Things A Step Further…

Now that we have a working basic script, it is time to add some error checking and additional logic. One of the things to look out for is that some versions of Windows are natively bundled with PowerShell v2 and it should not/can not really be removed as is the case with Windows 7, so in that case we can add some logic to skip the disable command if any of these versions of Windows are detected. Another scenario to account for is that if PowerShell v2 is already disabled, then there is no need to run the disable command again.

The updated script flow is as follows:
– Check the current OS version.
    – If Windows 10/Server 12/16/19, PowerShell v2 will be disabled.
        – If PowerShell v2 is already disabled, no changes will be made.
    – Any other OS, no changes will be made.

I have tested this script on Windows 10/Server 2012 R2/16 and have had no issues.

The updated script can be found on my GitHub here:
Disable-PSv2

Comments are closed.