CVE-2020-0863 - An Arbitrary File Read Vulnerability in Windows Diagnostic Tracking Service

Although this vulnerability doesn’t directly result in a full elevation of privileges with code execution as NT AUTHORITY\SYSTEM, it is still quite interesting because of the exploitation “tricks” involved. Diagnostic Tracking Service (a.k.a. Connected User Experiences and Telemetry Service) is probably one of the most controversial Windows features, known for collecting user and system data. Therefore, the fact that I found an Information Disclosure vulnerability in this service is somewhat ironic. The bug allowed a local user to read arbitrary files in the context of NT AUTHORITY\SYSTEM.

DiagTrack RPC Interfaces

This time, I won’t talk about COM but pure old school RPC so, let’s check the interfaces exposed by Diagtrack thanks to RpcView.

We can see that it has quite a few interfaces but we will focus on the one with the ID 4c9dbf19-d39e-4bb9-90ee-8f7179b20283. This one has 37 methods. This makes for quite a large attack surface! :wink:

The vulnerability I found lied in the UtcApi_DownloadLatestSettings procedure… :smirk:

The “UtcApi_DownloadLatestSettings” procedure

RpcView can generate the Interface Definition Language (IDL) file corresponding to the RPC interface. Once compiled, we get the following C function prototype for the UtcApi_DownloadLatestSettings procedure.

long DownloadLatestSettings( 
    /* [in] */ handle_t IDL_handle,
    /* [in] */ long arg_1,
    /* [in] */ long arg_2
)

Unsurprisingly, the first parameter is the RPC binding handle. The two other parameters are yet unknown.

Note: if you’re not familiar with the way RPC interfaces work, here is a very short explanation. While working with Remote Procedure Calls, the first thing you want to do is get a handle on the remote interface using its unique identifier (e.g. 4c9dbf19-d39e-4bb9-90ee-8f7179b20283 here). Only then, you can use this handle to invoke procedures. That’s why you’ll often find a handle_t parameter as the first argument of a procedure. Not all interfaces work like this but most of them do.

After getting a binding handle on the remote interface, I first tried to invoke this function with the following parameters.

RPC_BINDING_HANDLE g_hBinding;
/* ... initialization of the binding handle skipped ... */
HRESULT hRes;
hRes = DownloadLatestSettings(g_hBinding, 1, 1);

And, as usual, I analyzed the file operations running in the background with Process Monitor.

Although the service is running as NT AUTHORITY\SYSTEM, I noticed that it was trying to enumerate XML files located in the following folder, which is owned by the currently logged-on user.

C:\Users\lab-user\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\LocalState\Tips\

The user lab-user is the one I use for my tests. It’s a normal user with standard privileges and no admin rights. This operation originated from a call to FindFirstFileW() in diagtrack.dll.

The folder seems to be empty by default so I created a few XML files there.

I ran my test program again and observed the result.

This time, the QueryDirectory operation succeeds and the service reads the content of file1.xml, which is the first XML file present in the directory and copies it into a new file in the C:\ProgramData\Microsoft\Diagnosis\SoftLandingStage\ folder (with the same name).

The same process applies to the two other files: file2.xml, file3.xml.

Finally, all the XML files which were created in C:\ProgramData\[…]\SoftLandingStage are deleted at the end of the process.

Note: I created a specific rule in Procmon to highlight CreateFile operations occurring in the context of a DeleteFile API call.

The CreateFile operations originated from a call to DeleteFileW() in diagtrack.dll.

The Arbitrary File Read Vulnerability

The files are not moved with a call to MoveFileW() or copied with a call to CopyFileW() and we cannot control the destination folder so, a local attacker wouldn’t be able to leverage this operation to move/copy an arbitrary file to an arbitrary location. Instead, each file is read and then the content is written to a new file in C:\ProgramData\[...]\SoftLandingStage\. In a way, it’s a manual file copy operation.

The one thing we can fully control though is the source folder because it’s owned by the currently logged-on user. The second thing to consider is that the destination folder is readable by Everyone. It means that, by default, new files created in this folder are also readable by Everyone so this privileged file operation may still be abused.

For example, we could replace the C:\Users\lab-user\AppData\Local\Packages\[…]\Tips folder with a mountpoint to an Object Directory and create pseudo symbolic links to point to any file we want on the file system.

If a backup of the SAM file exists, we could create a symlink such as follows in order to get a copy of the file.

C:\Users\lab-user\AppData\Local\Packages\[…]\Tips -> \RPC Control
\RPC\Control\file1.xml -> \??\C:\Windows\Repair\SAM

Theoretically, if the service tries to open file1.xml, it would be redirected to C:\Windows\Repair\SAM. So, it would read its content and copy it to C:\ProgramData\[…]\SoftLandingStage\file1.xml, making it readable by any local user. Easy, right?! :sunglasses:

Well… Wait a minute. We have two problems here. :confused:

  1. The FindFirstFileW() call on the Tips folder would fail because the target of the mountpoint isn’t a “real” folder.
  2. The new file1.xml file which is created in C:\ProgramData\[…]\SoftLandingStage is deleted at the end of the process.

It turns out that we can work around these two issues using an extra mountpoint, several bait files and a combination of opportunistic locks (see the details in the next parts).

Solving The “FindFirstFileW()” Problem

In order to exploit the behavior described in the previous part, we must find a way to reliably redirect the file read operation to any file we want. But, we cannot use a pseudo symbolic link straight away because of the call to FindFirstFileW().

Note: the Win32 FindFirstFileW() function starts by listing the files which match a given filter in a target directory but this doesn’t make any sense for an Object Directory. To put it simple, you can dir C:\Windows but you cannot dir "\RPC Control".

This first problem is quite simple to address though. Instead of creating a mountpoint to an Object Directory immediately, we can first create a mountpoint to an actual directory, containing some bait files.

First, we would have to create a temporary workspace directory such as follows:

C:\workspace
|__ file1.xml 
|__ file2.xml

Then, we can create the mountpoint:

C:\Users\lab-user\AppData\Local\Packages\[…]\Tips -> C:\workspace

Doing so, FindFirstFileW() would succeed and return file1.xml. In addition, if we set an OpLock on this file we can partially control the execution flow of the service because the remote procedure would be paused whenever it tries to access it.

When the OpLock is triggered, we can switch the mountpoint to an Object Directory. This is possible because the QueryDirectory operation already occurred and is done only once at the beginning of the FindFirstFileW() call.

C:\Users\lab-user\AppData\Local\Packages\[…]\Tips -> \RPC Control
\RPC Control\file2.xml -> \??\C:\users\lab-admin\desktop\secret.txt

Note: at this point, we don’t have to create a symbolic link for file1.xml because the service already has a handle on this file.

Thus, when the service opens C:\Users\lab-user\AppData\[…]\Tips\file2.xml, it actually opens secret.txt and copies its content to C:\ProgramData\[…]\SoftLandingStage\file2.xml.

Conclusion: we can trick the service into reading a file we don’t own but, this leads us to the second problem. At the end of the process, C:\ProgramData\[…]\SoftLandingStage\file2.xml is deleted so we wouldn’t be able to read it anyway.

Solving The Final File Delete Problem

Since the target file is deleted at the end of the process, we must win a race against the service and get a copy of the file before this happens. To do so we have two options. The first one would be bruteforce. We could implement the strategy described in the previous part and then monitor the target directory C:\ProgramData\[…]\SoftLandingStage in a loop in order to get a copy of the file as soon as NT AUTHORITY\SYSTEM has finished writing the new XML file.

But, bruteforce is always the option of last resort. Here, we have a second option which is way more reliable but we have to rethink the strategy from the beginning.

Instead of creating two files in our initial temporary workspace directory, we will create three files.

C:\workspace
|__ file1.xml
|__ file2.xml  
|__ file3.xml

The next steps will be the same but, when the OpLock on file1.xml is triggered, we will perform two extra actions.

We will first switch the mountpoint and create two pseudo symbolic links. We must make sure that the file3.xml link points to the actual file3.xml file.

C:\Users\lab-user\AppData\Local\Packages\[…]\Tips -> \RPC Control
\RPC Control\file2.xml -> \??\C:\users\lab-admin\desktop\secret.txt
\RPC Control\file3.xml -> \??\C:\workspace\file3.xml

And, we set a new OpLock on file3.xml before releasing the first one.

Thanks to this trick, will are able to influence the service as follows:

  1. DiagTrack tries to read file1.xml and hits the first OpLock.
  2. At this point, we switch the mountpoint, create the two symlinks and set an OpLock on file3.xml.
  3. We release the first OpLock (file1.xml).
  4. DiagTrack copies file1.xml and file2.xml which points to secret.txt.
  5. DiagTrack tries to read file3.xml and hits the second OpLock.
  6. This is the crucial part. At this point, the remote procedure is paused so we can get a copy of C:\ProgramData\[…]\SoftLandingStage\file2.xml, which is itself a copy of secret.txt.
  7. We release the second OpLock (file3.xml).
  8. The remote procedure terminates and the three XML files are deleted.

Note: this trick works because the process performed by DiagTrack is done sequentially. Each file is copied one after each other and all newly created files are deleted at the very end.

This results in a reliable exploit which allows a normal user to get a copy of any file readable as NT AUTHORITY\SYSTEM. Here is a screenshot showing the PoC I developped.