The issue I’m about to describe was reported as part of public bug bounty program. It was reported, bounty was granted1, and issue is now fixed. However, vendor disagreed to disclose the issue, therefor I will not name vendor or product. The technical details will still be presented, but slightly (sometimes heavilly…) redacted when necessary.
While working on several targets at the same time, I usually monitor system activities and look for my favorite issues. One thing on my list is checking if any named pipes are used. Named pipes can be used for variaty of interesting operations and common misconception is that it is impossible to peek into the exchanged data. Of course the data may be protected from casual peeking by non-privileged users, but with proper toolkit and enough privileges it can be easily analyzed.
Firstly, how to check if named pipes are used at all? Well, in this case I simply run following PowerShell code:
This code opens
\\.\pipe\ directory2 and reads FullName property of each named pipe. You should expect several default system’s named pipes, software specific pipes (e.g. from Dropbox client), and perhaps a lot of pipes from Google Chrome’s mojo sandbox. This time, I saw one more on the list.
The redacted part was pointing at the specific file, giving out pipe’s origin. Otherwise I could still use GetNamedPipeServerProcessId function to pinpoint specific process.
I wanted to check traffic exchanged thru that pipe. The great tool that helps with this is called IO Ninja. Its pipe monitor feature may be used to sniff entire pipe traffic or filter out specific bits. I set up filters for specific pipe and began listening. To get clear picture, I restarted the tested app and collected the traffic.
There are couple interesting things here:
- We can see interaction with identified pipe.
- The program executable that reads and writes to the pipe is the same, but PIDs are different. Looks like client <-> server communication implemented within single binary.
- There is another pipe opened. New pipe has additional suffix (_2).
- The data sent via pipe looks like serialized object definition.
I though that reversing the application might be helpful, so I opened the tested executable in PE editor. Turned out that this is .NET binary with no obfuscation whatsoever. Immidietly, I loaded it into dnSpy to confirm. There it was, full reversed source code in C#3. The executable also used lots of 3rd pary libraries, most of them also easy to inspect. I began searching for strings related to my named pipe. The search returned PipeListener class that was handling objects sent via named pipe:
Another class (Pipes) was responsible for naming the pipe objects:
The highlighted line explains how name was constructed. In our example,
userName is lowpriv and
userDomainName is DESKTOP-OMNIO40. The sessionId seems to be 2 in this case. The suffix, that we saw in second pipe, is apparently added elsewhere.
Another interesting place to check was 3rd party library that was responsible for actual reading bytes from named pipe. Based on the library name and its version, I tracked it down to github where I noticed that it’s actually abandoned OpenSource project with last changes made over 4 years ago. Not a great choice. It contains following logic responsible for pipe reading:
OK, let’s see. In line #3,
ReadLength to read first 4 bytes. In line #25 we can see that bytes are returned in network-to-host-order, something we will need to keep in mind. Specialized version of
ReadObject is then called - it reads exactly
len following bytes and performs deserialization of read bytes. The deserialization is made using
This code doesn’t seem to verify if
len value makes sense. Hence, it is possible to provide huge number which will result in either DoS or… memory corruption? Interesting idea, but there’s much easier exploitation path: unsafe deserialization of provided data.
To exploit unsafe deserialization, we need to prepare correct payload. Fortunately, there is no need to craft it manually, as there exists a great tool for that: ysoserial.net. When an application with the required gadgets on the classpath unsafely deserializes payload, the specified chain will automatically be invoked and cause the command to be executed on the application host. For instance, we can generate code that spawns notepad.exe.
We instructed tool to generate payload using gadgets related to
DataSet objects. If you base64-decode it, along with lots of binary data, you should see the following XML:
<ObjectDataProvider MethodName="Start" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:a="clr-namespace:System.Diagnostics;assembly=System"><ObjectDataProvider.ObjectInstance><a:Process><a:Process.StartInfo><a:ProcessStartInfo Arguments="/c notepad.exe" FileName="cmd"/></a:Process.StartInfo></a:Process></ObjectDataProvider.ObjectInstance></ObjectDataProvider>
Process object is defined along with our command to start notepad.exe using cmd.exe. This basically means that we can run arbitrary code.
The last question remains - how can we send payload to the application? Actually, I already made an little assumption that I should be able to create necessary pipe before the application does that and serve malicious content using my pipe. This technique is called pipe squatting and is fairly popular. With this assumption in mind, I began coding PoC in PowerShell. I chose it because I can just copy-paste parts of the code from original app and benefit from straightforward support for named pipes.
The pipes permissions are purposely set to FullControl for Everyone as we want to maximize attack scope. The remaining pipe settings are taken from reversed source code. Once first client connects (line #9), the payload is prepand with payload length (with bytes in network-order, see line #16), and sent to the client. If everything goes right, we shall achieve code execution and see notepad.exe started by tested application.
I started the PoC script, cleared IO Ninja log, and restarted the app.
The first part of the attack is verification if my assumption regarding pipe squatting is correct. On the picture, we can see that:
- One process informs another to use named pipe of name that we were able to foresight.
- The process is unable to start the pipe - this is because we already created named pipe with such name. If my assumption was correct, it should simply ignore Access is denied error.
The process then reads payload from… my pipe! This means that squatting worked and we were able to plant custom pipe instance. After that, the process crashed, but fresh notepad.exe instance is run by tested application before that.
So… did we just attack ourselves? Yes, just to prove the point! However, the tested application is meant to be run also in multi-user environment.
On a single host, multiple users may be working at the same time. We can modify pipe name, and instead of targeting lowpriv user, go for Administrator account. We could use ysoserial to execute code from batch file on external share and add new admin account to the system or relay captured NTLMv2 hash.
The attack scope depends on privileges associated with given user, but vulnerability is severe enough to get reverse shell or elevate local privileges.
Thanks for reading!
Funny enough, it was resolved as critical issue in critical asset with a note that “will be paid as a Low vulnerability”. ↩︎
This is Local Device path. Although it looks like UNC path, the
.here actually triggers translation to DosDevices directory where ‘pipe’ symlink is located. This is further translated into
dnSpy works on CIL - intermediate code, but it has option to translate it into C# or Visual Basic. ↩︎