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
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!
The vulnerability I found lied in the
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
1 2 3 4 5 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.
1 2 3 4 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.
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
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:
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.
CreateFile operations originated from a call to
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?!
Well… Wait a minute. We have two problems here.
FindFirstFileW()call on the
Tipsfolder would fail because the target of the mountpoint isn’t a “real” folder.
- The new
file1.xmlfile which is created in
C:\ProgramData\[…]\SoftLandingStageis 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).
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
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
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
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
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.
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
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:
- DiagTrack tries to read
file1.xmland hits the first OpLock.
- At this point, we switch the mountpoint, create the two symlinks and set an OpLock on
- We release the first OpLock (
- DiagTrack copies
file2.xmlwhich points to
- DiagTrack tries to read
file3.xmland hits the second OpLock.
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
- We release the second OpLock (
- 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.
CVE-2020-0863 - Connected User Experiences and Telemetry Service Information Disclosure Vulnerability
My PoC for CVE-2020-0863
Symbolic Link Testing Tools - James Forshaw