Reading view

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

The strange case of “open-ssh” in Windows Server 2019

A few weeks ago I decided to install “open-ssh” on a Windows 2019 server for management purpose. The ssh server/client is based on the opensource project and MS implementation source code can be found here

Installing ssh is a very easy task, all you have to do is to install the “feature” via powershell:

ssh1

The first time you start the service, the necessary directories and files are created under the directory “c:\programdata\ssh

ssh3

 

ssh4

ssh2

A standard  “sshd_config” is created and this file is obviously readonly for  users:

ssh5

So a strange idea came in my mind: what if I created a special kind of malicious “sshd_config” file before the first launch of the open-ssh server?

As a standard user, with no special privileges, I am able to create the “ssh” directory  and write files…

And what should my “sshd_config” file contain? Well, easy: the possibility to login as an administrator with a predefined public/private key pair!

So let’s start again from the beginning…. sshd server has not yet been installed or launched for the first time.

First of all,  we need  to create a “special” sshd_config file, here the relevant parts:

StrictModes no
....
PubkeyAuthentication yes
....
Match Group administrators
     AuthorizedKeysFile c:\temp\authorized_keys
  1. StrictModes” set to “no” will bypass the owner/permissions strict checks on the “authorized_keys file”
  2. PubkeyAuthentication” set to yes will permit login via public/private keys
  3. Match Group administrators” will point to an “authorized”_keys” file generated by the user

3) is very interesting, we have the possibility to define a unique private/pubkey pair for authenticating the members of “administrators” groups..

Now we have to generate our public/private key. In this example, I will use the ssh utilities for Windows, but you can create them also on other systems (ex:Linux)

ssh1

 

Once done, we will copy the public key, in this example under c:\temp:

ssh2

Next step is creating the c:\programdata\ssh  directory and copy  the config file into it:

ssh4

At his point we have just to wait that “sshd” service will be launched, for testing we can do it on your own as an admin:

ssh5

Our config file has not been changed and we are still able to modify it!

Let’s see if it works. We are going to use OUR private/public key pair in order to login as administrator

sssh6

ssh7

 

Yes, it works! 🙂

Even if there are not so “common” preconditions, this installation/configuration  bug (which has nothing to do with the open-ssh software suite itself) could easily lead to EoP, don’t you agree?

So I informed MSRC about this, they opened a case and some days after they told me that this was already fixed with CVE-2020-757, probably as an “unintended” side effect…

Strange! My Windows 2019 server was fully patched including latest CVE’s. So I tested this on a Windows 10 machine, updated it with latest patches and actually after that I ran into an  empty “c:\programdata\ssh” folder  with correct permissions, even if open-ssh was not installed.

But why did this not happen on my Windows 2019 server?

I tested other servers as well, installed a new one from scratch and always same results, no empty c:\programdata\ssh directory!

I had a long debate about this with MSRC, basically  they were stating that they could not reproduce it and then magically, with March MS Tuesday patch, the directory was finally created with KB4538461 !

Really strange, but that’s it and given that now it’s fixed I decided to publish this post!

Stay safe, remember to keep “physical distancing” and not “social distancing” 🙂

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ssh7

💾

decoderblogblog

💾

ssh1

💾

ssh3

💾

ssh4

💾

ssh2

💾

ssh5

💾

ssh1

💾

ssh2

💾

ssh4

💾

ssh5

💾

sssh6

💾

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

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

DiagTrack RPC Interfaces

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

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

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

The “UtcApi_DownloadLatestSettings” procedure

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

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

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

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

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

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

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

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

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

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

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

I ran my test program again and observed the result.

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

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

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

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

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

The Arbitrary File Read Vulnerability

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

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

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

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

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

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

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

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

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

Solving The “FindFirstFileW()” Problem

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

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

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

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

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

Then, we can create the mountpoint:

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

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

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

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

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

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

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

Solving The Final File Delete Problem

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

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

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

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

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

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

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

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

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

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

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

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

Links & Resources

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:

  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

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

The strange RPC interface (MS, are you trolling me?)

On a dark and stormy night, I was playing with Forshaw’s fantastic NTOBJECTMANGER library which, among the millions of things, is able to “disassemble” RPC servers  and implement Local RPC calls  in .NET.

I was looking at “interesting”  RPC servers on my Windows 10 1909 machine when my attention was caught by the “XblGameSave.dll” library.

This Dll is used by the “Xbox Live Game Service“:

xblsrv2

What is the purpose of this service? Well, I never played with Xbox nor Xbox games on Windows, but MS states that:

This service syncs save data for Xbox Live save enabled games. If this service is stopped, game save data will not upload to or download from Xbox Live.”

The service runs under the SYSTEM user context and is set to manual-triggered startup:

xblsrv

In short,  XblGameSave can be started upon a remote procedure call event.

I immediately popped up a new powershell instance as a standard user, imported  the Ntobjectmanager library and took a look at the Dll:

xbl2

Looked promising! The Dll exported a Local RPC Call “svcScheduleTaskOperation” with an Interface ID: f6c98708-c7b8-4919-887c-2ce66e78b9a0 and running as SYSTEM , maybe I could abuse  this call to  schedule a task as a privileged user?

Side note: I was able to get all these detailed information because I also specified the relative .pdb symbol file located in c:\symbols.  You can download the symbols files with the symchk.exe tool available with the Windows SDK:

symchk.exe /v c:\windows\system32\xblagamesave.dll /s 

SRV*c:\symbols\*http://msdl.microsoft.com/download/symbols

 

In order to obtain more information about the exposed interface, I created a client instance:

xbl3

The mysterious svcScheduleTaskOperation()  wanted a string as input parameter.  Next step was connect to the RPC server:

xbl4

Cool! when connecting my client I was able to trigger and start the service, so I tried to invoke the RPC call:

xbl5

 

As you can imagine, now the problem was guessing which string the function was waiting for…

The return value -2147024809  was only telling me that the parameter  was “Incorrect”. Thanks, this was a great help 😦

I hate fuzzing and bruteforcing and must admit that I’m really a noob in this field, this clearly was not the right path for me.

At this point, decompiling the Dll was no more an option! I had also the symbol file, so the odds of getting something readable and understandable by  common humans like me were founded.

xblc1This was the pseudo-C code generated by IDA, and in short, as far as I understood (I’m not that good in reversing): if input parameter was not NULL (a2), the Windows API WindowsCreateString was called and the resulting HSTRING passed to the ScheduledTaskOperation() method belonging somehow the ConnectedStorage class.

connectedstor

I obviously googled for more information with the keyword “ConnectedStorage” but surprisingly all the resulting links pointing to MS site returned a 404 error… The only way was to retrieve cached pages: https://webcache.googleusercontent.com/search?q=cache:dEN5ets6TcYJ:https://docs.microsoft.com/en-us/gaming/xbox-live/storage-platform/connected-storage/connected-storage-technical-overview

 

It seemed that the “ConnectedStorage” class, implemented in this Dll, had the following purpose:  “Store saved games and app data on Xbox One”  (and probably Windows 10 “Xbox” games too?)

My goal was not to understand the deepest and mysterious mechanisms of these classes, so I jumped to the ScheduledTaskOperation() function:

void __fastcall ConnectedStorage::Service::ScheduledTaskOperation(LPCRITICAL_SECTION lpCriticalSection, const struct ConnectedStorage::SimpleHStringWrapper *a2)
{
  const struct ConnectedStorage::SimpleHStringWrapper *v2; // rdi
  LPCRITICAL_SECTION v3; // rbx
  unsigned int v4; // eax
  const unsigned __int16 *v5; // r8
  unsigned int v6; // eax
  const unsigned __int16 *v7; // r8
  unsigned int v8; // eax
  const unsigned __int16 *v9; // r8
  unsigned int v10; // eax
  const unsigned __int16 *v11; // r8
  unsigned int v12; // eax
  const unsigned __int16 *v13; // r8
  unsigned int v14; // eax
  const unsigned __int16 *v15; // r8
  unsigned int v16; // eax
  const unsigned __int16 *v17; // r8
  unsigned int v18; // eax
  const unsigned __int16 *v19; // r8
  unsigned int v20; // eax
  const unsigned __int16 *v21; // r8
  unsigned int v22; // eax
  const unsigned __int16 *v23; // r8
  const unsigned __int16 *v24; // r8
  int v25; // [rsp+20h] [rbp-40h]
  __int64 v26; // [rsp+28h] [rbp-38h]
  LPCRITICAL_SECTION v27; // [rsp+30h] [rbp-30h]
  __int64 v28; // [rsp+38h] [rbp-28h]
  __int128 v29; // [rsp+40h] [rbp-20h]
  int v30; // [rsp+50h] [rbp-10h]
  char v31; // [rsp+54h] [rbp-Ch]
  __int16 v32; // [rsp+55h] [rbp-Bh]
  char v33; // [rsp+57h] [rbp-9h]

  v2 = a2;
  v3 = lpCriticalSection;
  v27 = lpCriticalSection;
  v28 = 0i64;
  EnterCriticalSection(lpCriticalSection);
  v26 = 0i64;
  v4 = WindowsCreateString(L"standby", 7i64, &v26);
  if ( (v4 & 0x80000000) != 0 )
    ConnectedStorage::ReportErrorAndThrow(
      (ConnectedStorage *)v4,
      (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)",
      v5);
  v25 = 0;
  v6 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25);
  if ( (v6 & 0x80000000) != 0 )
    ConnectedStorage::ReportErrorAndThrow(
      (ConnectedStorage *)v6,
      (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)",
      v7);
  WindowsDeleteString(v26);
  if ( v25 )
  {
    v26 = 0i64;
    v8 = WindowsCreateString(L"maintenance", 11i64, &v26);
    if ( (v8 & 0x80000000) != 0 )
      ConnectedStorage::ReportErrorAndThrow(
        (ConnectedStorage *)v8,
        (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)",
        v9);
    v25 = 0;
    v10 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25);
    if ( (v10 & 0x80000000) != 0 )
      ConnectedStorage::ReportErrorAndThrow(
        (ConnectedStorage *)v10,
        (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)",
        v11);
    WindowsDeleteString(v26);
    if ( v25 )
    {
      v26 = 0i64;
      v12 = WindowsCreateString(L"testenter", 9i64, &v26);
      if ( (v12 & 0x80000000) != 0 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)v12,
          (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)",
          v13);
      v25 = 0;
      v14 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25);
      if ( (v14 & 0x80000000) != 0 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)v14,
          (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)",
          v15);
      WindowsDeleteString(v26);
      if ( !v25 )
      {
        v31 = 1;
LABEL_11:
        v30 = 4;
        _mm_storeu_si128((__m128i *)&v29, (__m128i)GUID_LOW_POWER_EPOCH);
        v33 = 0;
        v32 = 0;
        ConnectedStorage::Power::PowerChangeCallback(v3 + 4, 0i64, &v29);
        goto LABEL_12;
      }
      v26 = 0i64;
      v16 = WindowsCreateString(L"testexit", 8i64, &v26);
      if ( (v16 & 0x80000000) != 0 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)v16,
          (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)",
          v17);
      v25 = 0;
      v18 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25);
      if ( (v18 & 0x80000000) != 0 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)v18,
          (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)",
          v19);
      WindowsDeleteString(v26);
      if ( !v25 )
      {
        v31 = 0;
        goto LABEL_11;
      }
      v26 = 0i64;
      v20 = WindowsCreateString(L"logon", 5i64, &v26);
      if ( (v20 & 0x80000000) != 0 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)v20,
          (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)",
          v21);
      v25 = 0;
      v22 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25);
      if ( (v22 & 0x80000000) != 0 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)v22,
          (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)",
          v23);
      WindowsDeleteString(v26);
      if ( v25 )
        ConnectedStorage::ReportErrorAndThrow(
          (ConnectedStorage *)0x80070057i64,
          (const wchar_t *)L"Service::ScheduledTaskOperation called with an invalid operation type.",
          v24);
    }
  }
LABEL_12:
  LeaveCriticalSection(v3);
}


 

In short:

  • the expected input strings were “logon“, “standby“,”maintenance“, “testenter“, “testexit
  • logon“, “standby“,”maintenance” did nothing! (fake??)
  • testenter” and “testexit” called the PowerChangeCallback  method with a  parameter set to  GUID_LOW_POWER_EPOCH and a flag set  1 if  “testenter” and 0 if “testexit“. This GUID identifies  a “low power state” of the device.

The disassembled  output of the PowerChangeCallback was the following:

__int64 __fastcall ConnectedStorage::Power::PowerChangeCallback(LPCRITICAL_SECTION lpCriticalSection, __int64 a2, __int64 a3)
{
  LPCRITICAL_SECTION v3; // rdi
  int v4; // ebx
  HANDLE v5; // rcx
  __int64 v6; // rdx
  __int64 v7; // r8
  HANDLE v8; // rcx
  DWORD v10; // eax
  const unsigned __int16 *v11; // r8
  ConnectedStorage *v12; // rcx
  DWORD v13; // eax
  const unsigned __int16 *v14; // r8
  ConnectedStorage *v15; // rcx

  v3 = lpCriticalSection;
  v4 = *(_DWORD *)(a3 + 20);
  EnterCriticalSection(lpCriticalSection);
  v5 = v3[1].OwningThread;
  if ( v4 )
  {
    LOBYTE(v3[1].LockSemaphore) = 1;
    if ( !ResetEvent(v5) )
    {
      v13 = GetLastError();
      v15 = (ConnectedStorage *)((unsigned __int16)v13 | 0x80070000);
      if ( (signed int)v13 <= 0 )
        v15 = (ConnectedStorage *)v13;
      ConnectedStorage::ReportErrorAndThrow(v15, L"Event: ResetEvent failed", v14);
    }
    v8 = v3[2].OwningThread;
  }
  else
  {
    LOBYTE(v3[1].LockSemaphore) = 0;
    if ( !SetEvent(v5) )
    {
      v10 = GetLastError();
      v12 = (ConnectedStorage *)((unsigned __int16)v10 | 0x80070000);
      if ( (signed int)v10 <= 0 )
        v12 = (ConnectedStorage *)v10;
      ConnectedStorage::ReportErrorAndThrow(v12, L"Event: SetEvent failed", v11);
    }
    v8 = *(HANDLE *)&v3[3].LockCount;
  }
  if ( v8 )
    (*(void (__fastcall **)(HANDLE, __int64, __int64))(*(_QWORD *)v8 + 8i64))(v8, v6, v7);
  LeaveCriticalSection(v3);
  return 0i64;
}

This function was responsible for setting (“testenter“) and resetting (“testexit“) event objects in order to notify a waiting thread of the occurrence of the particular event ( I presume “low power change” in this case).

So back to us, what could I do with the original svcScheduleTaskOperation RPCcall ?

Probably nothing useful, maybe it has been exposed  only for testing purpose. Why did MS not implement the other functions like logon, maintenance, standby ?

And why did they call it svcScheduleTaskOperation ? Perhaps they will complete it in a future Windows release?

Mystery!

captxbl

As you can see, all legitimate commands returned 0  (success), that was a cold comfort 😦

My research sadly led to a dead end, but perhaps there are other forgotten or or leftover RPC interfaces to look for?

That’s all, for now 🙂

 

 

 

rpc

decoderblogblog

xblsrv2

xblsrv

xbl2

xbl3

xbl4

xbl5

xblc1

connectedstor

captxbl

Avira VPN Local Privilege Escalation via Insecure Update Location

Product Version: Avira VPN
Operating System tested on: Windows 10 1709 (x64)
Vulnerability: Avira VPN Service Local Privilege Escalation

Brief Description: When the Phantom VPN Service (Avira.VPNService.exe) starts, it checks to see if there are any updates available. The service executes the update from C:\ProgramData\Avira\VPN\Update, which is writable by a low privileged user. Additionally, the service implements checks to prevent exploitation that can be circumvented. This allows an attacker to plant a valid Avira executable along with a malicious DLL in “C:\ProgramData\Avira\VPN\Update” and cause the service to execute the update file. A DLL hijack will occur, resulting in code-execution as SYSTEM.

Vulnerability Explanation:
When the Phantom VPN Service (Avira.VPNService.exe) starts, one of the first things it does is check for updates, which is done in C:\ProgramData (which is writable for low privileged users by default). The service does so by calling “VPNUpdater.UpdateProduct()”, which in turn calls “Updater.UpdateToNewPackageIfValid()”. This function handles all the logic for updating the VPN software:

Upon entering “Updater.UpdateToNewPackageifValid()”, the service first checks if there is an update that is downloaded via a call to “Updater.CheckForDownloadedUpdatePackage()”. In order to do this, it checks for the existence of “C:\ProgramData\Avira\VPN\Update\AviraVPNInstaller.exe” and if the update file has already been installed or not:

The service determines if the update is already present or not by comparing the “ProductVersion” property on the update executable with the “ProductVersion” property on the VPN service itself (Avira.VPNService.exe). If the update executable’s ProductVersion is greater than the ProductVersion of “Avira.VPNService.exe”, then the service continues down the path to install it:

After validating that “C:\ProgramData\Avira\VPN\Update\AviraVPNInstaller.exe” exists and hasn’t already been installed, the service makes a call to “Updater.IsUpdateFolderAccessRestricted()”. This function appears to make sure that “C:\ProgramData\Avira\VPN\Update” is locked down and cannot be written to by a low privileged user (in order to protect the update executable before it is executed). The service does this by first checking that the folder is owned by either NT AUTHORITY\SYSTEM, NT AUTHORITY\SERVICE or Administrators (values stored in “AcceptedSIDs”):

If the update folder is not owned by any of those SIDs, the function returns and a call is made to “Updater.RestoreUpdateFolder()”, which promptly deletes “C:\ProgramData\Avira\VPN\Update” and then re-creates it with a DACL that restricts access to the 3 accepted SIDs mentioned above. If the folder has an owner that is any of those accepted SIDs, the service then loops through each entry in the folder’s DACL to make sure that those 3 accepted SIDs are in the DACL as well (I assume to make sure that only those 3 privileged users/groups have the ability to control the folder’s contents).

The issue here is that it is possible to circumvent those checks and plant a malicious update in “C:\ProgramData\Avira\Update”. The first task is to pass the “Owner” check on the update folder. This can be accomplished by simply moving another folder on the filesystem that is owned by SYSTEM yet is writable by low privileged users to “C:\ProgramData\Avira\Update”. Since moving a file/folder on the same volume retains the permission set, the update folder will have an Owner of “SYSTEM”, which is what the service is checking for.

To abuse this, we can copy our version of “AviraVPNInstaller.exe” (and dependencies) to “C:\ProgramData\Avira\Launcher\LogFiles”, which is owned by SYSTEM yet writable by low privileged users:

Once done, we can move “C:\ProgramData\Avira\Launcher\Logfiles” to “C:\ProgramData\Avira\VPN\Update”:

At this point, we have a version of “C:\ProgramData\Avira\VPN\Update” that passes the “Owner” check. The next hurdle is to pass the DACL check that the service does to ensure the 3 accepted SIDs are present. This can simply be done by setting the DACL on the update folder to include “Administrators”, “SYSTEM” and “SERVICE”:

Once done, the update folder will only be accessible by the 3 accepted SIDs. After circumventing the Owner and DACL checks, the last hurdle is to circumvent the file integrity checks. Before executing the update file, the service checks to make sure it is signed by Avira and that the signature is valid (via a call to Updater.IsUpdatePackageAuthentic()). If “C:\ProgramData\Avira\VPN\Update\AviraVPNInstaller.exe” is not signed by Avira and does not contain a valid digital signature, the service will not execute it. In order to circumvent this, we need a file signed by Avira that has a ProductVersion greater than the currently installed version of Avira.VPNService.exe. After some hunting, I came across an Avira signed executable named “CefSharp.BrowserSubprocess.exe” that has a product version of “65.0.0.0”:

Since this executable is signed by Avira, has a valid digital certificate and has a product version greater than the present version of “Avira.VPNService.exe”, it will pass all of the checks that the service implements. This executable was renamed to “AviraVPNInstaller.exe” and used above in the file copy and folder move actions.

At this point, we have the following:

  1. A valid Avira signed executable that has a ProductVersion greater than the installed version of Avira.VPNService.exe
    1. When this executable starts, it looks for “VERSION.dll” in the current working directory
  2. The ability to plant this renamed executable, along with a malicious copy of VERSION.dll, in C:\ProgramData\Avira\VPN\Update via circumventing the folder Owner and DACL checks

Once the VPN service starts (via a reboot or manually), it will see that “C:\ProgramData\Avira\VPN\Update\AviraVPNInstaller.exe” is present. It will then see that the “Update” folder is owned by “SYSTEM” and that the folder DACL contains the “Administrators”, “SYSTEM”, and “SERVICE” SIDs. It will then check the file integrity of “AviraVPNInstaller.exe” and see that it is signed by Avira, that the digital signature is valid and that the ProductVersion is greater than the deployed VPN service. After passing all of those checks, the service will then execute the renamed version of “AviraVPNInstaller.exe” as SYSTEM and load our malicious “VERSION.dll”, resulting in local privilege escalation:

This issue has been fixed in the latest Avira VPN version.

CDPSvc DLL Hijacking - From LOCAL SERVICE to SYSTEM

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

CVE-2019-19248: Local Privilege Escalation in EA’s Origin Client

Version: Origin Client version 10.5.35.22222-0 (https://www.origin.com/usa/en-us/store/download)
Operating System tested on: Windows 10 1709 (x64)
Advisory: https://www.ea.com/security/news/easec-2019-001-elevation-of-privilege-vulnerability-in-origin-client
EA’s Blog: https://www.ea.com/security/news/origin-security-update-in-collaboration-with-external-security-researchers

Vulnerability: Origin Client Service DACL Overwrite Elevation of Privilege

Brief Description: When Origin is installed, it comes with a few different services, such as the “Origin Client Service”. This service can be stopped and started by low privileged users. When the Origin Client service starts, it checks for the existence of “C:\ProgramData\Origin\local.xml”. If this file doesn’t exist, it creates it and grants the “Everyone” group “FullControl” over the file. Since a low privileged user has rights to this file, it is possible to create a hardlink on “C:\ProgramData\Origin\local.xml” and point it to another file, resulting in the target file having “FullControl” rights granted to the “Everyone” group.

A low privileged user can use this to overwrite the DACL on privileged files, resulting in elevation of privilege to “NT AUTHORITY\SYSTEM”.

Vulnerability Explanation 
When Origin is installed, it comes with a few different services. One such service is the “Origin Client Service”. This service can be stopped and started by low privileged users:

When restarting the Origin Client Service, it checks to see if “C:\ProgramData\Origin\local.xml” exists. If it doesn’t, it will create it and then set the file’s security descriptor to grant Everyone GENERIC_ALL over the file:

Since a low privileged user has control of that file, its possible to delete it and replace it with a hardlink that points to a privileged file. In this case, we are creating a hardlink that points to “C:\Program Files (x86)\Origin\OriginWebHelperService.exe” (using James Forshaw’s Symbolic Link Testing Tools)

After creating the hardlink, restarting the “Origin Client Service” service will cause it to try and set the DACL on “C:\ProgramData\Origin\local.xml” to grant “FullControl” rights to the “AuthenticatedUsers” group. Since a hardlink is in place, it will follow it and end up setting the DACL on “C:\Program Files (x86)\Origin\OriginWebHelperService.exe” instead:

With the DACL on “C:\Program Files (x86)\Origin\OriginWebHelperService.exe” overwritten, all that needs done to elevate privileges is to stop the Origin Web Helper Service, replace “C:\Program Files (x86)\Origin\OriginWebHelperService.exe” and then start the service again:

The service will fail to start since “Payload.exe” is not a service executable, but the service will start it and cmd.exe will be running as “NT AUTHORITY\SYSTEM”, resulting in elevation of privilege.

This vulnerability has been fixed in 10.5.56.33908. The Origin team re-wrote the Origin client to include a “Restricted” mode that places restrictive ACLs on all of the Origin files.

DISCLOSURE TIMELINE

  • March 13th, 2019: Vulnerability sent to the EA security team
  • March 14th,  2019: EA acknowledged the vulnerability and assigned a case number
  • March 28th, 2019: Followed up with EA to see if there is anything they need
  • April 4th, 2019: EA classified the report as a high severity issue and notified me that they are working on a fix and have found other variants via additional hunting
  • May 2nd, 2019: Reached out to EA to inform them of the approaching 60 day window
  • May 23rd, 2019: EA responded with a note that they are still working on a fix and have ran into some issues with fixing the root cause
  • June 17th, 2019: Reached out to EA to inform them that the 90 day period has lapsed. Asked for an update and if additional time was needed
  • June 25th, 2019: EA informed me they are still having issues with implementing a fix that doesn’t break older game titles. Stated they have a way forward, but will need some time to dev it out. EA asked to schedule a phone call.
  • June 25th, 2019: Responded to EA’s request to schedule a phone call
  • July 8th, 2019: Had a phone call with EA’s security and engineering teams, agreed on periodical 30 day extensions due to the complexity of the issue being fixed
  • August 12th, 2019: Sent EA an additional variant of the issue
  • August 13th, 2019: EA informed me they have preliminary builds of the new Origin client in Alpha, stated they are tracking late September – early October for a fix
  • September 6th, 2019: Reached out to EA to get an estimated timeline on the fix
  • September 12th, 2019: EA responds with a note that they will have a beta build for me to test within the next week and are working on addressing the Mac client
  • September 25th, 2019: EA provides a link to the beta build to test with a well written explanation of the design decisions behind the fix and next steps (released to beta channel eventually). Also provided me an advisory to review.
  • September 26th, 2019: Replied to EA acknowledging receipt of the beta build and a thumbs up on the advisory draft
  • September 26th, 2019: Sent EA a few notes on the beta build, fix seemed sufficient
  • October 28th, 2019: Reached out to EA for a shipping ETA
  • October 28th, 2019: EA responded noting they have a request out to the Origin team for an update, and will provide an update when they can. Noted they are finishing up the Mac rewrite.
  • November 13th, 2019: Reached out to EA for a status update
  • November 13th, 2019: EA replied with dates the new builds will hit the public beta channels. Provided a newly updated Windows build for me to look at
  • November 14th, 2019: Replied to EA noting the beta looked good with restricted mode enabled
  • December 9th, 2019: EA informed me they are on track to publish the Origin update to the public and release the advisory on the 10th
  • December 10th, 2019: Advisory published, issue opened.

Avira Optimizer Local Privilege Escalation

Version: Avira Optimizer < 1.2.0.367
Operating System tested on: Windows 10 1803 (x64)
Vulnerability: Avira Optimizer Local Privilege Escalation through insecure named pipes

Vulnerability Overview
When users install the latest Avira antivirus, it comes shipped with a few different components along with it. One of these components is the Avira Optimizer. In short, “Avira.OptimizerHost.exe” runs as “NT AUTHORITY\SYSTEM” and takes commands issued over the “AviraOptimizerHost” named pipe (\\.\pipe\AviraOptimizerHost). The service does improper validation of the calling client along with invalid checks for started executables, which allows malicious code to issue process create calls to “Avira.OptimizerHost.exe” leading to local privilege escalation.

Identification and Exploitation
When assessing software for privilege escalation vulnerabilities, finding a starting point can often be overwhelming as there are many different primitives and vulnerability classes that exist. My approach often includes starting with the basics and working my way up in complexity. This process typically involves running a tool such as PowerUp, which will identify various trivial (yet common) misconfigurations.

If nothing interesting is returned, the next step is often looking for logical vulnerabilities. These vulnerabilities can be more difficult to automatically identify and require a little more manual poking. My workflow typically involves analyzing world-writable directories, writeable registry locations, exposed named pipes and RPC interfaces via NTObjectManager. When analyzing existing named pipes, it became apparent that some Avira process had created a named pipe with a NULL DACL. This effectively means that full access is granted to any user that requests it:

While interesting, it isn’t incredibly useful if the pipe isn’t being used by a privileged Avira process in some way. Checking the using process IDs of the pipe revealed that a SYSTEM Avira process is utilizing it:

The next step would be to figure out what “Avira.OptimizerHost.exe” is actually doing with the named pipe. This is a rabbit hole worth exploring since a privileged process is interacting with a resource that low privileged users have control over. Since “Avira.OptimizerHost.exe” has a handle to the pipe, it would make sense that the process is ingesting some sort of data being passed over it. In an effort to validate this, the next step was to pop open “Avira.OptimizerHost.exe” in IDA. After some poking, it became evident that the service was taking any client that connected to the “AviraOptimizerHost” named pipe and validating that it is a valid Avira file.

 In order to abuse this named pipe, we need to circumvent this check in order to successfully send data to the service via the named pipe. The service does the check by getting the connecting client via GetNamedPipeClientProcessID() and then pulls the full image path via QueryFullProcessImageNameW()

Once the path is obtained, the service pulls the calling client’s certificate and makes sure that it is signed by Avira and hasn’t been tampered with. The idea here was to make sure only valid Avira processes are able to issue commands to the service. In order to circumvent this, we can simply inject our code into a running Avira process (or probably just clone an existing certificate).

The next step is to figure out what we can issue to the service over the named pipe. In cases like this, I typically like to investigate any potential legitimate clients and see what they do during normal operation. Since this pipe is a part of Avira’s optimizer, I began to look through the installed Avira components. After some dead ends, Avira’s System Speedup application boiled to the top due to the fact that optimization and speedup are Synonymous. After looking in Avira’s “System Speedup” folder, I stumped upon the Avira System Speedup libraries. I then loaded all of the files in the System Speedup folder into DnSpy and began to search for named pipe references. This led me down to “Avira.SystemSpeedup.Core.Client.Services.dll”, specifically the “StartServiceHost()” method.

As suspected, this is code to connect to the “AviraOptimizerHost” named pipe. Underneath, this function goes on to call the “OptimizerHostCommandsClient.Connect()” in the Avira.Optimizer.Common.Tools.OptimizerHostClient class, which sounds really interesting. When looking at this function, it just calls WaitNamedPipe() to wait for the pipe to be ready. Once it is, CreateFile is used to get a handle to the named pipe.

Looking back at the “StartServiceHost” method, it instantiates an instance of the Avira.Optimizer.Common.Tools.OptimizerHostClient class, connects to the “AviraOptimizerHost” named pipe and then goes on to call an interesting method named “StartParentProcess()”. 

When looking at that instantiated class, there are many interesting methods. Such items include: StartProcess, StartParentProcess, StopProcess, AddTask and RemoveTask. These methods take various parameters and then go on to call “SendMessage” after converting the tasking to JSON:

The “SendMessage()” method takes the JSON of the command and sends it to the “AviraOptimizerHost” named pipe, where the SYSTEM process “Avira.OptimizerHost.exe” ingests it:

Looking at “Avira.OptimizerHost.exe”, we can see where the service reads in the JSON and parses out the arguments:

In this case, if we send the “StartProcess()” method to the named pipe, the service will pull out the “procid”, “exec” (executable path),“args” (arguments)/etc from the JSON blob sent over the named pipe. From there, it follows the same logic that was used to validate the named pipe in client, in which it takes the executable path from the “exec” parameter and checks the file’s certificate in order to ensure it belongs to Avira. The service relies on the subject and certificate serial number (both of which are attacker controlled), so it is possible to use a tool like SigPirate to clone the certificate off of a valid Avira executable and apply it to a custom payload.

In order to exploit this, we need to accomplish a few things:

  1. Prepare our payload. In this case, it is a .NET executable named Avira.SystemSpeedup.RealTime.Client.exe that starts cmd.exe
  2. Clone the certificate off of a valid Avira file and apply it to our payload
  3. Write code that injects into a valid Avira process, loads up Avira.Optimizer.Common.Tools.dll and instantiates an instance of the OptimizerHostClient class
  4. Use the exposed methods to connect to the “AviraOptimizerHost” named pipe and issue our commands to the service

For payload creation and certificate cloning, I will leave that as an exercise for the reader. In order to connect to the named pipe and send commands, we can reuse the existing Avira libraries by adding a reference to Avira.Optimizer.Common.Tools.dll and importing the Avira.Optimizer.Common.Tools.OptimizerHostClient namespace. Once done, we can just create an instance of the OptimizerHostCommandsClient class and call any of the interesting methods, such as “StartProcess”.

In order to achieve LPE, all we need to do is inject this assembly into an Avira process and invoke our entrypoint. Again, this is an exercise left up to the reader…but there are various projects that make this process easy (https://github.com/ChadSki/SharpNeedle). 

After injecting into an Avira process and executing the above C# code, cmd.exe will be started as SYSTEM after the assembly connects to the “AviraOptimizerHost” named pipe and sends the “StartProcess()” method with the “exec” argument set to the payload with a cloned Avira certificate (in this case, a payload named Avira.SystemSpeedup.RealTime.Client.exe).

This vulnerability has been fixed in Avira Optimizer version 1.2.0.367. After glancing at the fix, Avira now utilizes WinVerifyTrust() and an apparent path whitelist to ensure started processes aren’t influenced.

Disclosure Timeline

I’d like to take a second to give Avira and their development team props. The team remains in constant contact and fixes issues at a rapid pace. In the case of this report, a fix was developed and distributed to the public around 30 days after the initial report. It is refreshing to work with a vendor that takes vulnerability reports seriously and follows the community’s set expectations of 90 day fixes.

As committed as SpecterOps is to transparency, we acknowledge the speed at which attackers adopt new offensive techniques once they are made public. This is why prior to publication of a new bug or offensive technique, we regularly inform the respective vendor of the issue, supply ample time to mitigate the issue, and notify select, trusted vendors in order to ensure that detections can be delivered to their customers as quickly as possible.

  • July 23rd, 2019: Vulnerability sent to the Avira security team
  • July 24th,  2019: Avira acknowledged the report, noted some compile issues with the PoC
  • July 26th, 2019: Avira reproduced the vulnerability with the PoC provided
  • August 6th, 2019: Avira noted the developers fixed the issue, asked if I would like to test the fix
  • August 6th, 2019: Replied to Avira with a bypass for the patch, provided updated PoC and details
  • August 16th, 2019: Avira replied noting the developers implemented a new fix and asked if I’d like to test it
  • August 16th, 2019: Tested the new fix. Let Avira know that it seemed decent enough
  • August 27th, 2019: Fix pushed live
  • August 29th, 2019: Details published

CVE-2019-13382: Local Privilege Escalation in SnagIt

Version: Snagit 2019.1.2 Build 3596
Operating System tested on: Windows 10 1803 (x64)
Vulnerability: SnagIt Relay Classic Recorder Local Privilege Escalation through insecure file move

This vulnerability was found in conjunction with Marcus Sailler, Rick Romo and Gary Muller of Capital Group’s Security Testing Team

Vulnerability Overview
Every 30-60 seconds, the TechSmith Uploader Service (UploaderService.exe) checks the folder “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations” for any presentation files in the “*.xml” format. If an invalid one is found, the service moves that file to “C:\ProgramData\Techsmith\TechSmith Recorder\InvalidPresentations” as SYSTEM.

Since a low privileged user has full control over the QueuedPresentations and InvalidPresentations folders, it is possible to create an invalid presentation in the QueuedPresentations folder and then place a symbolic link for that file name in the InvalidPresentations folder that points to a privileged location.

When the service checks for presentations, it will move the file out of the QueuedPresentations folder and into the InvalidPresentations folder. When it does so, the service will hit the symbolic link and write the new file into a protected location with permissions that allow the low privileged user full control over the contents, resulting in Elevation of Privilege to NT AUTHORITY\SYSTEM.

Identification and Exploitation
When assessing software for privilege escalation vulnerabilities, finding a starting point can often be overwhelming as there are many different primitives and vulnerability classes that exist. My approach often includes starting with the basics and working my way up in complexity. This process typically involves running a tool such as PowerUp, which will identify various trivial (yet common) misconfigurations.

If nothing interesting is returned, the next step is often looking for logical vulnerabilities; specifically abusing symlink/mountpoint/hardlink primitives. In order to quickly identify potential vulnerabilities that could be exploited with the linking primitives, we need to identify locations on the OS where a privileged process (often SYSTEM) is interacting with a folder or file that a low privileged user has control over. This logic is true in most logical vulnerabilities in that interesting attack surface is linked to a privileged process utilizing a resource a low privileged user controls.

When hunting for such bugs, I often start with running Process Monitor with a filter on SYSTEM processes and commonly abused filesystem locations, such as C:\ProgramData, C:\Windows\Temp and C:\Users\<username>\AppData. Such a filter might look like so:

 

When applying the Process Monitor and watching the output for a few minutes, it became apparent that “UploaderService.exe” was querying the “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations” directory for any XML files:

Looking at the DACL on that folder, it also stood out that that “BUILTIN\Users” had write access:

This is particularly interesting in that a privileged SYSTEM process (UploaderService.exe) is looking for files in a directory that low privileged users have read/write access. With this information, the next step was to give “UploaderService.exe” an XML file to find and see what happens.

As expected, “UploaderService.exe” checks “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations” for any XML files and finds the one we created:

The next question was, what does “UploaderService.exe” do with our XML file? Does it read it in and ingest the contents? Does it place it someplace else?

Looking at the rest of the Process Monitor output answers that question for us. In this case, “UploaderService.exe” takes any XML files in “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations” and determines if the XML presentation file is valid. Since we simply echoed “1” into our XML file, the service executable determines that “1.xml” is an invalid presentation and moves it to “C:\ProgramData\Techsmith\TechSmith Recorder\InvalidPresentations\1.xml”:

Looking at the “C:\ProgramData\Techsmith\TechSmith Recorder\InvalidPresentations” directory, “BUILTIN\Users” also have read/write access:

At this point, we have identified that a SYSTEM process (UploaderService.exe) is checking a user-controlled directory for any XML files. When found, the privileged process takes the attacker supplied XML file and moves it from the QueuedPresentations folder to the InvalidPresentations folder while retaining the original file name.

Why is this interesting? This presents the opportunity to use Symbolic Links during the move file operation to accomplish a privileged file write. How you might ask? The workflow would be like so:

  • Create a Symbolic Link on “C:\ProgramData\Techsmith\TechSmith Recorder\InvalidPresentations\1.xml” that points to “C:\Windows\System32\ualapi.dll”
    • It should be noted that “C:\Windows\System32\ualapi.dll” doesn’t exist. This is a DLL we are planting to get code-execution as SYSTEM
    • Since the process is privileged “SYSTEM”, it will have the correct permissions to write this file.
  • Write a dummy xml file to “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations\1.xml”
  • When “UploaderService.exe” checks “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations” for any XML files, it will find “1.xml” and move it to “C:\ProgramData\Techsmith\TechSmith Recorder\InvalidPresentations\1.xml”. While doing so, it will hit our Symbolic Link and instead move the file to “C:\Windows\System32\ualapi.dll” (while retaining the original DACL)

In theory, this should work. Let’s test it out! For the Symbolic Link, I used “CreateSymlink.exe” from James Forshaw’s Symbolic Link Testing Tools repo. All we need to do is place a symbolic link on “C:\ProgramData\Techsmith\TechSmith Recorder\InvalidPresentations\1.xml” that points to “C:\Windows\System32\ualapi.dll” and then create “C:\ProgramData\Techsmith\TechSmith Recorder\QueuedPresentations\1.xml”:

With the symlink created and our dummy XML file created, we wait 60 seconds for “UploaderService.exe” to check the QueuedPresentations folder. When it does, it finds our “1.xml” file and tries to move it to “C:\ProgramData\TechSmith\TechSmith Recorder\InvalidPresentations\1.xml”. When it does so, it hits our symbolic link on “C:\ProgramData\TechSmith\TechSmith Recorder\InvalidPresentations\1.xml” and instead writes it to “C:\Windows\System32\ualapi.dll”:

We can then confirm the existence of “C:\Windows\System32\ualapi.dll”:

This is great and all, but shouldn’t the newly created “ualapi.dll” file simply inherit the permissions of the parent folder (C:\Windows\System32) and prevent a low privileged user from writing to it? That was my thought at first (before checking the DACL on the file), but “UploaderService.exe” uses MoveFileW(). According to the documentation, MoveFileW() retains the original DACL when moving the file on the same volume:

While not explicitly stated, it can be inferred that if the file is not moved across volumes, it is moved with the DACL intact. This means that when “UploaderService.exe” hits the symbolic link on “C:\ProgramData\TechSmith\TechSmith Recorder\InvalidPresentations\1.xml” and tries to move the original file to “C:\Windows\System32\ualapi.dll”, it keeps the original DACL for “1.xml”.  Since it was created by a low privileged user, it has a DACL that has the low privileged user as the Owner with “FullControl” rights:

At this point, we now have “C:\Windows\System32\ualapi.dll” that allows our low privileged user to write to it. This means we can simply copy over the newly created ualapi.dll file with a payload of our choosing. In this case, the payload starts cmd.exe when loaded.

We now have a payload sitting in C:\Windows\System32\ualapi.dll. This DLL gets loaded when the spooler service starts. For the PoC, all that is left is to reboot the host in order to get the spooler service to restart. Additionally, one could use the CollectorService to load the DLL without a reboot. Since this is a PoC, that is an exercise left up to the reader.

Once the host is rebooted, “spoolsv.exe” will load our payload from C:\Windows\System32\ualapi.dll as SYSTEM, resulting in privilege escalation:

A video of exploitation can be found here: https://www.youtube.com/watch?v=V90JRwlaHRY&feature=youtu.be

This vulnerability has been fixed in SnagIt versions 2019.1.3, 2018.2.4 and 13.1.7 with CVE-2019-13382. The fixed involved using _time64 when moving the file combined with a check for reparse points (FSCTL_GET_REPARSE_POINT). If a reparse point exists, it is removed.

 

Disclosure Timeline

As committed as SpecterOps is to transparency, we acknowledge the speed at which attackers adopt new offensive techniques once they are made public. This is why prior to publicization of a new bug or offensive technique, we regularly inform the respective vendor of the issue, supply ample time to mitigate the issue, and notify select, trusted vendors in order to ensure that detections can be delivered to their customers as quickly as possible.

  • June 19th, 2019: Vulnerability identified in conjunction with Capital Group’s Security Testing Team
  • June 20th, 2019: Joint disclosure with Capital Group began. Support case opened with a request for contact information for the security team at TechSmith
  • June 21st, 2019: Case assigned to a handler, new comment stated that the details can be uploaded to the current case and they will be forwarded to the security team
  • June 21st, 2019: Full write-up, PoC code and a demonstration video was uploaded to the open support case
  • June 25th, 2019: TechSmith confirmed the vulnerability and offered temporary remediation advice. TechSmith also requested notice before public disclosure.
  • June 25th, 2019: Informed TechSmith that public disclosure would be 90 days after the initial report with a note that an extension would be considered if needed.
  • July 2nd, 2019: TechSmith stated a fixed build is done and set to be deployed before the end of July with a note asking if we would verify the fix
  • July 2nd, 2019: Informed TechSmith that I would verify the fix
  • July 3rd, 2019: TechSmith provided a private fixed build
  • July 9th, 2019: Informed SnagIt that based on testing, the fix seemed sufficient 
  • July 23rd, 2019: Patch released, issue publicly disclosed

CVE-2019-13142: Razer Surround 1.1.63.0 EoP

Version: Razer Surround 1.1.63.0
Operating System tested on: Windows 10 1803 (x64)
Vulnerability: Razer Surround Elevation of Privilege through Insecure folder/file permissions

Purpose
I hope that this post serves as a motivator for folks who see vulnerability research as an intimidating area to get started in. While this bug can be considered simple, the primary purpose of this post is to outline the methodology behind how to get started and what to look for. Additionally, I’d like it to serve as a reminder to not discount the low hanging fruit, no matter how large the organization.

Brief Description:
Razer Surround installs a service named “RzSurroundVADStreamingService” that runs as SYSTEM. This service runs “RzSurroundVADStreamingService.exe” out of “C:\ProgramData\Razer\Synapse\Devices\Razer Surround\Driver”. The permissions on  RzSurroundVADStreamingService.exe and “C:\ProgramData\Razer\Synapse\Devices\Razer Surround\Driver” allow for overwriting the service binary with a malicious one, resulting in elevation of privilege to SYSTEM.

Identification and Exploitation
When doing vulnerability research, picking a target to go after can be challenging. Hunting for bugs in large software platforms can be intimidating as there is an assumption that all vulnerabilities are complex and take a special skill set to identify. I’d like to use this vulnerability as an example as to why the act of hunting for vulnerabilities isn’t as hard as it sounds.

You may ask, why Razer? How do you identify a piece of software to begin hunting for vulnerabilities in? The answer is simple: Investigate what interests you. In this case, I own various Razer products. It is hard to ignore the urge to look when you use a product and the software associated with it every day.

When looking for vulnerabilities, there is often a common workflow that I follow once the software of interest is installed. This stage involves analyzing the potential attack surface that the target software has exposed. I typically start with the basics and then resort to dynamic/static analysis if needed. The things I typically look for initially are:

  1. Installed services (both the service permissions and the service executable/path permission)
  2. Named pipes (and their ACLs)
  3. Log file permissions in folders like C:\ProgramData
  4. Network sockets
  5. DCOM servers and hosted interfaces

As far as tooling goes, I mostly stick to Process Monitor and James Forshaw’s NTObjectManager project.

In the instance of Razer Surround, I began by checking what privileged processes the software uses by looking at the process list. This revealed that “RzSurroundVADStreamingService.exe” was running as “NT AUTHORITY\SYSTEM”. The next step was to figure out how that process was being started. Given the name of the process has “service” in it, that is a good starting point. To verify, it was easy enough to do “Get-Service *Rz*” in Powershell, which returned all of the services with “Rz” in the name. This led me to the “RzSurroundVadStreamingService” system service with the ImagePath set to the executable of interest. After dumping the ImagePath, the location of the service executable stood out as it was running out of “C:\ProgramData\Razer\Synapse\Devices\Razer Surround\Driver\”

Why is this interesting? By default, “BUILTIN\Users” have “GenericWrite” access to C:\ProgramData:

A very common error that software developers make is not properly locking down the permissions of any created subfolders in C:\ProgramData. If an installer simply creates a folder in C:\ProgramData, that folder and any subfolders will have inherited permissions of C:\ProgramData, which include the “GenericWrite” access right for “BUILTIN\Users”.

Improper file and folder permissions were the culprit in this case as “Everyone” was eventually granted “FullControl” over any files in “C:\ProgramData\Razer\Synapse\Devices\Razer Surround\Driver”

As previously noted, this path is where the “RzSurroundVADStreamingService” ImagePath for the service executable was pointing to. Given a low privileged user has “FullControl” over the folder and included files, it is possible to just replace the service executable for the “RzSurroundVADStreamingService” system service:

Once the payload is copied, rebooting the host will cause the service to start the new executable as SYSTEM. In this instance, the new service executable will start cmd.exe as SYSTEM:

Razer fixed this vulnerability by moving “RzSurroundVADStreamingService.exe” and the associated dependencies to a secured location in “C:\Program Files (x86)\Razer”.

Disclosure Timeline
As committed as SpecterOps is to transparency, we acknowledge the speed at which attackers adopt new offensive techniques once they are made public. This is why prior to publicization of a new bug or offensive technique, we regularly inform the respective vendor of the issue, supply ample time to mitigate the issue, and notify select, trusted vendors in order to ensure that detections can be delivered to their customers as quickly as possible.

  • March 20th, 2019 — Initial report sent to Razer
  • March 21st, 2019  — Report acknowledgement received from Razer
  • April 30th, 2019 —  30 days after initial report
  • May 2nd, 2019  —  Razer provided a fixed build to test
  • May 2nd, 2019 —  Fix was verified
  • May 20th, 2019 —  60 days after initial report
  • June 6th, 2019 —  Reached out to Razer for a timeframe regarding a public fix
  • June 6th, 2019 —  Razer informed me a fix should be live, but verification from the development team was needed
  • June 7th, 2019 —  Informed Razer that a fix wasn’t available on the site or via Surround’s update mechanism
  • June 10th, 2019 —  Razer informed me that there had been some internal confusion and that a fix was going live the end of June
  • June 11th, 2019 —  Informed Razer I would hold off on disclosure until the fix is live
  • June 20th, 2019 —  90 days after initial report, extension granted
  • July 1st, 2019 —  Razer informed me that a note is out to the development team regarding when the fix would be pushed live
  • July 5th, 2019 —  Fix published

-Matt N.

Avira VPN (2.15.2.28160) Elevation of Privilege through Insecure Update location

Product Version: Avira Phantom VPN
Downloaded from: https://package.avira.com/package/oeavira/win/int/avira_en_vpn__ws.exe
Operating System tested on: Windows 10 1709 (x64)
Vulnerability: Avira VPN Elevation of Privilege

Brief Description: The Avira Phantom VPN Service performs a handful of checks when it checks for an update (this happens when the service starts, so on boot or via manual start/stopping). When updating, the VPN service downloads the update to “C:\Windows\Temp\” as AviraVPNInstaller.exe (which any user can write to). It then checks if the file is signed, and if the version is less than the installed product version. If these requirements are met, the service will run the update binary as SYSTEM.  It is possible to overwrite “AviraVPNInstaller.exe” with a signed, valid Avira executable (with a version greater than the installed version) that looks for various DLLs in the current directory. Due to the search order, it is possible to plant a DLL in “C:\Windows\Temp” and elevate to NT AUTHORITY\SYSTEM.

Vulnerability Explanation
When the Avira VPN service (Avira.VPNService.exe, version 2.15.2.28160) starts, it checks to see if there is an update available:

After some poking, it was determined that the VPN service updates from “C:\Windows\Temp”

As you may know already, any authenticated user can write (but not read) to C:\Windows\Temp. Once the update is there (as C:\Windows\Temp\AviraVPNInstaller.exe), the VPN service checks the “FileVersion” property of the executable in order to determine if the “update” is already installed on the system:

If the version of the file shows it hasn’t been installed, the service will check the file to make sure it is valid and signed by Avira:

If the file is indeed valid and signed by Avira, the VPN service will start the “update” package. Since all of this happens in a place a low privilege user can write to, it is possible to hijack the update package and perform DLL sideloading.

In order to hijack “AviraVPNInstaller.exe”, three of the following conditions have to be met:

  1. “AviraVPNInstaller.exe” has to be signed by Avira
  2. The signature on “AviraVPNInstaller.exe” has to be valid (any modification of that file would invalidate the signature
  3. The version number of “AviraVPNInstaller.exe” proves the update hasn’t been installed on the system.

This means we need to abuse an Avira signed file that has a version greater than what is deployed.

After some hunting, “ToastNotifier.exe” fit the bill as it satisfies all three requirements:

(If you are curious of where ToastNotifier.exe came from, it is from the Avira Antivirus product suite that was abused in a similar manner in a bug reported on the Antivirus platform).

To exploit this, all we need to do is copy “ToastNotifier.exe” to “C:\Windows\Temp\AviraVPNInstaller.exe”. Since the requirements are met, the VPN service will run it as SYSTEM when the service restarts. This can be accomplished via a reboot, or by running powershell as an Administrator and then doing “Get-Service AviraPhantomVPN | Stop-Service” followed by “Get-Service AviraPhantomVPN | Start-Service”. Once executed, “ToastNotifier.exe” (which is now C:\Windows\Temp\AviraVPNInstaller.exe) will try to load a handful of DLLs out of C:\Windows\temp:

 To elevate our privileges to SYSTEM, all we need to do is provide one of those DLLs. In this case, we will hijack “cryptnet.dll”. This will result in the VPN service starting our “AviraVPNInstaller.exe” binary as SYSTEM, which will then load “cryptnet.dll” as SYSTEM, which is our malicious code. This results in elevation from a low privileged user to SYSTEM:

 

Disclosure Timeline:

  1. Submitted to Avira on September 28, 2018
  2. October 1, 2018: Issue reproduced by Avira
  3. December 13, 2018: Issue fixed by Avira

 

 

 

 

 

 

 

 

 

Avira VPN 2.15.2.28160 Elevation of Privilege

Product Version: Avira Phantom VPN version 2.15.2.28160

Downloaded from: https://package.avira.com/package/oeavira/win/int/avira_en_vpn__ws.exe

Operating System tested on: Windows 10 1803 (x64)

Vulnerability: Avira VPN Elevation of Privilege

Brief Description: The Avira Phantom VPN service changes the DACL on “C:\ProgramData\Avira\VPN\VpnSharedSettings.backup” and “C:\ProgramData\Avira\VPN\VpnSharedSettings.config” when a configuration change is made in the VPN settings menu. By setting a hardlink on “C:\ProgramData\Avira\VPN\VpnSharedSettings.backup”, it is possible to overwrite the DACL on an arbitrary file, leading to elevation from a low privileged user to SYSTEM.

Vulnerability Explanation

When making a configuration change via the VPN GUI, the VPN service (Avira.VPNService.exe) calls the function “AdjustSecurity()” that resides in “Avira.VPN.Core.dll” to change the DACL to allow any authenticated user to write to “C:\ProgramData\Avira\VPN\VpnSharedSettings.backup” or “C:\ProgramData\Avira\VPN\VpnSharedSettings.config”. When a configuration change is made (which can be done as a low privileged user), the service makes sure the “Shared” VPN profile setting file (C:\ProgramData\Avira\VPN\VpnSharedSettings.backup and C:\ProgramData\Avira\VPN\VpnSharedSettings.config) exist by calling “EnsureFileExists()”:

If the configuration file doesn’t exist, the service will create it and continue. If it does, it simply continues to  the “StorageSecurity.AdjustSecurity()” function. In the case of the shared vs private configuration profiles, the “StorageType” being passed is either “AllUserAccess” or “Secure”. The “private” VPN profile is assigned the “Secure” storage type while the shared profile is assigned “AllUserAccess”.

When the “AdjustSecurity()” function is called (passing the storage type of the configuration file), it adjusts the DACL on the file itself appropriately. This is where the bug lies. For the “shared” profile (with the StorageType of “AlluserAccess”), the VPN service calls “AdjustSecurity()” and grants the everyone full control over the file:

This is dangerous because the “SetAccessControll()” call changes the DACL on a file in which a low privileged user can control. So, how do we exploit this?

First, an attacker needs to create a hardlink on “C:\ProgramData\Avira\VPN\VpnSharedSettings.backup” and point it at a file. In this case, we will point it at  “C:\Program Files (x86)\Avira\VPN\OpenVPN\phantomvpn.exe”. Doing so will overwrite the DACL on “C:\Program Files (x86)\Avira\VPN\OpenVPN\phantomvpn.exe”.

After doing so, open the VPN and click the “Settings” tab:

After doing so, uncheck the “Send Diagnostic Data” box. This will trigger a config change and kick the code-path off:

At this point, the DACL on “C:\Program Files (x86)\Avira\VPN\OpenVPN\phantomvpn.exe” has been overwritten to allow any user to write to it. The attacker now just needs to copy a malicious binary to replace it:

Once done, execution of the malicious binary can be accomplished by simply trying to connect to the VPN. This can be done by clicking “Secure my Connection” in the VPN GUI:

After clicking “Secure my connection”, you should see a few cmd.exe processes running as SYSTEM:

Disclosure Timeline:

  1. Reported to Avira on September 28, 2018
  2. October 1, 2018: Avira acknowledged the report
  3. October 4, 2018: Avira was able to reproduce
  4. December 13, 2018: Issue resolved

Razer Synapse 3 Elevation of Privilege

Product Version: Razer Synapse 3 (3.3.1128.112711) Windows Client
Downloaded from: https://www.razer.com/downloads
Operating System tested on: Windows 10 1803 (x64)
Vulnerability: Razer Synapse Windows Service EoP

Brief Description: The Razer Synapse software has a service (Razer Synapse Service) that runs as “NT AUTHORITY\SYSTEM” and loads multiple .NET assemblies from “C:\ProgramData\Razer\*”. The folder “C:\ProgramData\Razer\*” and recursive directories/files have weak permissions that grant any authenticated user FullControl over the contents. It is possible to circumvent signing checks and elevate to SYSTEM using assembly sideloading.

Vulnerability Explanation:
When the Razer Synapse service starts, it will load .NET assemblies out of various directories within “C:\ProgramData\Razer\*”, such as “C:\ProgramData\Razer\Synapse3\Service\bin”.

When looking at the DACL on the folder “C:\ProgramData\Razer\Synapse3\Service\bin”, you will notice that “Everyone” has “FullControl” rights over the folder (including any files within the folder):

In theory, an attacker could simply replace an existing .NET assembly with a malicious one, reboot the system and let the Razer Synapse Service load it when it starts. This approach came with some complications, such as a race condition to replace an assembly before the service loads it. Additionally, the service implements some checks that must be passed before the assembly can be loaded. For efficient exploitation, it is important to fully understand the conditions in which an assembly can be loaded successfully.

The first issue to tackle is getting a malicious assembly planted in such a way that the service will try to load it. Hijacking an existing assembly can be challenging as low privileged users do not have rights to stop or start the Razer Synapse service. This means that to trigger the assembly loading code path, the box needs to be rebooted. This makes winning the race condition for swapping out a legitimate assembly with a malicious one challenging. Looking at the service, this problem is solved pretty easily as it recursively enumerates all DLLs in “C:\ProgramData\Razer\*”.

This means that we can simply drop an assembly in one of the folders (C:\ProgramData\Razer\Synapse3\Service\bin, for example) and it will be treated the same as an existing, valid assembly.

After recursively enumerating all DLLs in “C:\ProgramData\Razer\*”, the service attempts to ensure those identified assemblies are signed by Razer. This is done by grabbing certificate information from “Razer.cer”, calling X509Certificate.CreateFromSignedFile() on each assembly and then comparing the certificate chain from Razer.cer with the assembly being loaded.

If the certificate chain on the assembly doesn’t match that of Razer.cer, the service will not load it. While the thought behind checking the trust of .NET assemblies before loading them is good, the implementation wasn’t robust, as X509Certificate.CreateFromSignedFile() only extracts the certificate chain and in no way attests the validity of the signature of the file being checked (https://twitter.com/tiraniddo/status/1072475737142239233). This means that it is possible to use a tool such as SigPirate to clone the certificate from a valid Razer assembly onto a malicious one, due to the fact that the signature of said assembly is never actually verified.

Once the assembly passes the certificate check, the service will then load it into the current app domain via  Assembly.LoadFile(). No malicious code will execute during the Assembly.LoadFile() call, however. After doing so, the service will check to make sure there is an IPackage interface implemented.

This interface is specific to the SimpleInjector project, which is well documented. The only requirement to pass this check is to implement the IPackage interface in our malicious assembly. Once the service validates the certificate chain of the assembly and verifies the presence of IPackage, it adds the assembly to a running list. Once this is done for all the assemblies found in “C:\ProgramData\Razer\*”, the list is then passed to SimpleInjector’s “RegisterPackages()” function.

RegisterPackages() will take the list of “verified” assemblies and call the “RegisterServices()” function within the IPackage interface of each assembly.

This is the point in which we, as an attacker, can execute malicious code. All that needs done is to add malicious logic in the “RegisterServices()” method within the IPackage interface of our malicious assembly.

At this point, we have found ways to abuse all of the requirements to get elevated code-execution.

  1. Write a custom assembly that implements the IPackage interface from the SimpleInjector project
  2. Add malicious logic in the “RegisterServices()” method inside the IPackage interface
  3. Compile the assembly and use a tool such as SigPirate to clone the certificate chain from a valid Razer assembly
  4. Drop the final malicious assembly into “C:\ProgramData\Razer\Synapse3\Service\bin”
  5. Restart the service or reboot the host

Exploitation:
After understanding the requirements to get arbitrary code-execution in an elevated context, we can now exploit it. First, we need to create our malicious assembly that implements the required IPackage interface. To do so, a reference to the “SimpleInjector” and “SimpleInjector.Packaging” assemblies need to be added from the SimpleInjector project. Once the reference is added, we just need to implement the interface and add malicious logic. A PoC assembly would look something like this:

Since the Razer service is 32-bit, we compile the assembly as x86. Once compiled, we need to pass the certificate chain check. Since the service is using X509Certificate.CreateFromSignedFile() without any signature validation, we can simply clone the certificate from a signed Razer assembly using SigPirate:

Using “Get-AuthenticodeSignature” in PowerShell, we can verify that the certificate was applied to our “lol.dll” assembly that was created from SigPirate:

At this point, we have a malicious assembly with a “backdoored” IPackage interface that has a cloned certificate chain from a valid Razer assembly. The last step is to drop “lol.dll” in “C:\ProgramData\Razer\Synapse3\Service\bin” and reboot the host. Once the host restarts, you will see that “Razer Synapse Service.exe” (running as SYSTEM) will have loaded “lol.dll” out of “C:\ProgramData\Razer\Synapse3\Service\bin”, causing the “RegisterServices()” method in the implemented IPackage interface to execute cmd.exe.

When the service loads “lol.dll”, it sees it as valid due to the cloned certificate, and EoP occurs due to the “malicious” logic in the IPackage implementation.

Razer fixed this by implementing a new namespace called “Security.WinTrust”, which contains functionality for integrity checking. The service will now call “WinTrust.VerifyEmbeddedSignature() right after pulling all the “*.dll” files from the Razer directory.

When looking at “WinTrust.VerifyEmbeddedSignature()”, the function utilizes “WinTrust.WinVerifyTrust()” to validate that the file being checked has a valid signature (through WinVerifyTrust()).

If the file has a valid signature AND the signer is by Razer, then the service will continue the original code path of checking for a valid IPackage interface before loading the assembly. By validating the integrity of the file, an attacker can no longer clone the certificate off of a signed Razer file as the signature of the newly cloned file will not be valid.

For additional reading on trust validation, I encourage you to read the whitepaper “Subverting Trust in Windows” by Matt Graeber.

Disclosure Timeline:

06/05/2018: Submitted vulnerability report to Razer’s HackerOne program
06/08/2018: Response posted on the H1 thread acknowledging the report
06/08/2018: H1 staff asked for specific version number of the Synapse 3 installer
06/08/2018: Synapse 3 installer version number provided to Razer
07/05/2018: Asked for an update
08/06/2018: Report marked as triaged
08/27/2018: Asked for an update, no response
09/14/2018: Asked for update, along with a direct email address to speed up communication. No response
12/14/2018: Asked for a security contact for Razer via Twitter
12/14/2018: H1 program manager reached out to investigate the H1 report
12/15/2018: Razer CEO Min-Liang Tan reached out directly asking for a direct email to pass to the security team
12/16/2018: The Information Security Manager and SVP of Software reached out directly via email. I was provided context that a fix would be pushed out to the public in a couple of weeks
12/19/2018: Pulled down the latest Synapse 3 build and investigated vulnerable code path. Submitted additional information to Razer’s H1 program, along with notice to Razer’s Manager of Information Security
12/25/2018: I was contacted by someone at Razer with a link to an internal build for remediation verification
12/27/2018: Per their request, provided feedback on the implemented mitigation via the H1 report
01/09/2019: Asked for a timeline update for the fixed build to be provided to the public (via H1)
01/10/2019: Informed that the build is now available to the public
01/10/2019: Report closed
01/10/2019: Requested permission for public disclosure
01/10/2019: Permission for public disclosure granted by Razer
01/21/2019: Report published

*Note: While the disclosure timeline was lengthy, I have to assume it was due to a disconnect between the folks at Razer managing the H1 program and the folks at Razer working on the fix. Once I was provided an internal contact, the timeline and experience improved drastically.

 

-Matt N.

CVE-2018–8414: A Case Study in Responsible Disclosure

The process of vulnerability disclosure can be riddled with frustrations, concerns about ethics, and communication failure. I have had tons of bugs go well. I have had tons of bugs go poorly.

I submit a lot of bugs, through both bounty programs (Bugcrowd/HackerOne) and direct reporting lines (Microsoft). I’m not here to discuss ethics. I’m not here to provide a solution to the great “vulnerability disclosure” debate. I am simply here to share one experience that really stood out to me, and I hope it causes some reflection on the reporting processes for all vendors going forward.

First, I’d like to give a little background on myself and my relationship with vulnerability research.

I’m not an experienced reverse engineer. I’m not a full-time developer. Do I know C/C++ well? No. I’m relatively new to the industry (3 years in). I give up my free time to do research and close my knowledge gaps. I don’t find crazy kernel memory leaks, rather, I find often overlooked user-mode logic bugs (DACL overwrite bugs, anyone?).

Most importantly, I do vulnerability research (VR) as a hobby in order to learn technical concepts I’m interested in that don’t necessarily apply directly to my day job. While limited, my experience in VR comes with the same pains that everyone else has.

When I report bugs, the process typically goes like this:

  1. Find bug->Disclose bug->Vendor’s eyes open widely at bug->Bug is fixed and CVE possibly issued (with relevant acknowledgement)->case closed
  2. Find bug->Disclose bug->Vendor fails to see the impact, issues a won’t fix->case closed

When looking at these two situations, there are various factors that can determine if your report lands on #1 or #2. Such factors can include:

  1. Internal vendor politics/reorg
  2. Case handler experience/work ethic/communication (!!!!)
  3. Report quality (did you explain the bug well, and outline the impact the bug has on a product?)

Factors that you can’t control can start to cause frustration when they occur repeatedly. This is where the vendor needs to be open to feedback regarding their processes, and where researchers need to be open to feedback regarding their reports.

So, let us look at a case study in a vulnerability report gone wrong (and then subsequently rectified):

On Feb 16, 2018 at 2:37 PM, I sent an email to [email protected] with a write-up and PoC for RCE in the .SettingContent-ms file format on Windows 10. Here is the original email:

This situation is a good example where researchers need to be open to feedback. Looking back on my original submission, I framed the bug mostly around Office 2016’s OLE block list and a bypass of the Attack Surface Reduction Rules in Windows Defender. I did, however, mention in the email that “The PoC zip contains the weaponized .settingcontent-ms file (which enables code-execution from the internet with no security warnings for the user)”. This is a very important line, but it was overshadowed by the rest of the email.

On Feb 16, 2018 at 4:34 PM, I received a canned response from Microsoft stating that a case number was assigned. My understanding is that this email is fairly automated when a case handler takes (or is assigned) your case:

Great. At this point, it is simply a waiting game while they triage the report. After a little bit of waiting, I received an email on March 2nd, 2018 at 12:27pm stating that they successfully reproduced the issue:

Awesome! This means that they were able to take my write-up with PoC and confirm its validity. At this very point, a lot of researchers see frustration. You take the time to find a bug, you take the time to report it, you get almost immediate responses from the vendor, and once they reproduce it, things go quiet. This is understandable since they are likely working on doing root cause analysis on the issue. This is the critical point in which it will be determined if the bug is worth fixing or not.

I will admit, I generally adhere to the 90 day policy that Google Project Zero uses. I do not work for GPZ, and I don’t get paid to find bugs (or manage multiple reports). I tend to be lenient if the communication is there. If a vendor doesn’t communicate with me, I drop a blog post the day after the 90 days window closes.

Vendors, PLEASE COMMUNICATE TO YOUR RESEARCHERS!

In this case, I did as many researchers would do once more than a month goes by without any word…I asked for an update:

At this point, it has almost been a month and a half since I have heard anything. After asking for an update, this email comes in:

Interesting…I all of the sudden have someone else handling my case? I can understand this as Microsoft is a huge organization with various people handling the massive load of reports they get each day. Maybe my case handler got swamped?

Let’s pause and evaluate things thus far: I reported a bug. This bug was assigned a case number. I was told they reproduced the issue, then I hear nothing for a month and a half. After reaching out, I find out the case was re-assigned. Why?

Vendors, this is what causes frustration. Researchers feel like they are being dragged along and kept in the dark. Communication is key if you don’t want 0days to end up on Twitter. In reality, a lot of us sacrifice personal time to find bugs in your products. If people feel like they don’t matter or are placed on the backburner, they are less likely to report bugs to you and more likely to sell them or drop them online.

Ok, so my case was re-assigned on April 25th, 2018 at 12:42 pm. I say “Thanks!!” a few days later and let the case sit while they work the bug.

Then, a little over a month goes by with no word. At this point, it has been over 90 days since I submitted the original report. In response, I sent another follow up on June 1st, 2018 at 1:29pm:

After a few days, I get a response on June 4th, 2018 at 10:29am:

Okay. So, let’s take this from the top. On Feb 16, 2018, I reported a bug. After the typical process of opening a case and verifying the issue, I randomly get re-assigned a case handler after not hearing back for a while. Then, after waiting some time, I still don’t hear anything. So, I follow up and get a “won’t fix” response a full 111 days after the initial report.

I’ll be the first to admit that I don’t mind blogging about something once a case is closed. After all, if the vendor doesn’t care to fix it, then the world should know about it, in my opinion.

Given that response, I went ahead and blogged about it on July 11, 2018. After I dropped the post, I was contacted pretty quickly by another researcher on Twitter letting me know that my blog post resulted in 0days in Chrome and Firefox due to Mark-of-the-Web (MOTW) implications on the .SettingContent-ms file format. Given this new information, I sent a fresh email to MSRC on June 14, 2018 at 9:44am:

At this point, I saw two exploits impacting Google Chrome and Mozilla FireFox that utilized the .SettingContent-ms file format. After resending details, I got an email on June 14, 2018 at 11:05am, in which MSRC informed me the case would be updated:

On June 26, 2018 at 12:17pm, I sent another email to MSRC letting them know that Mozilla issued CVE-2018-12368 due to the bug:

That same day, MSRC informed me that the additional details would be passed along to the team:

This is where things really took a turn. I received another email on July 3, 2018 at 9:52pm stating that my case had been reassigned once again, and that they are re-evaluating the case based on various other MSRC cases, the Firefox CVE, and the pending fixes to Google Chrome:

This is where sympathy can come into play. We are all just people doing jobs. While the process I went through sucked, I’m not bitter or angry about it. So, my response went like this:

After some time, I became aware that some crimeware groups were utilizing the technique in some really bad ways (https://www.proofpoint.com/us/threat-insight/post/ta505-abusing-settingcontent-ms-within-pdf-files-distribute-flawedammyy-rat). After seeing it being used in the wild, I let MSRC know:

MSRC quickly let me know that they are going to ship a fix as quickly as possible…which is a complete 180 compared to the original report assessment:

Additionally, there was mention of “another email on a different MSRC case thread”. That definitely piqued my interest. A few days later, I got a strange email with a different case number than the one originally assigned:

At this point, my jaw was on the floor. After sending some additional information to a closed MSRC case, the bug went from a “won’t fix” to “we are going to ship a fix as quickly as possible, and award you a bounty, too”. After some minor logistic exchanges with the Microsoft Bounty team, I saw that CVE-2018-8414 landed a spot on cve.mitre.org. This was incredibly interesting given less than a month ago, the issue was sitting as a “won’t fix”. So, I asked MSRC about it:

This is when I quickly found out that CVE-2018-8414 was being issued for the .SettingContent-ms RCE bug:

This is where the process gets cool. Previously, I disclosed a bug. That bug was given a “won’t fix” status. So, I blogged about it (https://posts.specterops.io/the-tale-of-settingcontent-ms-files-f1ea253e4d39). I then found out it had been used to exploit 2 browsers, and it was being used in the wild. Instead of letting things sit, I was proactive with MSRC and let them know about all of this. Once the August patch Tuesday came around, I received this email:

Yay!!! So Microsoft took a “Won’t Fix” bug and reassessed it based on new information I had provided once the technique was public. After a few more days and some logistical emails with Microsoft, I received this:

I have to give it to Microsoft for making things right. This bug report went from “won’t fix” to a CVE, public acknowledgement and a $15,000 bounty pretty quickly.

As someone who likes to critique myself, I can’t help but acknowledge that the original report was mostly focused on Office 2016 OLE and Windows Defender ASR, neither of which are serviceable bugs (though, RCE was mentioned). How could I have done better, and what did I learn?

If you have a bug, demonstrate the most damage it can do. I can’t place all the fault on myself, though. While I may have communicated the *context* of the bug incorrectly, MSRC’s triage and product teams should have caught the implications in the original report, especially since I mentioned “which enables code-execution from the internet with no security warnings for the user”.

This brings me to my next point. We are all human beings. I made a mistake in not clearly communicating the impact/context of my bug. MSRC made a mistake in the assessment of the bug. It happens.

Here are some key points I learned during this process:

  1. Vendors are people. Try to do right by them, and hopefully they try to do right by you. MSRC gave me a CVE, an acknowledgement and a $15,000 bounty for a bug which ended up being actively exploited before being fixed
  2. Vendors: PLEASE COMMUNICATE TO YOUR RESEARCHERS. This is the largest issue I have with vulnerability disclosure. This doesn’t just apply to Microsoft, this applies to every vendor. If you leave a researcher in the dark, without any sort of proactive response (or an actual response), your bugs will end up in the last place you want them.
  3. If you think your bug was misdiagnosed, see it through by following up and stating your case. Can any additional information be provided that might be useful? If you get a bug that is issued a “won’t fix”, and then you see it being exploited left and right, let the vendor know. This information could change the game for both you and their customers.

Vulnerability disclosure is, and will continue to be, a hard problem. Why? Because there are vendors out there that will not do right by their researchers. I am sitting on 0days in products due to a hostile relationship with “VendorX” (not Microsoft, to be clear). I also send literally anything I think might remotely resemble a bug to other vendors, because they do right by me.

At the end of the day, treat people the way you would like to be treated. This applied to both the vendors and the researchers. We are all in this to make things better. Stop adding roadblocks.

Timeline:

Feb 16, 2018 at 2:37 PM EDT: Report submitted to [email protected]
Feb 16, 2018 at 4:34 PM EDT: MSRC acknowledged the report and opened a case
March 2, 2018 at 12:27 PM EDT: MSRC responded noting they could reproduce the issue
April 24, 2018 at 4:06 PM EDT: Requested an update on the case
April 25, 2018 at 12:42 PM EDT: Case was reassigned to another case handler.
June 1, 2018 at 1:29 PM EDT: Asked new case handler for a case update
June 4, 2018 at 10:29 AM EDT: Informed the issue was below the bar for servicing; case closed.
July 11, 2018: Issue is publicly disclosed via a blog post
June 14, 2018 at 9:44 AM EDT: Sent MSRC a follow up after hearing of 2 browser bugs using the bug
June 14, 2018 at 11:05 AM EDT: Case was updated with new information
June 26, 2018 at 12:17 PM EDT: informed MSRC of mozilla CVE (CVE-2018-12368)
June 26, 2018 at 1:15 PM EDT: MSRC passed the mozilla CVE to the product team
July 3, 2018 at 9:52 PM EDT: Case was reassigned to another case handler
Jul 23, 2018 at 4:49 PM EDT: Let MSRC know .settingcontent-ms was being abused in the wild.
Jul 27, 2018 at 7:47 PM EDT: MSRC informed me they are shipping a fix ASAP
Jul 27, 2018 at 7:55 PM EDT: MSRC informed me of bounty qualification
Aug 6, 2018 at 3:39 PM EDT: Asked MSRC if CVE-2018-8414 was related to the case
Aug 6, 2018 at 4:23 PM EDT: MSRC confirmed CVE-2018-8414 was assigned to the case
Aug 14, 2018: Patch pushed out to the public
Sept 28, 2018 at 4:36 PM EDT: $15,000 bounty awarded

Before publishing this blog post, I asked MSRC to review it and offer any comments they may have. They asked that I include on official response statement from them, which you can find below:

Cheers,
Matt N.

CVE-2018-8212: Device Guard/CLM bypass using MSFT_ScriptResource

Device Guard and the enlightened scripting environments that come with it are a lethal combination for disrupting attacker activity. Device Guard will prevent unapproved code from executing while placing scripting languages such as PowerShell and the Windows Scripting Host in a locked down state. In order to operate in such an environment, researching bypasses can be immensely useful. Additionally, there are evasion advantages that can come with executing unsigned code via signed/approved scripts or programs.

When hunting for Constrained Language Mode (CLM) bypasses in the context of Device Guard, investigating Microsoft-signed PowerShell modules for calls that allow arbitrary, unsigned code to be executed is always a fruitful endeavor as most Microsoft PowerShell modules will be signed (i.e. implicitly approved per policy). To combat abusing signed PowerShell modules to circumvent CLM, Microsoft added a check to make sure a module can only execute exported functions if the module is loaded in CLM (CVE-2017-8715). This means that, while a script may be signed and allowed per policy, that script can only execute functions that are explicitly exported via Export-ModuleMember. This addition significantly reduces the attack surface for signed PowerShell modules as non-exported functions will be subject to CLM, the same as unsigned code.

While this addition reduces the attack surface, it doesn’t remove it entirely. While analyzing Microsoft-signed PowerShell module files for functions that allowed unsigned code-execution, “MSFT_ScriptResource.psm1” from the Desired State Configuration (DSC) module cropped up. This module is signed by Microsoft, and has a function called “Get-TargetResource” that takes a “GetScript” parameter:

Looking at this function, the code passed via -GetScript is added to a new scriptblock via [ScriptBlock]::Create(). After doing so, it passes the psboundparameters to the function “ScriptExecutionHelper”.

If we take a look at “ScriptExecutionHelper”, all it does is take the psboundparameters (which includes our newly created ScriptBlock) and execute it via the call operator (&):

Since all of this is happening within a Microsoft signed module, the module is permitted to run in FullLanguage mode (i.e. without any restrictions imposed upon it). To abuse this, all we need to do is pass our malicious PowerShell code to Get-TargetResource via the -GetScript parameter. But, isn’t the Export-ModuleMember mitigation from CVE-2017-8715 supposed to prevent function abuse? Looking at the exported functions in “MSFT_ScriptResource.psm1”, the abusable function “Get-TargetResource” is actually exported for us:

Excellent! To test this out, we can add some arbitrary C# code (that simply takes the Square Root of 4) to a PowerShell variable called $code:

After doing so, we just need to import the “MSFT_ScriptResource” PowerShell module and call “Get-TargetResource” with “Add-Type -TypeDefinition $code” as the -GetScript parameter. When this executes, the Microsoft signed PowerShell module will be loaded in FullLanguage mode (since it is signed and permitted via the Device Guard policy), and the code passed to the Get-TargetResource function will thus be executed in FullLanguage mode as well:

As you can see above, we are running in ConstrainedLanguage mode and getting the square root of 4 fails as those method calls are blocked. We then add our “malicious” code to the $code variable. All this code does is take the SquareRoot of 4, like we previously tried to do. Once that is done, the “MSFT_ScriptResource” module is imported and our “malicious” code is passed to “Get-TargetResource” via the -GetScript parameter. When that executes, the Add-Type call is executed and our “malicious” code is executed, thus circumventing CLM on Device Guard. It should be noted that enabling ScriptBlock logging will still capture the CLM bypass attempt.

This bug was fixed via CVE-2018-8212. If you are interested, Microsoft recently added bypasses like this to the WDAC Application Security bounty program: https://www.microsoft.com/en-us/msrc/windows-security-servicing-criteria

 

Cheers,

Matt N.

❌