# [CVE-2020-0787 - Windows BITS - An EoP Bug Hidden in an Undocumented RPC Function](https://itm4n.github.io/cve-2020-0787-windows-bits-eop/)
March 11, 2020
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.
![](https://images.seebug.org/1584588262817-w331s)
## TL;DR
If you don’t know this Windows feature, here is a quote from Microsoft documentation ([link](https://docs.microsoft.com/en-us/windows/win32/bits/background-intelligent-transfer-service-portal)).
> *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](https://github.com/tyranid/oleviewdotnet) (a big thanks to [James Forshaw](https://twitter.com/tiraniddo) once again).
![](https://images.seebug.org/1584588281941-w331s)
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`.
![](https://images.seebug.org/1584588296214-w331s)
### 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`.
```txt
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`.
```txt
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](https://docs.microsoft.com/en-us/windows/win32/api/bits/nn-bits-ibackgroundcopymanager) and [here](https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopyqmgr). 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](https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopygroup). 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**.
![](https://images.seebug.org/1584588313705-w331s)
`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()`.
![](https://images.seebug.org/1584588335193-w331s)
## 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.
![](https://images.seebug.org/1584588355040-w331s)
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);
```
![](https://images.seebug.org/1584588369331-w331s)
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.
![](https://images.seebug.org/1584588387828-w331s)
## 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**.
![](https://images.seebug.org/1584588415475-w331s)
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.
![](https://images.seebug.org/1584588438471-w331s)
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**.
![](https://images.seebug.org/1584588459437-w331s)
The following screenshot confirms that the `SetRenameInformationFile` call originated from the Win32 `MoveFileEx()` function.
![](https://images.seebug.org/1584588483337-w331s)
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.
![](https://images.seebug.org/1584588497596-w331s)
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.
![](https://images.seebug.org/1584588512675-w331s)
This function starts by invoking the `ValidateAccess()` method of the `CLockedJobWritePointer` class, which calls the `CheckClientAccess()` method of the `CJob` class.
![](https://images.seebug.org/1584588524856-w331s)
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
![](https://images.seebug.org/1584588539626-w331s)
The calls can be summarized as follows:
```txt
(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.
![](https://images.seebug.org/1584588552227-w331s)
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:
```txt
(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()`:
```txt
(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()`:
```txt
(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.
![](https://images.seebug.org/1584588563868-w331s)
## 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:
```txt
<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.
```txt
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:
```txt
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:
```txt
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:
```txt
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
![](https://images.seebug.org/1584588575085-w331s)
## 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-0787>
- My PoC for CVE-2020-0787
<https://github.com/itm4n/BitsArbitraryFileMove>
- OleViewDotNet - James Forshaw
<https://github.com/tyranid/oleviewdotnet>
- Symbolic Link Testing Tools - James Forshaw
<https://github.com/googleprojectzero/symboliclink-testing-tools>
- UsoDllLoader
<https://github.com/itm4n/UsoDllLoader>
- MSDN - IBackgroundCopyManager interface
<https://docs.microsoft.com/en-us/windows/win32/api/bits/nn-bits-ibackgroundcopymanager>
- MSDN - IBackgroundCopyQMgr interface
<https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopyqmgr>
Unavailable Comments