The full house of vulnerabilities behind the Microsoft Exchange hack

Developers
C#
Windows

Everyone talked about the attacks on Microsoft Exchange servers, but what about the vulnerabilities that enabled them?

Hot on the heels of the SolarWinds hack, hundreds of thousands of organizations were hacked between January 2021 and March 2021 through at least four (possibly seven!) different zero-day vulnerabilities in the Microsoft Exchange e-mail server software. The vulnerabilities have been fixed (the fixes are available in the Microsoft April 2021 Security Update), and at the end of March, Microsoft declared that 92% of all deployments have been patched against the attack. That said, deployments will remain vulnerable until the April update is applied.

Just like SolarWinds, this attack is suspected to have been originally carried out as targeted long-term espionage activity by Advanced Persistent Threats (APTs) with considerable resources at their disposal. However, unlike SolarWinds, this was not a supply chain attack. The APT originally managed to breach these systems by actively exploiting vulnerabilities; this eventually turned into nine distinct criminal groups doing simultaneous exploitation of the same vulnerabilities, gaining footholds in vulnerable systems, and deploying the ‘DearCry’ ransomware (named as such due to the similar file encryption headers to 2017’s WannaCry). Due to the severity of the situation, Microsoft had to release security patches for versions of Microsoft Exchange Server that are out of support (just like they released Windows XP patches for WannaCry back in 2017).

As for the name Hafnium: normally it’s a type of metal used for a lot of things from microprocessors to nuclear reactors, but in this context it’s the name coined by Microsoft’s Threat Intelligence Center for the APT behind this attack.

The CISA alert contains information about the exploitation and mitigation of the vulnerability; however, the vulnerabilities that ultimately caused the issue are not discussed in depth. Let’s take a look at the design flaws and bugs behind the attack – and draw some very important conclusions that are relevant for any kind of software (especially when it comes to implementing authentication).

Can you invoke this API for me as NT AUTHORITY\SYSTEM, please?

The first step in the exploit chain – and, arguably, the lynchpin of the attack – is the ProxyLogon vulnerability (CVE-2021-26855) discovered by the security researcher Orange Tsai of DEVCORE.

Several security companies have discovered active exploitation of this vulnerability going back to early January 2021 – Volexity posted the first detailed write-up on it. In particular, Praetorian’s analysis is very valuable from a software security perspective – the researchers have reverse engineered the patches to identify the vulnerable code and reproduced the exploit chain in full. The analysis revealed that ProxyRequestHandler composed a URI based on user input in an insecure way (specifically, this.AnchoredRoutingTarget.BackendServer.Fqdn and this.AnchoredRoutingTarget.BackendServer.Version were parsed from the X-BEResource cookie earlier).

protected virtual Uri GetTargetBackEndServerUrl() {
    // …
    UriBuilder clientUrlForProxy = new UriBuilder(this.ClientRequest.Url);
    clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
    clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;
    clientUrlForProxy.Port = 444;
    if (this.AnchoredRoutingTarget.BackendServer.Version < Server.E15MinVersion) {
        this.ProxyToDownLevel = true;
        clientUrlForProxy.Port = 443;
    }
    return clientUrlForProxy.Uri;
}

(Code source: Praetorian)

UriBuilder is a standard C# class whose ToString() function basically performs string concatenation on various fields within the object. In this case, the attacker can control the Host field (it is parsed from the X-BEResource cookie). Since the part of the URL after the destination anchor (#) is not processed by the server, an attacker can set a host value that ends with the # character and thus remove the rest of the URL from the process, like in an SQL injection attack. Thus, a Host value of targetsite.com/api/someendpoint# will tell the server to make a request to https://targetsite.com/api/someendpoint regardless of the other fields in the URI object.

This can be used to carry out a so-called server-side request forgery (SSRF) attack – where the attacker tricks the server into making a request to an attacker-controlled URL. By carefully setting the value of this URL, the request may be targeted at APIs exposed over the intranet (e.g. http://192.168.0.1/admin/shutdown), local files (e.g. file:///etc/passwd) or internal services that are normally not accessible to outside requests (via schemes such as data://, php:// or telnet://).

In this case, the target URL was a Microsoft Exchange server API. Basically, the attacker had to

  • Make a request to an /ecp/{staticfile} URL (which didn’t have to be valid, as long as {staticfile} had an extension of a static file such as .js or .css) to trigger an HTTP request to the backend
  • Put an X-BEResource cookie into the request containing a URL to an Exchange server endpoint, terminated with a #
  • Specify a very high version number in the X-BEResource cookie so ProxyToDownLevel could not be set to true (this would lead to an authentication failure down the line)

If the attacker performed all of these steps, the proxy called the endpoint specified by the attacker while also performing Kerberos authentication as NT AUTHORITY\SYSTEM (i.e. LocalSystem).  Based on this knowledge, ProxyLogon can be exploited as part of a spoofing attack to gain access to Exchange server APIs. While this is bad enough – it could be used to steal emails, for example –, at this point the attacker doesn’t have the capability to execute arbitrary code… yet. For that, it is necessary to exploit a different vulnerability, which is where the other three CVEs come into play.

vulnerability

Some other vulnerabilities – deserializing arbitrary objects like it’s 2012

The first of these vulnerabilities – CVE-2021-26857 – exploited insecure deserialization to accomplish this goal if Unified Messaging was enabled on the target server. Insecure deserialization is a common vulnerability type, especially on .NET and Java platforms – it is #8 on the OWASP Top Ten for a reason.

The vulnerable source code in Base64Deserialize(), called by Microsoft.Exchange.UM.UMCore.PipelineContext.FromHeaderFile():

internal static object Base64Deserialize(string base64String)
{
    object obj = null;
    using (MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(base64String))) {
        obj = ExchangeBinaryFormatterFactory.CreateBinaryFormatter(null).Deserialize(memoryStream);
    }
    return obj;
}

(Code source: Praetorian)

The key problem here is the use of BinaryFormatter. There are multiple ways to deserialize objects in .NET, and BinaryFormatter is the most straightforward – it takes a binary data stream and transforms it into an object. However, that particular deserializer is very vulnerable – if the attacker constructs an object in the right way, it will force execution of arbitrary code as part of the deserialization process. This was originally demonstrated for .NET by James Forshaw in the Are You My Type? Breaking .NET Sandboxes Through Serialization Black Hat 2012 presentation. There is also an attack technique – called Property Oriented Programming, or POP – that can be used to construct malicious payloads, originally demonstrated by Chris Frohoff and Gabriel Lawrence in the Marshalling Pickles AppSec Cali 2015 talk. In .NET, the ysoserial.net tool can be used to generate such payloads automatically. And for this reason, Microsoft’s own code quality rules and security documentation clearly both say that BinaryFormatter should not be used!

Note that this is not the first time a deserialization vulnerability was exploited in the Universal Messaging component of the Exchange server – this 2018 write-up on CVE-2018-8302 demonstrates a very similar attack where a transcription service deserialized a certain object with BinaryFormatter.

When it rains vulnerabilities, it pours…

But insecure deserialization was not the only RCE vulnerability. CVE-2021-27065 was used to write an .ASPX web shell to the server, which was then accessible through the web interface to execute arbitrary .NET code. According to Praetorian’s analysis, the vulnerability was in Microsoft.Exchange.Management.ControlPanel.DDIService.WriteFileActivity (only showing the relevant part of the function here):

    // …
    DataRow dataRow = dataTable.Rows[0];
    string value = (string)input[this.InputVariable];
    string path =
(string)input[this.OutputFileNameVariable];
    RunResult runResult = new RunResult();
    try { 
        runResult.ErrorOccur = true;
        using (StreamWriter streamWriter = new
StreamWriter(File.Open(path, FileMode.CreateNew))) {
            streamWriter.WriteLine(value);
        }
    }
    // …

(Code source: Praetorian)

Obviously, this function does not validate value or path, thus the attacker could use it to create a file with arbitrary contents anywhere on the file system… eventually using it to create an .aspx file in a directory such as /ecp/auth. They could then exploit this to upload a so-called web shell that executed arbitrary OS commands when the attacker made an HTTP request to the right URL. 10 such webshells were identified in the CISA alert (all variants of the ‘China Chopper’ web shell used by Hafnium), though the list is not necessarily complete.

While technical details on CVE-2021-26858 are still rather scarce, it is a similar post-authentication arbitrary file write vulnerability to CVE-2021-27065, so its mechanics are likely to be similar as well.

Up to this point, we have discussed the four vulnerabilities that are mentioned in any kind of article or write-up about Hafnium. But the story doesn’t end there. There are three other critical RCE vulnerabilities in the Exchange server that Microsoft fixed in the same patch; each of these could potentially allow attackers to execute code as SYSTEM. These were discovered by security researcher Steven Seeley and were only disclosed on March 9th:

  • CVE-2021-26412: Time-of-check-to-time-of-use (TOCTOU) vulnerability and lack of input validation when processing DLP policy rule parameters, allowing attackers to execute code once they’re authenticated with the “Data Loss Prevention” role
  • CVE-2021-26854: Insufficient input validation of DLP policy template data allowing remote attackers to execute code once they are authenticated with the “Data Loss Prevention” role
  • CVE-2021-27078: Insufficient input validation of rule collection allowing remote attackers to execute code once they are authenticated with the “Records Management” role

While details about these vulnerabilities were not publicly disclosed, even the short descriptions are telling a clear story – the code had too much trust in the validity of inputs coming from authenticated users, and didn’t consider this input to be on the application’s attack surface. Thus, a vulnerability like ProxyLogon could give attackers a lot of opportunities to get malicious data into the system (possibly more BinaryFormatter deserialization?), ultimately resulting in code execution. It also wouldn’t be surprising to see other ‘authentication required’ RCE vulnerabilities in Microsoft Exchange pop up as the Hafnium incident draws more attention from security researchers.

Don’t put your security eggs in one basket

When looking at the hack from a birds-eye perspective, it is obvious that the ProxyLogon vulnerability made it all possible – once authenticated, attackers had at least 6 different ways of executing code locally, and at least one of them exploited the insecure BinaryFormatter deserializer. Basically, the code trusted that attackers would not be able to get past the authentication control with the right roles / privileges, and thus would not be able to access the vulnerable functions. This basically makes authentication the single point of failure, which is a very dangerous design anti-pattern!

Secure software design goes in the other direction by applying the principle of defense-in-depth: instead of using a single layer to catch potential attacks, it’s much better to layer multiple protections. From a programming perspective, this means that – while it is important to have a robust authentication system – all input that enters the program from outside must be validated before use. This is in line with the Zero Trust Architecture concept that has been quickly gaining traction among security experts and was standardized by NIST.

As for the vulnerabilities enlisted in the article: all of them are well-known security problems, and so there are also well-established best practices to prevent them. Validating file paths can stop arbitrary file access vulnerabilities and escaping user data before writing it into a file can limit successful exploitation; the Server Side Request Forgery Prevention Cheat Sheet from OWASP is a good starting point to stop vulnerabilities like ProxyLogon; and finally, the Deserialization Cheat Sheet gives a lot of good information about deserializing potentially malicious data safely. For instance, Microsoft fixed the deserialization vulnerability by validating the incoming data before deserializing the object, and addressed the direct file write vulnerability by forcing a .txt extension on the filename before writing to it (which, while not a good validation strategy to follow in general, worked well to prevent the vulnerability in this case). The ProxyLogon vulnerability itself was fixed by adding whitelist-based input validation to the backend URI value (Fully Qualified Domain Name – FQDN) instead of just accepting any URL – which makes sense, since this value comes from an attacker-controlled cookie!

All aforementioned coding mistakes – and many more – are covered in our secure coding courses. Check them out in our catalog.