作者:0xcc
原文链接:https://mp.weixin.qq.com/s/TrSb4hMD6QUVyCth3IaBbg
这篇文章是上一次 realworld ctf 论坛上的议题《Bifr?st 揭秘:VMware Fusion REST API 漏洞分析》。本来去年这时候就该动笔写一下,正好当时 InfiltrateCon 的主办方看到了这个议题,邀请了投稿,我就想着讲完再写。没料到四月份的会议因为疫情一直拖到现在,最后只能改成线上。
VMware Fusion 在去五月修复了这个编号为 CVE-2019-5514 漏洞。与常规的虚拟机安全问题,即通过 guest 攻击 host 的方式不同,这是一个远程代码执行漏洞。它从 host 的浏览器远程触发,在 guest 里执行任意代码。PoC 极度简单,却需要花不少功夫逆向才能分析明白。
除此之外我还(重新)发现了一种特殊配置下,不需要漏洞即可逃逸至 host 执行任意命令的方法。RWCTF 举办时还在和厂商沟通,所以我保留了这一部分没讲。这两部分串联起来之后,便是一个完整的 RCE 链条。 VM Fusion 11 引入了一个新功能,在 mac 桌面右上角添加了一个快捷菜单,可以操作虚拟机的开关机、快照等状态。当 guest 是 Windows 系统时,甚至可以将其“开始”菜单映射出来,在客户机里直接运行(任意?)程序。
这个功能引起了我的注意,事实上也找到了严重的问题。
经过简单的分析发现,这个菜单界面是一个 Electron 应用。而在后台有一个 go 语言 amsrv 进程开启了一个 HTTP 服务,通过 WebSocket 和 HTTP 协议与 Electron 界面通信,然后再由 amsrv 作为代理的角色操纵 VMware Fusion 剩余的组件。这个 http 服务的架构有些多余,毕竟 Electron 本身可以实现很多 native 的功能。笔者估计是因为 nodejs 嵌入 C 的库比 go 麻烦。
问题出在 HTTP 和 WebSocket 的鉴权上。开发者应该是认为 localhost 的应用没有做访问控制的必要,然而 WebSocket 默认支持跨域访问,也就是任何一个浏览器都可以与之通信。更神奇的是 HTTP 协议的部分还开启了 CORS,不限制请求来源。简单复现一下弹计算器的命令,然后在 Wireshark 对 loopback 抓包,便得到了一个 PoC:
const ws = new WebSocket(`ws://127.0.0.1:8698/ws`)
ws.onopen = () => {
ws.send(JSON.stringify({
name: 'menu.onAction',
object: '11 22 33 44 55 66 77 88-99 aa bb cc dd ee ff 00',
userInfo: {
action: 'launchGuestApp:',
vmUUID: '11 22 33 44 55 66 77 88-99 aa bb cc dd ee ff 00',
representedObject: 'calculator:'
}
}))
}
ws.onmessage = msg => {
console.log(JSON.parse(msg.data))
ws.close()
}
显然,其中的 vmUUID 字段是虚拟机的唯一标识符。但经过一番分析发现,这个字段保存在本地的一个 plist 文件里,Electron 通过解析对应的文件获取,而远程攻击者自然没有这个条件。这个格式也不能被简单的爆破。这个漏洞似乎不能利用,加上我一开始对逆向 go 没有耐心,于是就在网上公开了。
然而一段时间过后,我突然收到一条国外网友的私信说,他找到了利用的方法,让我非常震惊。
https://theevilbit.github.io/posts/vmware_fusion_11_guest_vm_rce_cve-2019-5514/
他在 WebSocket 协议里找到了一个隐藏的参数 selectedIndex,可以完全取代 vmUUID。这个整型的参数可以遍历,有微博网友称,测试时把自己所有的虚拟机全部打开挨个弹了一遍 cmd,当场大惊失色。
这件事说明,为了负责任地披露,在完全搞清楚一个漏洞的可利用性之前,不要随便地公开讨论。
这也勾起了我的好奇心,于是我把手上的 binary 又打开梳理了一遍。
Redress 工具简单查看了一下 amsrv,其实这个程序并不是真正的服务端,而是一个类似守护进程的角色。服务端实现是在另一个叫 vmrest 的程序。在 VMware 官网上可以找到这个 vmrest 的文档,标题叫《Use the VMware Workstation Pro REST API Service》。但这个程序的帮助信息里却提示有一个 -D 的命令开关可以以 internal 模式运行,说明事情没这么简单。
Redress 提示这个程序用到了 web 框架 gorilla。以此为线索搜索 github_com_gorilla_mux__Router_HandleFunc 的交叉引用,找到了所有的后端 API 接口:
WebSocket 后端的处理非常简单,在对 JSON 内容进行一些解码和重新序列化的操作和,几乎远洋通过 Mach Message 转发给下一个进程,也就是 VMware Fusion 主界面。我写了一端简单的 frida 脚本拦截 CFMessagePortSendRequest 函数调用,打印所有的进程间消息,也证实了这一点。
这一阶段的进程间通信主要靠 VMIPCServer 和 VMIPCClient 类实现。顾名思义,这一对 class 借鉴了 client/server 的角色,封装了基于消息的接口。在消息接收端有一个 VMIPCCommon 单例,其中的 observerTable 注册了所有的消息接收端。这是一个 NSDictionary 键值对,key 是指向具体 handler 类实例的指针,值则是一个 VMIPCObserverEntry 对象,记录了 WebSocket 协议当中的 name 字段和具体 handler 类处理方法的 selector 的对应关系:
回到弹计算器的 PoC 上。根据 observerTable 的映射关系,这个消息会触发 -[PLVMStartMenuProxy onMenuAction:] 方法。最终消息来到 -[PLVMStartMenuManager onStartMenuPerformAction:withInfo:],在这个函数里居然是优先检查 selectedIndex 参数,如果不存在,再尝试将另一个可选的 vmUUID 参数转换成 selectedIndex。这也就解释了一开始被我漏掉的 PoC 的现象。而为了搞清楚这一步,我们已经逆了至少三个程序。
@interface DUIVMActionController : DUIActionController <DUISettingsCDROMOpenPanelDelegate>
- (void)togglePause:(id)arg1;
- (void)installVirtualPrinter:(id)arg1;
- (void)toggleToolsInstall:(id)arg1;
- (void)onSendKey:(id)arg1;
- (void)sendCtrlAltDel:(id)arg1;
...
除了启动客户机程序之外,以上的方法也是对 WebSocket 开放的。
接下来便是主界面和 vmx 进程的通信。VMware Fusion 的每一个虚拟机都用一个独立的 vmx 进程执行,即使界面崩溃了,也不会影响到 vmx 的稳定性;反之亦然。
这个阶段的通信使用一个叫 vmdb 的机制。
这是一个类似键值对数据库的集中化状态管理,既支持数据的存储也提供跨进程传递数据的能力。一个 key 类似文件系统的路径,类似这样的字符串:
vmx/vigor/ields/Audio/
vmdb 支持的基本数据类型有整形、字符串、二进制等。既然叫做 db,它还提供了类似 SQL 的查询机制,但实际上只支持 SELECT WHERE 语句。首先用 Vmdb_ParseQuery 从“SQL”创建一个查询,接着用 Vmdb_ExecuteQuery 函数执行,并用 Vmdb_NextResult 遍历结果集。这个 db 还支持事件处理,可以用 Vmdb_RegisterCallback 在特定的键上添加监听函数处理状态变化。
与常见的虚拟机漏洞不同,这一次攻击载荷是反向传递的——从 host 到 guest。常规的漏洞利用一个叫 RPCI 的“后门”通信接口来调用宿主的功能,而与之对等的是 TCLO。两者其实区别不大,仅仅是 Message_Open 时传入的 magic number 不同。命令的格式也是函数名、空格,然后紧跟参数的 buffer。
在 Windows 上,VMware Tools 有两个进程,一个具有 SYSTEM 权限,一个普通用户权限。用来弹计算器的命令叫做 unity.shell.open,最后来到 GHIPlatformShellCommandRun 函数。
这个函数并不完全等同于 ShellExecute,它还内置了一些特殊 URL 的处理。
如果字符串不满足以上 URL,就会进入这个分支。只有命令包含一个“.”符号,才会触发支持命令行参数的逻辑:
比如 cmd /c calc 必须写成 cmd.exe /c calc,否则没有反应。这时候可以使用 Powershell 下载逃逸的 payload 完成整个链路。
对于非 Windows 虚拟机,例如 Linux,unity.shell.open 早在 2008 年的一个 commit 便被移除掉了。不过前文提到,有一个命令可以模拟键盘输入,而且这个命令并不需要客户机上安装了 VMTools。这就是 onSendKey: 方法。在 guest 没有被锁屏的情况下,仍然可以通过模拟按键的方式打开终端,回车执行任意命令。
官方在 2019 年五月初推出的 11.0.3 修复了这个问题,在 HTTP 协议中加入了随机 token 的验证;而 WebSocket 则要求 Origin: 满足 file:// 域。
在 VMware 的安全公告 VMSA-2019-0005 当中, 除了这个严重级别的远程代码执行,还有 Pwn2Own 2019 上 Fluoroacetate 用到的几个 VMware Workstation 逃逸漏洞,同样影响 Fusion。
此外在逆向 VMTools 时,我重新发现了一个已经被公开讨论过的特性,可以在特殊的虚拟机配置环境下无需漏洞逃逸到宿主机。
一旦勾选了这个“在 Mac 打开 Windows 的文件和链接”,便可使用 VMTool 内置的 VMwareHostOpen.exe 调用这一功能。这个选项显然极大削弱了 guest 和宿主之间的隔离性。经过一番搜索我发现 ZDI 在 2017 年已经在 DerbyCon 上公开讲过,只不过没有分析实现细节。
这个命令会转化成一条 RPCI 命令 ghi.guest.shell.action,参数为序列化的 xdr_GHIShellAction。它一共有两个成员,actionURI 永远是 x-vmware-action:///run,而 targetURI 则是希望打开的地址。
在这个函数里特别针对 file:/// 做了过滤,不做任何操作,有可能是为了安全考虑。但其他任意的 URL,例如 dict:// 是允许的。当 URL 的 sheme 是 x-vmware-share:// 时,会转换为宿主的地址,然后打开对应文件。
在客户机开启了任意一个可写的共享目录之后,攻击载荷可以写入一个可执行文件(例如 .command),内容是一个 shell 脚本。由于 HGFS 需要尽可能消除不同操作系统下的表现差异,在 mac 端默认便给足文件权限,包括可执行。这时候相当于宿主机双击打开文件,便会触发脚本命令。这便有了我们开头的动画。
$name = "calc.command";$writable = ls "\\vmware-host\Shared Folders" | Where-Object { $acl = Get-Acl $_.FullName; $acl.Access | Where-Object { $_.IdentityReference -eq 'Everyone' }} | Select-Object -first 1;
$output = Join-Path $writable.FullName -ChildPath $name;
$content = '#!/bin/sh
open -a Calculator
killall Terminal'
[IO.File]::WriteAllLines($output, $content);
& "C:\Program Files\VMware\VMware Tools\VMwareHostOpen.exe" "--url" "x-vmware-share://$($writable.Name)/$($name)";
VMware 官方认为这不是一个安全问题,毕竟需要两个条件:
1.允许打开 URL
2.开启至少一个可写的共享目录
然而我们再想一想,这两个条件都和虚拟机本身的配置有关。仍然有一种场景存在风险,那就是从网上下载一个虚拟机镜像然后双击启动——所有的配置自然都是攻击者可控的。
通过这个案例的分析可以看到,炫酷的 Web 技术虽然极大提升了开发效率,却把本不该出现的安全问题引入到桌面端产品中。VMware 一个看上去很小的功能却分了很多层次的调用,结构复杂却能保证架构的清晰。当然最大的教训还是,在确保分析完全之前不要随便低估一个漏洞的可利用性。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1550/