Normal view

There are new articles available, click to refresh the page.
Before yesterdayitm4n

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

By: itm4n
18 March 2020 at 00:00

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.

Links & Resources

CVE-2020-0787 - Windows BITS - An EoP Bug Hidden in an Undocumented RPC Function

By: itm4n
11 March 2020 at 00:00

This post is about an arbitrary file move vulnerability I found in the Background Intelligent Transfer Service. This is yet another example of a privileged file operation abuse in Windows 10. There is nothing really new but the bug itself is quite interesting because it was hidden in an undocumented function. Therefore, I will explain how I found it and I will also share some insights about the reverse engineering process I went through in order to identify the logic flaw. I hope you’ll enjoy reading it as much as I enjoyed writing it.

TL;DR

If you don’t know this Windows feature, here is a quote from Microsoft documentation (link).

Background Intelligent Transfer Service (BITS) is used by programmers and system administrators to download files from or upload files to HTTP web servers and SMB file shares. BITS will take the cost of the transfer into consideration, as well as the network usage so that the user’s foreground work has as little impact as possible. BITS also handles network interuptions, pausing and automatically resuming transfers, even after a reboot.

This service exposes several COM objects, which are different iterations of the “Control Class” and there is also a “Legacy Control Class”. The latter can be used to get a pointer to the legacy IBackgroundCopyGroup interface, which has two undocumented methods: QueryNewJobInterface() and SetNotificationPointer().

If a user invokes the CreateJob() method of the IBackgroundCopyGroup interface (i.e. the legacy one), he/she will get a pointer to the old IBackgroundCopyJob1 interface. On the other hand, if he/she invokes the QueryNewJobInterface() method of this same interface, he/she will get a pointer to the new IBackgroundCopyJob interface.

The issue is that this call was handled by the service without impersonation. It means that users get a pointer to an IBackgroundCopyJob interface in the context of NT AUTHORITY\SYSTEM. Impersonation is implemented in the other methods though so the impact is limited but there are still some side effects.

When a job is created and a file is added to the queue, a temporary file is created. Once the service has finished writing to the file, it is renamed with the filename specified by the user thanks to a call to MoveFileEx(). The problem is that, when using the interface pointer returned by QueryNewJobInterface(), this last operation is done without impersonation.

A normal user can therefore leverage this behavior to move an arbitrary file to a restricted location using mountpoints, oplocks and symbolic links.

How do the BITS COM Classes work?

The Background Intelligent Transfer Service exposes several COM objects, which can be easily listed using OleViewDotNet (a big thanks to James Forshaw once again).

Here, we will focus on the Background Intelligent Transfer (BIT) Control Class 1.0 and the Legacy BIT Control Class and their main interfaces, which are respectively IBackgroundCopyManager and IBackgroundCopyMgr.

The “new” BIT Control Class

The BIT Control Class 1.0 works as follows:

  1. You must create an instance of the BIT Control Class (CLSID: 4991D34B-80A1-4291-83B6-3328366B9097) and request a pointer to the IBackgroundCopyManager interface with CoCreateInstance().
  2. Then, you can create a “job” with a call to IBackgroundCopyManager::CreateJob() to get a pointer to the IBackgroundCopyJob interface.
  3. Then, you can add file(s) to the job with a call to IBackgroundCopyJob::AddFile(). This takes two parameters: a URL and a local file path. The URL can also be a UNC path.
  4. Finally, since the job is created in a SUSPENDED state, you have to call IBackgroundCopyJob::Resume() and IBackgroundCopyJob::Complete() when the state of the job is TRANSFERRED.
CoCreateInstance(CLSID_4991D34B-80A1-4291-83B6-3328366B9097)   -> IBackgroundCopyManager*
|__ IBackgroundCopyManager::CreateJob()                        -> IBackgroundCopyJob*
    |__ IBackgroundCopyJob::AddFile(URL, LOCAL_FILE) 
    |__ IBackgroundCopyJob::Resume() 
    |__ IBackgroundCopyJob::Complete()  

Although the BIT service runs as NT AUTHORITY\SYSTEM, all these operations are performed while impersonating the RPC client so no elevation of privilege is possible here.

The Legacy Control Class

The Legacy Control Class works a bit differently. An extra step is required at the beginning of the process.

  1. You must create an instance of the Legacy BIT Control Class (CLSID: 69AD4AEE-51BE-439B-A92C-86AE490E8B30) and request a pointer to the IBackgroundCopyQMgr interface with CoCreateInstance().
  2. Then, you can create a “group” with a call to IBackgroundCopyQMgr::CreateGroup() to get a pointer to the IBackgroundCopyGroup interface.
  3. Then, you can create a “job” with a call to IBackgroundCopyGroup::CreateJob() to get a pointer to the IBackgroundCopyJob1 interface.
  4. Then, you can add file(s) to the “job” with a call to IBackgroundCopyJob1::AddFiles(), which takes a FILESETINFO structure as a parameter.
  5. Finally, since the job is created in a SUSPENDED state, you have to call IBackgroundCopyJob1::Resume() and IBackgroundCopyJob1::Complete() when the state of the job is TRANSFERRED.
CoCreateInstance(CLSID_69AD4AEE-51BE-439B-A92C-86AE490E8B30)   -> IBackgroundCopyQMgr*
|__ IBackgroundCopyQMgr::CreateGroup()                         -> IBackgroundCopyGroup*
    |__ IBackgroundCopyGroup::CreateJob()                      -> IBackgroundCopyJob1*
        |__ IBackgroundCopyJob1::AddFiles(FILESETINFO)
        |__ IBackgroundCopyJob1::Resume()
        |__ IBackgroundCopyJob1::Complete()

Once again, although the BIT service runs as NT AUTHORITY\SYSTEM, all these operations are performed while impersonating the RPC client so no elevation of privilege is possible here either.

The use of these two COM classes and their interfaces is well documented on MSDN here and here. However, while trying to understand how the IBackgroundCopyGroup interface worked, I noticed some differences between the methods listed on MSDN and its actual Proxy definition.

The documentation of the IBackgroundCopyGroup interface is available here. According to this resource, it has 13 methods. Though, when viewing the proxy definition of this interface with OleViewDotNet, we can see that it actually has 15 methods.

Proc3 to Proc15 match the methods listed in the documentation but Proc16 and Proc17 are not there.

Thanks to the documentation, we know that the corresponding header file is Qmgr.h. If we open this file, we should get an accurate list of all the methods that are available on this interface.

Indeed, we can see the two undocumented methods: QueryNewJobInterface() and SetNotificationPointer().

An Undocumented Method: “QueryNewJobInterface()”

Thanks to OleViewDotNet, we know that the IBackgroundCopyQMgr interface is implemented in qmgr.dll so, we can open it in IDA and see if we can find more information about the IBackgroundCopyGroup interface and the two undocumented methods I mentionned.

The QueryNewJobInterface() method requires 1 parameter: an interface identifier (REFIID iid) and returns a pointer to an interface (IUnknown **pUnk). The prototype of the function is as follows:

virtual HRESULT QueryNewJobInterface(REFIID iid, IUnknown **pUnk);

First, the input GUID (Interface ID) is compared against a hardcoded value (1): 37668d37-507e-4160-9316-26306d150b12. If it doesn’t match, then the function returns the error code 0x80004001 (2) – “Not implemented”. Otherwise, it calls the GetJobExternal() function from the CJob Class (3).

The hardcoded GUID value (37668d37-507e-4160-9316-26306d150b12) is interesting. It’s the value of IID_IBackgroundCopyJob. We can find it in the Bits.h header file.

The Arbitrary File Move Vulnerability

Before going any further into the reverse engineering process, we could make an educated guess based on the few information that was collected.

  • The name of the undocumented method is QueryNewJobInterface().
  • It’s exposed by the IBackgroundCopyGroup interface of the Legacy BIT Control Class.
  • The GUID of the “new” IBackgroundCopyJob interface is involved.

Therefore, we may assume that the purpose of this function is to get an interface pointer to the “new” IBackgroundCopyJob interface from the Legacy Control Class.

In order to verify this assumption, I created an application that does the following:

  1. It creates an instance of the Legacy Control Class and gets a pointer to the legacy IBackgroundCopyQMgr interface.
  2. It creates a new group with a call to IBackgroundCopyQMgr::CreateGroup() to get a pointer to the IBackgroundCopyGroup interface.
  3. It creates a new job with a call to IBackgroundCopyGroup::CreateJob() to get a pointer to the IBackgroundCopyJob1 interface.
  4. It adds a file to the job with a call to IBackgroundCopyJob1::AddFiles().
  5. And here is the crucial part, it calls the IBackgroundCopyGroup::QueryNewJobInterface() method and gets a pointer to an unknown interface but we will assume that it’s an IBackgroundCopyJob interface.
  6. It finally resumes and complete the job by calling Resume() and Complete() on the IBackgroundCopyJob interface instead of the IBackgroundCopyJob1 interface.

In this application, the target URL is \\127.0.0.1\C$\Windows\System32\drivers\etc\hosts (we don’t want to depend on a network access) and the local file is C:\Temp\test.txt.

Then, I analyzed the behavior of the BIT service with Procmon.

First, we can see that the service creates a TMP file in the target directory and tries to open the local file that was given as an argument, while impersonating the current user.

Then, once we call the Resume() function, the service starts reading the target file \\127.0.0.1\C$\Windows\System32\drivers\etc\hosts and writes its content to the TMP file C:\Temp\BITF046.tmp, still while impersonating the current user as expected.

Finally, the TMP file is renamed as test.txt with a call to MoveFileEx() and, here is the flaw! While doing so, the current user isn’t impersonated anymore, meaning that the file move operation is done in the context of NT AUTHORITY\SYSTEM.

The following screenshot confirms that the SetRenameInformationFile call originated from the Win32 MoveFileEx() function.

This arbitrary file move as SYSTEM results in an Local Privilege Escalation. By moving a specifically crafted DLL to the System32 folder, a regular user may execute arbitrary code in the context of NT AUTHORITY\SYSTEM as we will see in the final “Exploit” part.

Finding the Flaw

Before trying to find the flaw in the QueryNewJobInterface() function itself, I first tried to understand how the “standard” CreateJob() method worked.

The CreateJob() method of the IBackgroundCopyGroup interface is implemented in the COldGroupInterface class on server side.

It’s not obvious here because of CFG (Control Flow Guard) but this function calls the CreateJobInternal() method of the same class if I’m not mistaken.

This function starts by invoking the ValidateAccess() method of the CLockedJobWritePointer class, which calls the CheckClientAccess() method of the CJob class.

The CheckClientAccess() method is where the token of the user is checked and is applied to the current thread for impersonation.

Eventually, the execution flow goes back to the CreateJobInternal() method, which calls the GetOldJobExternal() method of the CJob class and returns a pointer to the IBackgroundCopyJob1 interface to the client

The calls can be summarized as follows:

(CLIENT) IBackgroundCopyGroup::CreateJob()
   |
   V
(SERVER) COldGroupInterface::CreateJob()
         |__ COldGroupInterface::CreateJobInternal()
             |__ CLockedJobWritePointer::ValidateAccess()
             |   |__ CJob::CheckClientAccess() // Client impersonation
             |__ CJob::GetOldJobExternal() // IBackgroundCopyJob1* returned

Now that we know how the CreateJob() method works overall, we can go back to the reverse engineering of the QueryNewJobInterface() method.

We already saw that if the supplied GUID matches IID_IBackgroundCopyJob, the following piece of code is executed.

That’s where the new interface pointer is queried and returned to the client with an immediate call to CJob::GetExternalJob(). Therefore, it can simply be summarized as follows:

(CLIENT) IBackgroundCopyGroup::QueryNewJobInterface()
   |
   V
(SERVER) COldGroupInterface::QueryNewJobInterface()
         |__ CJob::GetJobExternal() // IBackgroundCopyJob* returned

We can see a part of the issue now. It seems that, when requesting a pointer to a new IBackgroundCopyJob interface from IBackgroundCopyGroup with a call to the QueryNewJobInterface() method, the client isn’t impersonated. This means that the client gets a pointer to an interface which exists within the context of NT AUTHORITY\SYSTEM (if that makes any sense).

The problem isn’t that simple though. Indeed, I noticed that the file move operation occurred after the call to IBackgroundCopyJob::Resume() and before the call to IBackgroundCopyJob::Complete().

Here is a very simplified call trace when invoking IBackgroundCopyJob::Resume():

(CLIENT) IBackgroundCopyJob::Resume()
   |
   V
(SERVER) CJobExternal::Resume()
         |__ CJobExternal::ResumeInternal()
             |__ ...
             |__ CJob::CheckClientAccess() // Client impersonation
             |__ CJob::Resume()
             |__ ...

Here is a very simplified call trace when invoking IBackgroundCopyJob::Complete():

(CLIENT) IBackgroundCopyJob::Complete()
   |
   V
(SERVER) CJobExternal::Complete()
         |__ CJobExternal::CompleteInternal()
             |__ ...
             |__ CJob::CheckClientAccess() // Client impersonation
             |__ CJob::Complete()
             |__ ...

In both cases, the client is impersonated. This means that the job wasn’t completed by the client. It was completed by the service itself, probably because there was no other file in the queue.

So, when a IBackgroundCopyJob interface pointer is received from a call to IBackgroundCopyGroup::QueryNewJobInterface() and the job is completed by the service rather than the RPC client, the final CFile::MoveTempFile() call is done without impersonation. I was not able to spot the exact location of the logic flaw but I think that adding the CJob::CheckClientAccess() check in COldGroupInterface::QueryNewJobInterface() would probably solve the issue.

Here is a simplified graph showing the functions that lead to a MoveFileEx() call in the context of a CJob object.

How to Exploit this Vulnerability?

The exploit strategy is pretty straightforward. The idea is to give the service a path to a folder that will initially be used as a junction to another “physical” directory. We create a new job with a local file to “download” and set an Oplock on the TMP file. After resuming the job, the service will start writing to the TMP file while impersonating the RPC client and will hit the Oplock. All we need to do then is to switch the mountpoint to an Object Directory and create two symbolic links. The TMP file will point to any file we own and the “local” file will point to a new DLL file in the System32 folder. Finally, after releasing the Oplock, the service will continue writing to the original TMP file but it will perform the final move operation through our two symbolic links.

1) Prepare a workspace

The idea is to create a directory with the following structure:

<DIR> C:\workspace
|__ <DIR> bait
|__ <DIR> mountpoint
|__ FakeDll.dll

The purpose of the mountpoint directory is to switch from a junction to the bait directory to a junction to the RPC Control Object Directory. FakeDll.dll is the file we want to move to a restricted location such as C:\Windows\System32\.

2) Create a mountpoint

We want to create a mountpoint from C:\workspace\mountpoint to C:\workspace\bait.

3) Create a new job

We’ll use the interfaces provided by the Legacy Control Class to create a new job with the following parameters.

Target URL: \\127.0.0.1\C$\Windows\System32\drivers\etc\hosts
Local file: C:\workspace\mountpoint\test.txt

Because of the junction that was previously created, the real path of the local file will be C:\workspace\bait\test.txt.

4) Find the TMP file and set an Oplock

When adding a file to the job queue, the service immediately creates a TMP file. Since it has a “random” name, we have to list the content of the bait directory to find it. Here, we should find a name like BIT1337.tmp. Once we have the name, we can set an Oplock on the file.

5) Resume the job and wait for the Oplock

As mentioned earlier, as soon as the job is resumed, the service will open the TMP file for writing and will trigger the Oplock. This technique allows us to pause the operation and therefore easily win the race.  

6) Switch the mountpoint

Before this step:

TMP file   = C:\workspace\mountpoint\BIT1337.tmp -> C:\workspace\bait\BIT1337.tmp
Local file = C:\workspace\mountpoint\test.txt -> C:\workspace\bait\test.txt

We switch the mountpoint and create the symbolic links:

C:\workspace\mountpoint -> \RPC Control
Symlink #1: \RPC Control\BIT1337.tmp -> C:\workspace\FakeDll.dll
Symlink #2: \RPC Control\test.txt -> C:\Windows\System32\FakeDll.dll

After this step:

TMP file   = C:\workspace\mountpoint\BIT1337.tmp -> C:\workspace\FakeDll.dll
Local file = C:\workspace\mountpoint\test.txt -> C:\Windows\System32\FakeDll.dll

7) Release the Oplock and complete the job

After releasing the Oplock, the CreateFile operation on the original TMP file will return and the service will start writing to C:\workspace\bait\BIT1337.tmp. After that the final MoveFileEx() call will be redirected because of the symbolic links. Therefore, our DLL will be moved to the System32 folder.

Because it’s a move operation, the properties of the file are preserved. This means that the file is still owned by the current user so it can be modified afterwards even if it’s in a restricted location.

8) (Exploit) Code execution as System

To get code execution as System, I used the arbitrary file move vulnerability to create the WindowsCoreDeviceInfo.dll file in the System32 folder. Then, I leveraged the Update Session Orchestrator service to load the DLL as System.

Demo

Links & Resources

CVE-2020-0668 - A Trivial Privilege Escalation Bug in Windows Service Tracing

By: itm4n
14 February 2020 at 00:00

In this post, I’ll discuss an arbitrary file move vulnerability I found in Windows Service Tracing. From my testing, it affected all versions of Windows from Vista to 10 but it’s probably even older because this feature was already present in XP.

TL;DR

Service Tracing is an old feature that I could trace back to Windows XP but it probably already existed in previous versions of the OS. It aims at providing some basic debug information about running services and modules. It can be configured by any local user, simply by editing some registry keys and values under HKLM\SOFTWARE\Microsoft\Tracing.

A service or module is associated to a registry key. Each key contains 6 values (i.e. settings). The 3 values we will focus on are: EnableFileTracing (enable / disable the “tracing”), FileDirectory (set the location of the output log file) and MaxFileSize (set the maximum file size of the log file).

Once EnableFileTracing is enabled, the target service will start writing to its log file in the directory of your choice. As soon as the size of the output file exceeds MaxFileSize, it will be moved (the .LOG extension is replaced by .OLD) and a new log file will be created.

Thanks to James Forshaw’s symbolic link testing tools, the exploit is quite simple. All you need to do is set the target directory as a mountpoint to the \RPC Control object directory and then create two symbolic links:

  • A symbolic link from MODULE.LOG to a file you own (its size must be greater than MaxFileSize).
  • A symbolic link from MODULE.OLD to any file on the file system (e.g.: C:\Windows\System32\WindowsCoreDeviceInfo.dll).

Finally, the file move can be triggered by targeting a service running as NT AUTHORITY\SYSTEM and, the Update Session Orchestrator service can then be leveraged to get arbitrary code execution.

The Tracing Feature for Services

As briefly mentioned before, the Service Tracing feature can be configured by any local user, simply by editing some registry keys and values under HKLM\SOFTWARE\Microsoft\Tracing.

Using AccessChk from the Windows Sysinternals tools suite, we can see that regular Users have Read/Write permissions on almost all the sub-keys.

For the rest of this article, I’ll use the RASTAPI module as an example since it’s the one I leveraged in my exploit. This module is used by the IKEEXT service. Therefore, log events can be easily triggered by initiating dummy VPN connections. The following screenshot shows the default content of the registry key. The exact same values are configured for the other services and modules.

From a local attacker’s standpoint, here are the most interesting values:

Name Possible values Description
EnableFileTracing 0 - 1 Start / Stop writing to the log file.
FileDirectory A String The absolute path of a directory.
MaxFileSize 0x00000000 - 0xffffffff The maximum size of the output log file.

By setting these values, we can:

  • Force a specific service or module to start or stop writing debug information to a log file by setting EnableFileTracing to either 0 or 1.
  • Specify the location of the log file by setting FileDirectory.
  • Specify the maximum size of the output file by setting MaxFileSize.

The only caveat is that we cannot choose the name of the output file since it’s based on the name of the debugged service or module. This issue can be easily addressed using symbolic links though.

The Arbitrary File Move Vulnerability

With all the previous elements of context in mind, the vulnerability can be easily explained.

Case #1: MaxFileSize - Default value

For this first test case, I simply set C:\LOGS as the output directory and enabled the File Tracing.

Now, if we want the target service to start writing to this file, we must generate some events. A very simple way to do so is to initiate a dummy VPN connection using the rasdial command and a PBK file.

It worked! The log file was written by NT AUTHORITY\SYSTEM. Its size is now around 24KB.

Case #2: MaxFileSize - Custom value

In the previous test, we saw that the final size of the output log file was around 24KB. Therefore, this time, we will set MaxFileSize to 0x4000 (16,384 bytes) and restart the test.

The events captured by “Process Monitor” can be summarized as follows:

  1. Basic information about the log file is fetched by the service. We can see that the EndOfFile is at offset 23,906, which is the size of the file at this moment. The problem is that we specified a max file size of 16,384 bytes so, the system will consider that there is no more free space.
  2. SetRenameInformationFile is called with FileName=C:\LOGS\RASTAPI.OLD. In other words, since the existing file is considered as full, it is moved from C:\LOGS\RASTAPI.LOG to C:\LOGS\RASTAPI.OLD.
  3. The service creates a new C:\LOGS\RASTAPI.LOG file and starts writing to it.

The “Move” operation is performed as NT AUTHORITY\SYSTEM. Therefore, it can be leveraged to move a user-owned file to any location on the file system, such as C:\Windows\System32\.

The Exploit

The exploit is simple and can be summarized as follows:

  1. Create (or copy) a file with a size greater than 0x8000 (32,768) bytes.
  2. Create a new directory (C:\EXPLOIT\mountpoint\ for example) and set it as a mountpoint to \RPC Control.
  3. Create the following symbolic links:
    \RPC Control\RASTAPI.LOG -> \??\C:\EXPLOIT\FakeDll.dll (owner = current user)
    \RPC Control\RASTAPI.OLD -> \??\C:\Windows\System32\WindowsCoreDeviceInfo.dll
    
  4. Configure the following values in the registry:
    FileDirectory = C:\EXPLOIT\mountpoint
    MaxFileSize = 0x8000 (32,768‬ bytes)
    EnableFileTracing = 1
    
  5. Trigger RASTAPI related events using the RasDial function from the Windows API.
  6. Trigger the Update Session Orchestrator service to load the DLL in the context of NT AUTHORITY\SYSTEM.

Demo

Links & Resources

CDPSvc DLL Hijacking - From LOCAL SERVICE to SYSTEM

By: itm4n
11 December 2019 at 00:00

A DLL hijacking “vulnerability” in the CDPSvc service was reported to Microsoft at least two times this year. As per their policy though, DLL planting issues that fall into the category of PATH directories DLL planting are treated as won’t fix , which means that it won’t be addressed (at least in the near future). This case is very similar to the IKEEXT one in Windows Vista/7/8. The big difference is that CDPSvc runs as LOCAL SERVICE instead of SYSTEM so getting higher privileges requires an extra step.

CDPSvc DLL Hijacking

Before we begin, I’ll assume you know what DLL hijacking is. It’s probably one of the oldest and most basic privilege escalation techniques in Windows. Besides, the case of the CDPSvc service was already well explained by Nafiez in this article: (MSRC Case 54347) Microsoft Windows Service Host (svchost) - Elevation of Privilege.

Long story short, the Connected Devices Platform Service (or CDPSvc) is a service which runs as NT AUTHORITY\LOCAL SERVICE and tries to load the missing cdpsgshims.dll DLL on startup with a call to LoadLibrary(), without specifying its absolute path.

Therfore, following the DLL search order of Windows, it will first try to load it from the “system” folders and then go through the list of directories which are stored in the PATH environment variable. So, if one of these folders is configured with weak permissions, you could plant a “malicious” version of the DLL and thus execute arbitrary code in the context of NT AUTHORITY\LOCAL SERVICE upon reboot.

Note: the last PATH entry varies depending on the current user profile. This means that you will always see this folder as writable if you look at your own PATH variable in Windows 10. If you want to see the PATH variable of the System, you can check the registry with the following command: reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /V Path.

That’s it for the boring stuff. :sleeping: Now let’s talk about some Windows internals and lesser known exploitation techniques. :smiley:

A Word (Or Maybe Two…) About Tokens And Impersonation

In my previous article, I discussed the specific case of service accounts running without impersonation privileges. As it turns out, it’s not the case of CDPSvc so we will be able to take advantage of this. However, I realize that I didn’t say much about the implications of each impersonation privilege. It’s not overly complicated but I know that it’s easy to overlook this kind of things because there are so many other things to learn.

Since I worked quite a bit on the inner working of tools such as RottenPotato or JuicyPotato, I’d like to share what I learned in an hopefully clear and concise way. If you’re already familiar with these concepts, you may skip to the next part.

Token Types

First things first. Let’s talk about tokens. There are 2 types of tokens: Primary tokens and Impersonation tokens. A Primary token represents the security information of a process whereas an Impersonation token represents the security context of another user in a thread.

  • Primary token: one per process.
  • Impersonation token: one per thread which impersonates another user.

Note: an Impersonation token can be converted to a Primary token with a call to DuplicateTokenEx().

Impersonation Levels

An Impersonation token comes with an impersonation level: Anonymous, Identification, Impersonation or Delegation. You can use a token for impersonation only if it has an Impersonation or Delegation level associated with it.

  • Anonymous: The server cannot impersonate or identify the client.
  • Identification: The server can get the identity and privileges of the client, but cannot impersonate the client.
  • Impersonation: The server can impersonate the client’s security context on the local system.
  • Delegation: The server can impersonate the client’s security context on remote systems.

Impersonation

Regarding the impersonation methods, there are 3 different ways to create a process as a different user in Windows as I far as I know.

This function doesn’t require any specific privilege. Any user can call this function. However you must know the password of the target account. That’s typically the method used by runas.

This function requires the SeImpersonatePrivilege privilege, which is enabled by default (for the LOCAL SERVICE account). As an input, it requires a Primary token.

This function requires the SeAssignPrimaryTokenPrivilege and SeIncreaseQuotaPrivilege privileges, which are both disabled by default (for the LOCAL SERVICE account) but only SeAssignPrimaryTokenPrivilege really needs to be enabled. SeIncreaseQuotaPrivilege will be transperently enabled/disabled during the API call. As an input, it also requires a Primary token.

API function Privilege(s) required Input
CreateProcessWithLogon() None Domain / Username / Password
CreateProcessWithToken() SeImpersonatePrivilege Primary token
CreateProcessAsUser() SeAssignPrimaryTokenPrivilege AND SeIncreaseQuotaPrivilege Primary token

The CDPSvc Case

As you can see on the below screenshot, the process in which CDPSvc runs has the three privileges I’ve just talked about so it can impersonate any local user with CreateProcessWithToken() or CreateProcessAsUser() provided that you have a valid token for this user.

As a conclusion, we have the appropriate privileges to impersonate NT AUTHORITY\SYSTEM. The second thing we need is a valid token but how can we get one of them? :thinking:

Bringing Back An Old Technique From The Dead: Token Kidnapping

In the old days of Windows, all services ran as SYSTEM, which means that when one of them was compromised all the other services and the host itself were also compromised. Therefore Microsoft added some segregation and introduced two other accounts with less privileges: NETWORK SERVICE and LOCAL SERVICE.

Unfortunately, this wasn’t enough. Indeed, if a service running as LOCAL SERVICE was compromised for example, it could execute code in any other service running as the same user account, access its memory space and extract privileged impersonation tokens: this is the technique called Token Kidnapping, which was presented by Cesar Cerrudo at several conferences in 2008.

To counter this attack, Microsoft had to redesign the security model of the services. The main feature they implemented was Service Isolation. The idea is that each service runs with a dedicated Security Identifier (SID). If you consider a service A with SID_A and a service B with SID_B, service A won’t be able to access the ressources of service B anymore because the two processes are now running with two different identities (although it’s the same account).

Here is a quote from MS Blog, Token Kidnapping in Windows.

The first issue to address is to make sure that two services running with the same identity not be able to access each other’s tokens freely. This concern has been mostly addressed with service hardening done in Windows Vista and above. There are some minor changes that would need to be done to strengthen service hardening to close some gaps identified during our investigation of this issue.

OK so, basically, you’re telling me that Token Kidnapping is now useless because of Service Isolation. What’s the point in talking about that then? :unamused:

Well, the fun fact about CDPSvc is that it runs within a shared process so Service Isolation is almost pointless here since it can access the data of almost a dozen services. CDPSvc runs within a shared process by default only if the machine has less than 3.5GB of RAM (See Changes to Service Host grouping in Windows 10). The question is, among all these services, is there at least one that leaks interesting token handles?

Let’s take a look at the properties of the process once again. Process Hacker provides a really nice feature. it can list all the Handles that are open in a given process.

It looks like the process currently has 5 open Handles to Impersonation tokens which belong to the SYSTEM account. How convenient! :sunglasses:

Fine! How do we proceed?! :grin:

A Handle is a reference to an object (such as a Process, a Thread, a File or a Token for example) but it doesn’t hold the address of the object directly. It’s just an entry in an internally maintained table where the “actual” address is stored. So, it can be seen as an ID, which can be easily bruteforced. That’s the idea behind the Token Kidnapping technique.

Token Kidnapping consists in opening another process and then bruteforcing the open Handles by duplicating them inside the current process. For each valid Handle, we check whether it’s a Handle to a Token, if it’s not the case, we go to the next one.

If we find a valid Token Handle, we must check the following:

  • The corresponding account is SYSTEM?
  • Is it an Impersonation token?
  • The Impersonation Level of the token is at least Impersonation?

Of course, because of Service Isolation, this technique can’t be applied to services running in different processes. However, if you are able to “inject” a DLL into one of these services, you can then access the memory space of the corresponding process without any restrictions. So, you can apply the same bruteforce technique from within the current process. And, once you’ve found a proper impersonation token, you can duplicate it and use the Windows API to create a process as NT AUTHORITY\SYSTEM. That’s as simple as that.

No conclusion for this post. I just hope that you learned a few things. Here is the link to my PoC.

Demo

Links & Resources

❌
❌