0x00 前言
Apache Guacamole是较为流行的一种远程办公基础框架,在全球范围内已有超过1000万次的docker下载。在研究过程中,我们发现Apache Guacamole存在多个严重的反向RDP漏洞,并且也受我们在FreeRDP中找到的一些新漏洞的影响。简而言之,如果攻击者成功入侵了某个组织的内部计算机,那么当正常用户尝试连接被控制的主机时,攻击者就可以利用这些漏洞来攻击Guacamole网关。恶意攻击者可以完全控制guacamole-server
,拦截并控制其他已连接的会话。
攻击过程可参考该视频,视频中我们成功利用这些漏洞控制Guacamole以及已连接的所有会话。
0x01 Apache Guacamole简介
随着疫情期间远程办公的兴起,我们认为针对远程办公的技术方案将变成一个热点的研究目标,因此选择Apache Guacamole作为这类解决方案的代表来研究。前文也提到过,这款产品是市场上最重要的工具之一。许多单位会使用该产品来连接网络,许多网络访问及安全产品也会在自身产品中集成Apache Guacamole,比如Jumpserver Fortress、Quali、Fortigate等。
经过一些背景调查后,我们绘制了典型的网络架构,如图1所示:
图1. 部署Apache Guacamole网关的典型网络架构
在这种场景中,员工使用浏览器连接公司在互联网上开放的服务器,进行身份认证,然后就可以访问公司内的主机。员工在使用浏览器时,guacamole-server
会选择某种支持的协议(RDP、VNC、SSH等),使用开源客户端连接企业网内的特定主机。连接成功后,guacamole-server
会充当中间人角色,来回传递事件,将数据从所选的协议转换为特定的“Guacamole Protocol”,反之亦然。
了解该架构后,我们可以考虑一些攻击场景:
1、反向攻击场景:企业网内部被入侵的一台主机可以利用入站连接来攻击网关,希望能控制目标网关。
2、恶意员工场景:恶意员工使用网内主机,利用链路两端节点的控制权来控制目标网关。
0x02 是否需要0-Day
在深入研究代码之前,我们先简单了解一下FreeRDP。在之前针对反向RDP攻击的研究中,我们找到了这款RDP客户端中的一些漏洞,攻击者可以通过恶意RDP“服务器”来利用该漏洞。换而言之,企业网内的恶意主机可以控制连接该主机的正常FreeRDP客户端。我们提供了针对某个漏洞(CVE-2018-8786)的基本PoC,也演示了RCE(远程代码执行)场景。
观察Apache Guacamole的发行版本,可以看到只有2020年1月底发布的1.1.0版中增加了对最新版FreeRDP(2.0.0)的支持。由于FreeRDP只在2.0.0-rc4版中修复了我们发现的漏洞,这意味着2020年1月之前发布的所有版本使用的都是存在漏洞的FreeRDP版本。
因此我们对0-Day漏洞的挖掘暂告一段落,推测大多数企业可能还没有升级至最新版本,可以使用已知的1-Day漏洞进行攻击。然而我们还是决定再次挖掘RDP协议中的漏洞,具体研究对象包括:
1、guacamole-server
代码,只关注其中对RDP协议的支持代码。
2、最新版FreeRDP的代码:2.0.0-rc4版。
我们设想的利用条件限定于默认安装的版本,也就是说,只能使用默认情况下处于启用状态的功能,并且希望不需要与客户端进行任何交互。
0x03 寻找新漏洞
由于我们对FreeRDP的代码非常熟悉,对RDP整体也比较了解,因此很快就找到了一些漏洞。
CPR-ID-2141:信息泄露漏洞
CVE编号: CVE-2020-9497
文件:
protocols\rdp\channels\rdpsnd\rdpsnd-messages.c
函数:
guac_rdpsnd_formats_handler()
备注:由于Apache并没有将我们报告的漏洞(CPR-ID)与他们公布的CVE-ID一一对应,因此我们主要根据他们提供的CPR-ID来对应具体漏洞,这样更准确一些。
为了在RDP连接以及客户端之间中继消息,开发者为默认RDP通道实现了一种扩展。其中有个通道负责获取服务端的音频,因此被称之为rdpsnd
(RDP Sound)。
然而在实际场景中,guacamole-server
与FreeRDP之间的集成节点很容易出错。传入的消息经过FreeRDP的wStream
对象封装,因此相应的数据应该使用该对象的API来解析。然而如图2所示,开发者忘了设置一个强制条件:传入的流对象所包含的字节数必须与数据包所声明的字节数相匹配。
图2. 缺少输入过滤导致出现越界读取
恶意RDP服务器可以发送一个恶意的rdpsnd
信道消息,导致客户端认为数据包中包含大量字节,而这些字节实际上为客户端内存中的字节。这样一来,客户端就会向服务端返回包含这些字节的响应包,导致RDP服务端最终得到了大量数据,实现类似心脏滴血的信息泄露原语。
CPR-ID-2142:信息泄露漏洞
CVE编号: CVE-2020-9497
文件:protocols\rdp\channels\rdpsnd\rdpsnd.c
函数:guac_rdpsnd_process_receive()
在这个RDP通道中,有另一个消息还存在类似的漏洞。这次越界(Out-of-Bounds)数据会发送给客户端,而不是发送回RDP服务端。
图3. 类似的越界读取漏洞,向客户端泄露信息
这个漏洞会将信息泄露给客户端,我们希望能够在客户端不知道网关已被攻击的情况下,开发出可用的利用代码。
CPR-ID-2143:信息泄露漏洞
CVE编号: CVE-2020-9497
文件:protocols\rdp\plugins\guacai\guacai-messages.c
函数:guac_rdp_ai_read_format()
我们找到了另一个通道:guacai
,该通道负责声音消息(“音频输入”),因此名为guacai
。尽管该漏洞存在前面类似的漏洞,但默认情况下处于被禁用状态。
图4. 与第一个漏洞类似的越界读取漏洞
此时我们已经找到了3个主要的信息泄露漏洞,应该足以绕过ASLR(地址空间布局随机化),然而我们仍需要一个内存破坏漏洞来构造完整的利用链。由于研究进入瓶颈期,我们开始重新分析FreeRDP,希望能找到之前可能被遗漏的漏洞。
0x04 熟悉的FreeRDP
与上一次相比,RDP客户端中并没有太多改动,打上补丁的版本仍然为当时最新的版本。我们先来理解该客户端涉及到的wStream
类型中关键要素,该结构的相关字段如图5所示:
图5. 用来封装传入/传出数据包的wStream
对象
这是一个典型的流封装结构,包括:
-
buffer
:指向传入数据包头的指针。 -
pointer
:指向传入数据包内头部的指针。 -
length
:传入数据包的字节数。
在解析传入流的指定字段之前,代码应该先检查数据流大小是否足以容纳相应数据。检查逻辑如图6所示:
图6. 使用Stream_GetRemainingLength()
检查可用的输入
这个输入检查非常重要,每次解析或跳过某个字段时,指针字段都会相应地往前挪动。在执行下一次检查时对应的代码逻辑如下:
图7. 使用当前流的头部来计算剩余长度
当指针字段越过传入数据包的末尾时,这个计算逻辑将会向下溢出,从而返回一个巨大的无符号值,代表剩余字节为负数。简而言之,只要有溢出检查失效,剩余的检查过程将毫无意义。这种设计思路导致FreeRDP很容易受到越界读取漏洞所影响,下文我们将分析这一点。
在提供这类越界读取漏洞之前,我们先了解一下这些漏洞的重要性。通常情况下,读取操作只有在以某种方式向攻击者返回读取的字节时才有能够利用。否则读取操作只能用来访问内存中未映射的页面,使程序崩溃。Apache Guacamole攻击场景比较特殊,这里我们能够接触连接的两端节点。比如,如果内存字节对应的是屏幕的图形更新数据,那么这些更新数据也会被发送给连接的客户端。
在这种场景下,每个越界读取漏洞都有可能变成微小却有用的信息泄露漏洞。
CPR-ID-2145 & CPR-ID-2146:FreeRDP越界读取漏洞
由于wStream
对象设计中存在缺陷,我们只需要寻找没有被过滤的读取操作即可。这个思路很准确,我们找到了两个漏洞:CPR-ID-2145以及CPR-ID-2146。
然而当我们向厂商报告这两个漏洞时,我们发现这些漏洞已经由其他研究人员报告过。即使我们只晚了几个小时,但胜利果实也必须归他人所有。
因此我们决定让其他人发表漏洞成果,在本文中不再详细介绍这些漏洞。
此时我们已经找到了5个漏洞,可以充当攻击场景中的信息泄露原语。然而我们至少还需要找到一个内存破坏漏洞,而在FreeRDP中寻找这类漏洞会比较麻烦,每次都会被代码中的检查逻辑所干扰。很多情况下,这种检查逻辑针对的都是我们报告过的漏洞,比较无奈。
现在我们的心情与Zensploitation(@zensploitation)的某篇推文非常相似:
图8. https://twitter.com/zensploitation/status/1244598246879547393
我们毕竟走了不少路,因此绝不轻言放弃。我们决定再次研究一下guacamole-server
,这次终于有所收获。
CPR-ID-2144:内存破坏漏洞
CVE编号: CVE-2020-9498
文件:protocols\rdp\plugins\guac-common-svc\guac-common-svc.c
函数:guac_rdp_common_svc_handle_open_event()
这个RDP协议会将不同的“设备”当成不同的“通道”,每个设备一个通道。比如声音对应的是rdpsnd
通道,剪贴板对应cliprdr
等。作为抽象层,通道消息支持分片,最大支持的消息为4GB。为了正确支持rdpsnd
以及rdpdr
(设备重定向)通道,guacamole-server
的开发者添加了一个附加的抽象层,具体实现位于guac_common_svc.c
文件中。该文件中对分片的处理逻辑如图9所示:
图9. 处理传入通道分片的代码片段
从上图可知,第一个分片必须包含CHANNEL_FLAG_FIRST
片段,当处理该分片时,代码会根据声明的消息总长度来分配一个流。
然而如果攻击者发送的分片不包含该标志时,会出现什么情况呢?代码似乎会将分片附加到先前的剩余流中,这看上去像是可能存在的一个“悬空指针”漏洞。现在我们只需要检查开发者在完成前一个分片消息的处理后,是否还记得将该指针置为NULL
。
图10. 在未清除悬空指针的情况下释放已使用的流
如图10所示,在完成分片消息重组及解析后,该流会被释放。也就是说,代码没有将悬空指针设置为NULL
!
恶意RDP服务器可以发送一个乱序消息片段,使用之前已被释放的wStream
对象,从而变成一个UAF(释放后重用)漏洞。最重要的是,wStream
对象是我们最后可能用来实现该漏洞的对象,因为如果指针字段指向所需的内存地址时,就能用来实现任意写原语。由于我们在rdpsnd
通道中有个信息泄露漏洞,紧跟在恶意wStream
对象被使用之后。因此经过努力后,我们可以通过精心构造的wStream
对象,将原始漏洞变成更强大的任意读取利用原语。
RCE(远程代码执行)
前面提到过,我们可以利用CVE-2020-9497以及CVE-2020-9498漏洞,最终实现任意读取及任意写入利用原语。利用这两个强大的原语,我们成功实现了RCE(远程代码执行)场景,当远程用户请求连接网内恶意配合的计算机(也就是我们的RDP“服务器”)时,我们就可以控制guacd
进程。
图11. 漏洞利用结果:使用被控制的guacd
进程弹出计算器
但我们的旅途还未结束。guacd
进程只处理单个连接,并且以较低权限运行。通常情况下,此时我们需要一个提权(PE)漏洞才能控制整个网关。在与Apache的漏洞协同分析过程中,对方也怀疑这种攻击场景是否真的可以实现:我们能否通过单个guacd
进程来控制网关中所有的连接?
现在我们来寻找这个问题的答案。
“在计算领域,真正了解整个系统的人只有一个:攻击者”
——转自Halvar Flake在offensivecon 2020上的演讲。
0x05 深入分析Apache Guacamole
如果深入分析前文提到的Guacamole网关,我们可以看到如下结构:
图12. Apache Guacamole结构中的关键节点
在权限提升上,我们重点关注如下两个组件:
1、guacamole-client
:上图中的Web服务器
2、guacamole-server
:上图中的代理节点
guacamole-client
guacamole-client
组件为执行用户认证的web服务器。这个web服务器保存每个用户会话所需的配置,存储的信息包括:
1、所需的协议:通常为RDP。
2、网内员工PC的IP地址。
3、其他信息。
当客户端成功通过身份认证后,guacamole-client
会向guacamole-server
发起Guacamole Protocol会话,为客户端创建匹配的会话。guacamole-client
会连接guacamole-server
的TCP 4822端口(默认端口),guacd
进程在该端口上监听。
创建会话后,guacamole-client
的作用是在guacamole-server
以及客户端浏览器之间来回中转消息。
guacamole-server
根据Apache的官方文档:“guacd
是Guacamole的核心”。在启动后,guacd
会在TCP 4822端口上监听,等待来自guacamole-client
的连接。需要注意的是,该端口上的通信没有使用身份认证或者加密(支持SSL,但默认情况下并没有启用)。因此,我们在图12中添加了2个防火墙,负责限制对该TCP端口的访问,只允许来自guacamole-client
的连接。
建立连接后,guacd
会创建一个新线程,调用负责初始化Guacamole Protocol的函数。此时,用户有两个选项可用:
1、创建一个新连接;
2、加入已有的连接。
备注:这里我们使用的是“连接”(connection)而不是“会话”(session),因为这个词在Guacamole中指的是与指定计算机的连接。每个计算机有一个连接,多个用户可以共享相同的连接。这里不存在“用户会话”,因为整个设计理念都基于与特定计算机的连接,用户只需要加入连接即可。
目前最常用的是第一个选项。此时系统会为新创建的连接生成一个随机的唯一ID(UUID),然后fork()
一个进程来处理。UUID与新进程的对应关系存在内存中名为proc-map
的一个字典中,UUID会被发送回guacamole-client
。需要注意的是,新生成的进程在启动与内网主机的连接前会立即释放权限。
第二个选项比较特别,应该已经实现,这样多个用户才能共享一个连接,协同工作。在这种场景中,用户提供连接的UUID来请求加入已有的连接。为了区分不同用户,创建该连接的用户为“所有者”,其他用户的owner
字段被设置为false
。这个选项还支持将非“所有者”的用户连接设置成只读连接。
图13. 添加新用户,在proc-map
中存储进程,允许其他用户加入
为了支持用户加入,为特定连接生成的进程会继承一个socket对,用来与guacd
父进程通信。当主线程初始化所需的客户端时(比如为RDP连接初始化FreeRDP),另一个线程会等待来自父进程的消息,当新用户要求加入连接时向我们的进程发送信号。
guacd
进程充当一个连接管理器角色,为每个连接生成进程,同时也为生成的进程实现了核心逻辑。因此从现在开始,我们将guacd
父进程称之为“主进程”。
0x06 权限提升
步骤0:控制单个guacd进程
前面已介绍过利用方式。
步骤1:伪装为guacamole-client
虽然我们控制的guacd
进程只是运行在网关内的一个低权限进程,但仍具有一些有用的权限。首先,由于该进程运行在网关上,因此我们可以通过TCP 4822端口连接主进程。由于主进程在该端口上没有身份认证,因此我们可以成功连接,像正常guacamole-client
一样控制该进程。
步骤2:从内存中获取秘密
这是漏洞利用的关键点。由于guacd
可执行文件中同时包含主进程以及每个连接进程的逻辑,因此当生成新进程时只用到了fork()
。这里我们要强调一点:只使用了fork()
,并没有使用execve()
。
这意味着什么呢?fork出来的进程包含父进程的整个内存快照,而当调用execve()
时,这个快照会被新的映像所替代。没有调用该函数时,子进程将继承父进程的整个内存地址空间。其中包括:
1、完整的内存布局:当我们想攻击父进程时,这个信息非常有用。
2、完整的内存内容:存放在主进程中的每个秘密信息都会提供给子进程。这意味着我们的进程也拥有proc-map
映射,其中包含每个UUID与各自进程的对应关系。我们只需要在自己的内存中找到这个数据结构,就能拥有当前活跃的所有的UUID。
定位proc-map
结果只是个技术上的过程。在漏洞利用中,我们从/proc/<pid>/maps
中读取到了我们进程的内存布局文件。该数据结构比较大,因此被mmap()
到独立的内存分配空间,在文件中具有自己的条目信息。
步骤3:加入所有会话
我们已经能够向主进程发起Guacamole Protocol请求,现在也知道了需要发送哪个请求。接下来我们需要根据连接对应的UUID,来请求加入每个已有的连接。
图14. 日志显示我们成功加入了已有的连接
令人惊讶的是,“只读”会话属性由guacamole-client
来设置。这意味着尽管我们不是连接的所有者,仍然可以关闭“只读”权限,获得该连接的完整权限。此外,除了主进程中的日志消息外(图14),没有其他信息表明另一个用户加入了该连接。
步骤4:重复操作
如果大家仔细观察,可能会注意到我们攻击计划中的一个缺点:我们的guacd
进程拿到的是过时的proc-map
映射信息。如果在我们进程生成后,网关又开启了其他会话,那么这些会话只会在实际的proc-map
中更新,不会反映到我们已过时的内存映像中。
这个缺点解决起来也比较简单。在指定间隔后(比如每5分钟),我们可以向主进程发送命令,启动新的RDP连接,连接网内被我们控制的主机。通过这种方法,我们可以定期生成新的guacd
,拿到新版的proc-map
。再结合我们原始的漏洞,我们同样可以攻击这个进程,“刷新”我们的proc-map
。
将这些元素结合在一起后,完成的利用链可参考此处视频,其中我们实现了RCE + PE。
0x07 时间线
- 2020年3月31日:向Apache披露漏洞。
- 2020年3月31日:Apache回应,要求提供更多信息。
- 2020年3月31日:向FreeRDP披露漏洞。
- 2020年3月31日:FreeRDP回应,要求提供更多信息。
- 2020年3月31日:FreeRDP告知我们CPR-ID-2145和CPR-ID-2156都已重复提交,有其他研究者在2020年3月30日分别报告过。
- 2020年5月8日: Apache在某次commit中悄悄修复了漏洞。
- 2020年5月10日:向Apache反馈,表示官方补丁的确修复了我们报告的所有漏洞。
- 2020年5月12日:Apache为我们报告的4个漏洞发布了2个CVE-ID。
- 2020年6月28日:Apache发布了正式补丁版本– 1.2.0。
0x08 总结
这次我们展示了反向RDP攻击的一个新角度,这是我们在2019年年初提出的一个攻击场景。用户通常认为这类攻击场景影响的是RDP客户端,而Apache Guacamole给了我们不同感受。在正常情况下,攻击者可以使用客户端中的漏洞来控制单个公司网内计算机。然而当在网关中部署时,这类漏洞对企业的影响将更为严重。
在疫情期间,远程办公是必不可少的一个环节,然而我们并不能忽略这种远程连接中存在的安全隐患。这里我们以Apache Guacamole为研究目标,成功演示了攻击者如何借助企业内部被入侵的计算机来控制负责处理所有内网连入会话的网关。成功控制网关后,攻击者就能窃听所有的连入会话,记录所使用的凭据,甚至可以启动新会话,控制企业内的其他计算机。在目前远程办公的大背景下,这个入侵点就有可能让攻击者完全控制整个企业网络。
我们强烈建议所有用户使用最新版的服务器,不论大家使用的是什么远程办公技术,一定要确保已打全补丁,才能阻止这类攻击活动。