CVE-2020-0787 - Windows BITS - An EoP Bug Hidden in an Undocumented RPC Function
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:
- You must create an instance of the BIT Control Class (CLSID:
4991D34B-80A1-4291-83B6-3328366B9097
) and request a pointer to theIBackgroundCopyManager
interface withCoCreateInstance()
. - Then, you can create a “job” with a call to
IBackgroundCopyManager::CreateJob()
to get a pointer to theIBackgroundCopyJob
interface. - 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. - Finally, since the job is created in a
SUSPENDED
state, you have to callIBackgroundCopyJob::Resume()
andIBackgroundCopyJob::Complete()
when the state of the job isTRANSFERRED
.
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.
- You must create an instance of the Legacy BIT Control Class (CLSID:
69AD4AEE-51BE-439B-A92C-86AE490E8B30
) and request a pointer to theIBackgroundCopyQMgr
interface withCoCreateInstance()
. - Then, you can create a “group” with a call to
IBackgroundCopyQMgr::CreateGroup()
to get a pointer to theIBackgroundCopyGroup
interface. - Then, you can create a “job” with a call to
IBackgroundCopyGroup::CreateJob()
to get a pointer to theIBackgroundCopyJob1
interface. - Then, you can add file(s) to the “job” with a call to
IBackgroundCopyJob1::AddFiles()
, which takes aFILESETINFO
structure as a parameter. - Finally, since the job is created in a
SUSPENDED
state, you have to callIBackgroundCopyJob1::Resume()
andIBackgroundCopyJob1::Complete()
when the state of the job isTRANSFERRED
.
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:
1
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:
- It creates an instance of the Legacy Control Class and gets a pointer to the legacy
IBackgroundCopyQMgr
interface. - It creates a new group with a call to
IBackgroundCopyQMgr::CreateGroup()
to get a pointer to theIBackgroundCopyGroup
interface. - It creates a new job with a call to
IBackgroundCopyGroup::CreateJob()
to get a pointer to theIBackgroundCopyJob1
interface. - It adds a file to the job with a call to
IBackgroundCopyJob1::AddFiles()
. - 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 anIBackgroundCopyJob
interface. - It finally resumes and complete the job by calling
Resume()
andComplete()
on theIBackgroundCopyJob
interface instead of theIBackgroundCopyJob1
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
MSRC - CVE-2020-0787 | Windows Background Intelligent Transfer Service Elevation of Privilege Vulnerability
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0787My PoC for CVE-2020-0787
https://github.com/itm4n/CVEs/tree/master/CVE-2020-0787/poc/OleViewDotNet - James Forshaw
https://github.com/tyranid/oleviewdotnetSymbolic Link Testing Tools - James Forshaw
https://github.com/googleprojectzero/symboliclink-testing-toolsUsoDllLoader
https://github.com/itm4n/UsoDllLoaderMSDN - IBackgroundCopyManager interface
https://docs.microsoft.com/en-us/windows/win32/api/bits/nn-bits-ibackgroundcopymanagerMSDN - IBackgroundCopyQMgr interface
https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopyqmgr