Reading view

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

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.

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.

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.

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

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

 

 

 

 

 

 

 

 

 

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.

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

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-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.

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

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.

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

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

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-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

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

💾

Windows Server 2008R2-2019 NetMan DLL Hijacking

What if I told you that all editions of Windows Server, from 2008R2 to 2019, are prone to a DLL Hijacking in the %PATH% directories? What if I also told you that the impacted service runs as NT AUTHORITY\SYSTEM and that the DLL loading can be triggered by a normal user, on demand, and without the need of a machine reboot? Provided that you found some %PATH% directories configured with weak permissions, this would probably be the most straightforward privilege escalation technique I know. I don’t know why there hasn’t been any publication about this yet. Anyway, I’ll try to fill this gap.

Foreword

To start things off, I probably don’t need to clarify this but DLL hijacking is not considered as a vulnerability by Microsoft (source). I tend to agree with this statement because, by default, even if a DLL is loaded from the %PATH% directories by a process running with higher privileges, this behavior cannot be exploited by a normal user. Though in practice, and especially in corporate environments, it’s quite common to see third-party applications configured with weak folder permissions. In addition, if they add themselves to the system’s %PATH%, the entire system is then put at risk. My personal opinion on the subject is that Microsoft should prevent these uncontrolled DLL loadings as far as possible in order to prevent a minor configuration issue affecting a single application from becoming a privilege escalation attack vector with a way higher impact.

Back to Basics: Searching for DLL Hijacking Using Procmon

This discovery is the unexpected result of some research I was doing on Windows 2008 R2. Although the system is no longer supported, it’s still widespread in corporate networks and, I was looking for the easiest way of exploiting binary planting through my CVE-2020-0668 exploit. I’ve done a lot of research on Windows 10 Worsktation during the past few months and working back on Windows 7/2008 R2 required me to forget about some techniques I’ve learned and to restart from the beginning. My original problem was: how to easily exploit arbitrary files writes on Windows 2008 R2?

My first instinct was to start with the IKEEXT service. On a default installation of Windows 2008 R2, this service is stopped, and it tries to load the missing wlbsctrl.dll library whenever it’s started. A normal user can easily trigger this service simply by attempting to initiate a dummy VPN connection. However, starting it only once affects its start mode, it goes from DEMAND_START to AUTOMATIC. Leveraging this service under such circumstances would therefore require a machine reboot, which makes it a far less interesting target. So, I had to look for other ways. I also considered the different DLL hijacking opportunities documented by Frédéric Bourla in his article entitled “A few binary plating 0-days for Windows” but they are either not easy to trigger or appear quite randomly.

I decided to begin my research process with firing up Process Monitor and checking for DLL loading events failing with a NAME NOT FOUND error. In the context of an arbitrary file write exploit, the research doesn’t have to be limited to the %PATH% folders so this yields a lot of results! To refine the research, I therefore added a constraint. I wanted to filter out processes which try to load a DLL from the C:\Windows\System32\ folder and then find it in another Windows folder, especially if they need it to function properly. The objective is to avoid a Denial of Service as far as possible.

I considered 3 DLL hijacking cases:

  • A program loads a DLL which doesn’t exist in C:\Windows\System32\ but exists in another Windows directory, C:\Windows\System\ for example. Since the C:\Windows\System32\ folder has a higher priority, this could be a valid candidate.
  • A program loads a non-existing DLL but uses a safe DLL search order. Therefore, it only tries to load it from the C:\Windows\System32\ folder for example.
  • A program loads a non-existing DLL and uses an unrestricted DLL search order.

The first case might lead to Denial of Service so I left it aside. The second case is interesting but can be a bit difficult to spot amongst all the results returned by Procmon. The third case is definetly the most interesting one. If the DLL doesn’t exist, the risk of causing a Denial of Service when hijacking it is reduced and it’s also easy to spot in Procmon.

To do so, I didn’t add a new filter in Process Monitor. Instead, I simply added a rule which highlights all the paths containing WindowsPowerShell. Why this particular keyword, you may ask. On all (modern) versions of Windows, C:\Windows\System32\WindowsPowerShell\v1.0\ is part of the default %PATH% folders. Therefore, whenever you see a program trying to load a DLL from this folder, it most probably means that it is prone to DLL Hijacking.

I then tried to start/stop each service or scheduled task I could. And, after having spent a few hours staring at Procmon’s output, I finally saw this:

Wait, what?! Is this really what I think it is? :astonished: Is this a non-existing DLL being loaded by a service running as NT AUTHORITY\SYSTEM? My first thought was: “if wlanhlp.dll is a hijackable DLL, I should already know about it, I must have made a mistake somewhere or I must have installed some third-party app causing this”. But then I remembered. Firstly, I’m using a fresh install of Windows Server 2008 R2 in a dedicated VM. The only third party application is “VMware Tools”. Secondly, all the research I’ve done so far was mostly on Worstation editions of Windows because it’s often more convenient. Could it be the reason why I saw this event only now?

Fortunately, I have another VM with Windows 7 installed so I quickly checked. It turns out that this DLL exists on a Workstation edition!

If you think about it, if wlanhlp.dll is really related to Wlan capabilities as its name implies, it would make sense. The Wlan API is only available on Workstation editions by default and must be installed as an additional component on Server editions. Anyway, I must be on to something…

NetMan and the Missing Wlan API

Let’s start by looking at the properties of the event in Procmon and learn more about the service.

The process runs as NT AUTHORITY\SYSTEM, that’s some good news for us. It has the PID 972 so let’s check the corresponding service in the Task Manager.

Three services run inside this process. Looking at the Stack Trace of the event in Procmon, we should be able to determine the name of the one which tried to load this DLL.

We can see an occurrence of netman.dll so the corresponding service must be NetMan (a.k.a. Network Connections). That’s one problem solved. If we take a closer look at this Stack Trace, we also notice several lines containing references to RPCRT4.dll or ole32.dll. That’s a good sign. It means that this event was most probably triggered through RPC/COM. If so, there is a chance we can also trigger this event as a normal user with a few lines of code but I’m getting ahead of myself.

This DLL hijacking opportunity is due to the fact that the Wlan API is not installed by default on a server edition of Windows 6.1 (7 / 2008 R2). The question is: does the same principle apply to other versions of Windows? :thinking:

Luckily, I use quite a lot of virtual machines for my research and I had instances of Windows Server 2012 R2 and 2019 already set up so it didn’t take long to verify.

On Windows Server 2012 R2, wlanhlp.dll doesn’t show up in Procmon. However wlanapi.dll does instead. Looking at the details of the event, it turns out that it is identical. This means that Windows 6.3 (8.1 / 2012 R2) is also “affected”.

Ok, this version of Windows is pretty old now, Windows 2019 cannot be affected by the same issue, right? Let’s check this out…

The exact same behavior occurs on Windows Server 2019 as well! :smirk: I ended up checking this on all possible versions of Windows Server from 2008 to 2019. I won’t bore you with the details, all the versions are prone to this DLL hijacking. The only one which I couldn’t test thoroughly was Server 2008, I wasn’t able to reproduce the issue on this one.

How to Trigger this DLL Hijacking Event on Demand?

Let’s summarize the situation. On all versions of Windows Server, the NetMan service, which runs as NT AUTHORITY\SYSTEM, tries to load the missing wlanhlp.dll or wlanapi.dll DLL without using a safe DLL search order. Therefore it ends up trying to load this DLL from the directories which are listed in the system’s %PATH% environement variable. That’s a great start I’d say! :slightly_smiling_face:

The next step is to figure out if we can trigger this event as a normal user. I already mentionned that this behavior was due to some RPC/COM events but it doesn’t necessarily mean that we can trigger it. This event could also be the result of two services communicating with each other through RPC.

Anyway, let’s hope for the best and start by checking the Stack Trace once again but, this time, using an instance of Procmon configured to use the public symbols provided by Microsoft. To do so, I switched to the Windows 10 VM I use for security research.

We can see that the CLanConnection::GetProperties() method is called here. In other events, the CLanConnection::GetPropertiesEx() method is called instead. Let’s see if we can find these methods by inspecting the COM objects exposed by NetMan using OleViewDotNet.

Simply based on the name of the class, the LAN Connection Class seems like a good candidate. So, I created an instance of this class and checked the details of the INetConnection interface.

Here it is! We can see the CLanConnection::GetProperties() method. We’re getting close! :ok_hand:

At this point, I was thinking that all of this looked too good to be true. First, I saw this DLL hijacking which I had never seen before. Then, I saw that it was triggered by an RPC/COM event. Finally, finding it with OleViewDotNet was trivial. There had to be a catch! Though, only one problem could arise now: restrictive permissions on the COM object.

COM objects are securable too and they have ACLs which define who is allowed to use them. So, we need to check this before going any further.

When I first saw Administrators and NT AUTHORITY\..., I thought for a second, “crap, this can only be triggered by high-privileged accounts”. And then I saw NT AUTHORITY\INTERACTIVE, phew… :sweat_smile:

What this actually means is that this COM object can be used by normal users only if they are authenticated using an interactive session. More specifically, you’d need to logon locally on the server. Not very useful, right?! Well, it turns out that when you connect through RDP (this includes VDI), you get an interactive session as well so, under these circumstances, this COM object could be used by a normal user. Otherwise, if you tried to use it in a WinRM session for example, you’d get an “Access denied” error. That’s not as good as I expected initially but that’s still a seemingly interesting trigger.

The below screenshot shows a command prompt opened in an RDP session on Windows Server 2019.

At this point, the research part is over so let’s write some code! Fortunately, the INetConnection interface is documented (here). This makes things a lot easier. Secondly, while searching how to enumerate the network interfaces with INetConnection->EnumConnections(), I stumbled upon an interesting solution posted by Simon Mourier on StackOverflow here. Yes, I copied some code from StackOverflow, that’s a bit lame, I know… :neutral_face:

Here is my final Proof-of-Concept code:

// https://stackoverflow.com/questions/5917304/how-do-i-detect-a-disabled-network-interface-connection-from-a-windows-applicati/5942359#5942359

#include <iostream>
#include <comdef.h>
#include <netcon.h>

int main()
{
    HRESULT hResult;

    typedef void(__stdcall* LPNcFreeNetconProperties)(NETCON_PROPERTIES* pProps);
    HMODULE hModule = LoadLibrary(L"netshell.dll");
    if (hModule == NULL) { return 1; }
    LPNcFreeNetconProperties NcFreeNetconProperties = (LPNcFreeNetconProperties)GetProcAddress(hModule, "NcFreeNetconProperties");

    hResult = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (SUCCEEDED(hResult))
    {
        INetConnectionManager* pConnectionManager = 0;
        hResult = CoCreateInstance(CLSID_ConnectionManager, 0, CLSCTX_ALL, __uuidof(INetConnectionManager), (void**)&pConnectionManager);
        if (SUCCEEDED(hResult))
        {
            IEnumNetConnection* pEnumConnection = 0;
            hResult = pConnectionManager->EnumConnections(NCME_DEFAULT, &pEnumConnection);
            if (SUCCEEDED(hResult))
            {
                INetConnection* pConnection = 0;
                ULONG count;
                while (pEnumConnection->Next(1, &pConnection, &count) == S_OK)
                {
                    NETCON_PROPERTIES* pConnectionProperties = 0;
                    hResult = pConnection->GetProperties(&pConnectionProperties);
                    if (SUCCEEDED(hResult))
                    {
                        wprintf(L"Interface: %ls\n", pConnectionProperties->pszwName);
                        NcFreeNetconProperties(pConnectionProperties);
                    }
                    else
                        wprintf(L"[-] INetConnection::GetProperties() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
                    pConnection->Release();
                }
                pEnumConnection->Release();
            }
            else
                wprintf(L"[-] IEnumNetConnection::EnumConnections() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
            pConnectionManager->Release();
        }
        else
            wprintf(L"[-] CoCreateInstance() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
        CoUninitialize();
    }
    else
        wprintf(L"[-] CoInitializeEx() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
    
    FreeLibrary(hModule);
    wprintf(L"Done\n");
}

The below screenshot shows the final result on Windows Server 2008 R2. As we can see, we can trigger the DLL loading simply by enumerating the Ethernet interfaces of the machine. No need to say that the machine must have at least one Ethernet interface, otherwise this technique doesn’t work. :smile:

The screenshot below shows an attempt to run the same executable as a normal user connected through a remote PowerShell session on Windows Server 2019.

(2020-04-13 update) Dealing with the INTERACTIVE restriction

A couple days after the publication of this blog post, @splinter_code brought to my attention that it was technically possible to spawn an interactive process from a non-interactive one.

Then, I had the chance to exchange a few words with him. It turns out that he developped a tool called RunasCs which implements among other things a generic way for spawning an interactive process. He also took the time to explain to me how it works. This trick involves some Windows internals subtleties which are not commonly well known. I won’t detail the technique here because it would require a dedicated blog post in order to explain everything clearly but I’ll try to give a high-level explanation. I hope we will see a blog post from the author himself soon! :slightly_smiling_face:

To put it simple, you can call CreateProcessWithLogon() in order to create an interactive process. This function requires the name and the password of the target user. The problem is that if you try to do that from a process running in session 0 (where most of the services live), the child process will immediately die. A typical example is when you connect remotely through WinRM. All your commands are executed through a subprocess running in session 0 with your identity.

Why is it a problem? You may ask. The thing is, an interactive process is called this way because it interacts with a desktop, which is a particular securable object in the Windows world. However, in the case of our WinRM process which runs in session 0, you wouldn’t (and you shouldn’t) be allowed to interact with this desktop. What @splinter_code found is that you can edit the ACL of the desktop object in the context of the current process in order to grant the current user access to this object. Child processes will then inherit these permissions and therefore have a desktop to interact with. Really clever!

As you can see on the below screenshot, using this trick, we can spawn an interactive process and therefore run NetManTrigger.exe as if we were logged in locally. :slightly_smiling_face:

Conclusion

Following this analysis, I can say that the NetMan service is probably the most useful target for DLL Hijacking I know about. It comes with a small caveat though. As a normal user you would need an interactive session (RDP / VDI), which makes it quite useless if you’re logged on through a remote PowerShell session for instance. But there is another interesting case, if you’ve compromised another service running as LOCAL SERVICE or NETWORK SERVICE, then you would still be able to trigger the NetMan service to elevate your privileges to SYSTEM.

With this discovery, I also learned a lesson. Focusing your attention and your research on a particular environment may sometimes prevent you from finding interesting stuff, which turns out to be particularly relevant in the context of a pentest.

Last but not least, I integrated this in my Windows Privilege Escalation Check script - PrivescCheck. Depending on the version of Windows, the Invoke-HijackableDllsCheck function will tell you which DLL may potentially be hijacked through the %PATH% directories. Thanks @1mm0rt41 for suggesting the idea! :thumbsup:

Links & Resources

Windows DLL Hijacking (Hopefully) Clarified

Whenever a “new” DLL hijacking / planting trick is posted on Twitter, it generates a lot of comments. “It’s not a vulnerability!” or “There is a lot of hijackable DLLs on Windows…” are the most common reactions. Though, people often don’t really speak about the same thing, hence the overall confusion which leads us nowhere. I don’t pretend to know the ultimate truth but I felt the need to write this post in order to hopefully clarify some points.

Introduction

Whenever I write about something that involves DLL hijacking (e.g.: NetMan DLL Hijacking), I assume that it’s common knowledge and that we are all on the same page. It turns out that it’s a big mistake, for multiple reasons! First, DLL hijacking is just a core concept and, in practice, there are some variants. Therefore, whether you are a pentester, a security researcher or a system administrator, your own conception of it may differ from someone else’s. And then, there is this recurring debate: is it a vulnerability? Before giving a factual answer to this question, I’ll first remind what DLL hijacking is about. Then I’ll illustrate two of its variants with real-life examples, depending on what you are trying to achieve. Finally, I’ll try to give some insight into how you can lower the risk of DLL hijacking.

DLL Hijacking: What are we talking about?

Dynamically compiled Win32 executables use functions which are exported by built-in or third-party Dynamic Link Libraries (DLL). There are two main ways to achieve this:

  • At link time - When the program is compiled, an import table is written into the headers of the Portable Executable (PE). To put it simple, it keeps track of which function needs to be imported from which DLL. Therefore, whenever the program is executed, the linker knows what to do and loads all the required libraries transparently on your behalf.
  • At runtime - Sometimes, you need to or want to import a library at runtime. At this point, the linker has already done its part of the job, so if you want to do so you’ll have to take care of a few things yourself. In particular, you can call LoadLibrary() or LoadLibraryEx() from the Windows API.

Note: in this post, I’ll consider only Win32 applications. Although they use the same extension, DLLs in the context of .NET applications have a completely different meaning so I won’t talk about them here. I don’t want to add to the confusion.

According to the documentation, the prototype of these two functions is as follows:

HMODULE LoadLibrary(LPCSTR lpLibFileName);
HMODULE LoadLibraryEx(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);

The main argument - lpLibFileName - is the path of the library file you want to load. Though, evaluating the full path of the file at runtime requires some work that we are not always willing to do, especially when the system can retrieve this path by itself. For example, instead of writing LoadLibrary("C:\Windows\System32\mylib.dll"), you could just write LoadLibrary("mylib.dll") and thus let the system find the DLL. This approach makes a lot of sense for third-party applications because they don’t necessarily know this path beforehand.

But then, if you don’t specify the full path of the library you want to load, how does the system know where to find it? The answer is simple, it uses a predefined search order, which is illustrated on the following diagram.

The locations in the “pre-search” are highlighted in green because they are safe (from a privilege escalation perspective). If the name of the DLL doesn’t correspond to a DLL which is already loaded in memory or if it’s not a known DLL, the actual search begins. The program will first try to load it from the application’s directory. If it succeeds, the search stops there otherwise it continues with the C:\Windows\System32 folder and so on…

Note: in this context, the term “Known DLL” has a very specific meaning. These DLLs are listed in the HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs registry key and are guaranteed to be loaded from the System folder.

I won’t bore you with the theory. Rather, I’ll illustrate this search order with some examples based on the following source code. The following program uses the first command line argument as the name of a library to load with LoadLibrary().

HMODULE hModule = LoadLibrary(argv[1]);
if (hModule) {
    wprintf(L"LoadLibrary() OK\n");
    FreeLibrary(hModule);
} else {
    wprintf(L"LoadLibrary() KO - Error: %d\n", GetLastError());
}

Scenario 1: loading a DLL which exists in the application’s directory.

The program finds the DLL in its directory C:\MyCustomApp, that’s the first location in the search order so the library is loaded successfully. Everything is fine. :ok_hand:

Scenario 2: loading a Windows DLL, dbghelp.dll for example.

The program first tries to load the DLL from C:\MyCustomApp, the application’s directory, and doesn’t find it there. Therefore, it tries to load it from the system directory C:\Windows\System32, where this library is actually located.

We can see a potential issue here. What if the C:\MyCustomApp directory is configured with incorrect permissions and allows any user to add files? You guessed it, a malicious version of the DLL could be planted in this directory, allowing a local attacker to execute arbitrary code in the context of any other user who would run this application. Although that’s DLL search order hijacking, this first variant is also sometimes rightly or wrongly called DLL Sideloading. It’s mostly used by malwares but it can also be used for privilege escalation (see my article about DLL Proxying).

Note: in theory DLL Sideloading has a specific meaning. According to MITRE: “Side-loading vulnerabilities specifically occur when Windows Side-by-Side (WinSxS) manifests are not explicit enough about characteristics of the DLL to be loaded. Adversaries may take advantage of a legitimate program that is vulnerable to side-loading to load a malicious DLL.

Scenario 3: loading a nonexistent DLL

If the target DLL doesn’t exist, the program continues its search in the other Windows directories. If it can’t find it there, it tries to load it from the current directory. If it still can’t find it, it eventually searches for it in all the directories that are listed in the %PATH% environment variable.

We can see that a lot of DLL hijacking opportunities arise there. If any of the %PATH% directories is writable, then a malicious version of the DLL could be planted and would be loaded by the application whenever it’s executed. This is another variant which is sometimes called Ghost DLL injection or Phantom DLL hijacking.

Scenario 4: loading a nonexistent DLL as NT AUTHORITY\SYSTEM

With this last scenario, we are slowly but surely approaching the objective. In the previous examples, I ran the executable as a low-privileged user so that’s not representative of a privilege escalation scenario. Let’s remediate this and run the last command as NT AUTHORITY\SYSTEM this time.

The exact same search order applies to NT AUTHORITY\SYSTEM as well and that’s completely normal. There is a slight difference though. The last directory in the search is different. With the low-privileged user it was C:\Users\Lab-User\AppData\Local\Microsoft\WindowsApps whereas it’s now C:\WINDOWS\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps. This difference is due to a per-user path that was added starting with Windows 10: %USERPROFILE%\AppData\Local\Microsoft\WindowsApps, where %USERPROFILE% resolves to the path of the user’s home folder.

Anyway, by default, all these folders are configured with proper permissions. So, low-privileged users wouldn’t be able to plant a malicious DLL, preventing them from hijacking the execution flow of a service running as NT AUTHORITY\SYSTEM for example. With this demonstration, I hope that it’s now clear why DLL hijacking is not a vulnerability.

OK, if DLL hijacking isn’t a vulnerability, why all this fuss? :confused:

Well, as I said before, DLL hijacking is just a core concept, an exploitation technique if you will. It’s just a means to an end. The end goal is either local privilege escalation or persistence (or even AV evasion) in most cases. Though, the means may differ a lot depending on your perspective. Based on my own experience, I know that this perspective generally differs between pentesters and security researchers, hence the potential confusion. So, I’ll highlight two real-life examples in the next parts.

DLL Hijacking From a Security Researcher’s Perspective

First of all, as a Windows bug hunter, if you want to find privilege escalation vulnerabilities on the operating system itself, you’ll often want to start from a blank page, with a clean installation of Windows. The objective is to prevent side-effects that could be caused by the installation of third-party applications. That’s already a big difference between a researcher and a pentester.

Previously, I said that a default installation of Windows is not vulnerable to DLL hijacking because all the directories that are used in the DLL search are configured with proper permissions so, how this technique can still be useful?

It turns out this technique comes in very handy when it comes to privileged file operations abuse for example, especially arbitrary file write. Let’s say that you found a vulnerability in a service that allows you to move any file you own to any location on the filesystem in the context of NT AUTHORITY\SYSTEM. That’s cool but that’s somewhat limited. What you really want to achieve is arbitrary code execution as NT AUTHORITY\SYSTEM. At this point, DLL hijacking is the missing piece that completes the puzzle.

An arbitrary file write vulnerability opens up many opportunities for DLL hijacking because you are not limited to the %PATH% directories (scenario #3), you could also consider hijacking a DLL in an application’s directory (scenario #2) or even in C:\Winows\System32 if it doesn’t exist there. Both DLL Sideloading and Phantom DLL Hijacking techniques can then be used.

If you search for DLL Sideloading opportunities using Process Monitor on a default installation of Windows, you’ll find a lot of them. Typically, any program which is not installed in C:\Windows\System32 and tries to load a DLL from this folder without specifying its full path will fall into this category.

Enough with the theory, let’s take a real-life example! On the below screenshot, you can see that the WMI service loads the wbemcomn.dll library on startup:

The first result is NAME NOT FOUND. That’s totally normal because wbemcomn.dll is a system library, its actual location is C:\Windows\System32\wbemcomn.dll. Though wmiprvse.exe tries to load it from C:\Windows\System32\wbem because this is the directory where it is installed.

Therefore, provided that you found an arbitary file write vulnerability, you could plant a malicious version of wbemcomn.dll in C:\Windows\System32\wbem. After a machine reboot, your DLL would be loaded by the service as NT AUTHORITY\SYSTEM. Though in practice you wouldn’t rely on this particular DLL hijacking opportunity in your exploit for two major reasons:

  • A reboot is required - Let’s say you found a vulnerability that allows you to move a file to an arbitrary location as SYSTEM. Ending you exploit chain with a machine reboot after having successfully planted your DLL would be a shame. You’d rather search for a DLL hijacking you can trigger on demand as a normal user.
  • Denial of Service - Let’s say that you finally decided to plant your DLL in the wbem folder because you didn’t find a better candidate. After a machine reboot, your DLL is properly loaded by the service and you get your arbitary code execution as SYSTEM. That’s cool but what about the service? Congratulations, you’ve just crashed it because it wasn’t able to import its required dependencies. Again that’s a shame. One could argue that you could craft a Proxy DLL in order to address this issue. Though in practice this would add to your exploit development workload so you want to avoid that as far as possible

This is only one example of DLL Sideloading. There is a ton of similar opportunities on a default installation of Windows. That’s why, security researchers often say that DLL hijacking on Windows is very common and widespread. From their perspective, they think of DLL hijacking in its entirety. However, with the two previous points in mind, you can see that it’s not that simple in the end. Although DLL hijacking is widespread, finding the perfect candidate for your exploit can easily become a headache. That’s why exploits such as the DiagHub technique by James Forshaw are very interesting. This specific technique is now patched but it met all the criteria back then:

  • It could be triggered by a normal user through RPC and you could even choose the name of the DLL you wanted to load. As long as it was in the System32 folder, it would be loaded by the service.
  • You could safely execute your own code without risking a service crash.
  • On top of that, you didn’t have to write your code in DllMain().

Microsoft finally prevented this exploit by enforcing code signing. In other words, only Microsoft-signed libraries can now be loaded using this trick. Later on, I found another technique that is not as good as this one but still meets almost all of the above criteria - Weaponizing Privileged File Writes with the USO Service, but I digress…

That’s it for DLL hijacking in the context of Windows security research. What about pentesters now?

DLL Hijacking From a Pentester’s Perspective

In the context of a pentest, the initial conditions are usually very different. You are given an environment to compromise and you have to adpat based on what you find along the way. Finding a 0-day vulnerability or leveraging the last privilege escalation exploit that was released publicly is usually the option of last resort. The first things you’re looking for are system misconfigurations. Based on my own experience, I’d say that it probably represents 80% of the job.

Security issues caused by misconfigurations are common in corporate environments. That is to some extent quite understandable because installing an operating system without any additional software is pretty useless. And sometimes, these third-party applications introduce vulnerabilities either because they are not installed correctly or they are themselves vulnerable.

Based on what I explained previously, I’ll discuss the two most common DLL hijacking scenarios you’ll face. Now for the setup, here is a common mistake I see very often in corporate environments: a third-party application is installed at the root of the main partition (C:\) or is installed on a seperate partition (D:\ for example).

If you don’t already know that, folders that are created at the root of a partition are granted permissive rights. They allow any “Authenticated User” to create files and folders in them. These permissions are then inherited by subdirectories by default. Therefore, if the program installer doesn’t take care of that or if the administrator doesn’t check them, there is a high chance that the application’s folder is vulnerable.

With this in mind, here are the two most common scenarios you’ll face:

  1. The program installer created a service which runs as NT AUTHORITY\SYSTEM and executes a program from this directory. In this example, we consider that the permissions of the executable itself are properly configured though. In this case, there is a high chance that it is vulnerable to DLL Sideloading. A local attacker could plant a Windows DLL that is used by this service in the application’s folder.
  2. The program installer added the application’s directory to the system’s %PATH%. This case is a bit different. You could still use DLL Sideloading in order to execute code in the context of any other user who would run this application but you could also achieve privilege escalation to SYSTEM. What you need in this case is Ghost DLL Hijacking because, as I explained before, a nonexistent DLL lookup will ultimately end up in the %PATH% directories.

From my experience, this second scenario is by far the most common one. So, assuming that you find yourself in such situation, what would you need? Well, you’d need to find a privileged process that tries to load a DLL from this unsecure folder. The most common place to look for this kind of opportunity is Windows services.

But then, what are the criteria for finding the perfect candidate? They can be summarized in these three points:

  • It tries to load a nonexistent DLL without specifying its full path.
  • It doesn’t use a safe DLL search order.
  • It runs as NT AUTHORITY\SYSTEM. Actually it’s not strictly required but I will consider only this case for simplicity. This particular subject will be discussed in an upcoming article. :wink:

On Windows 10 (workstation), services that match these criteria have almost disappeared. Therefore, I often say that DLL hijacking isn’t that common nowadays on Windows 10. That’s because when I think of it I refer to missing DLLs which are loaded from the %PATH% directories by services running as highly privileged account, which is only one variant of DLL hijacking. Nevertheless there are still a few of these services. One of them is the Task Scheduler, as explained in this blog post. This service tries to load the missing WptsExtensions.dll DLL upon startup.

As you can see on the above screenshot, the service tried to load this DLL from C:\MyCustomApp because this directory was added to the system’s %PATH%. Since this directory is configured with weak permissions, any local user can therefore plant a malicious version of this DLL and thus execute code in the context of this service after a machine reboot.

Note: once again, the %PATH% is an environment variable so it varies depending on the user profile. As a consequence, the %PATH% of the NT AUTHORITY\SYSTEM account is often different from the %PATH% of a typical user account.

Though, you have to be very careful with this particular DLL hijacking if you want to exploit it during a pentest. Indeed, when this DLL is loaded by the service, it’s not freed so you won’t be able to remove the file. One solution is to stop the service as soon as you get your SYSTEM shell, then remove the file and finally start the service again.

Note: starting/stopping the Task Scheduler service requires SYSTEM privileges.

This example applies to Windows 10 workstation but what about Windows servers? Well I won’t discuss this here because I already did that in my previous post: Windows Server 2008R2-2019 NetMan DLL Hijacking. On all versions of Windows Server, starting with 2008 R2, the NetMan service is prone to DLL hijacking in the %PATH% directories because of the missing WLAN API. So, if you find yourself in the situation I just described, you could trigger this service in order to load your malicious DLL as SYSTEM, very convenient.

How to prevent DLL Hijacking?

Hopefully, I made it clear that, whatever the situation, DLL hijacking isn’t a vulnerability. It’s just an exploitation technique for getting code execution in the context of an application or a service for example. An exploitation technique on its own is useless though, what you need is a vulnerability such as weak folder permissions or a privileged file operation abuse.

  • Weak folder permissions - This issue can be caused by the installation of a third-party application. The installer should take care of that but that’s not always the case so system administrators should pay extra attention to this issue.
  • Privileged file operation abuse - This issue is due to a flaw in the design of the application. In this case, developpers should review the code in order to prevent such operation on files and folders that can be controlled by normal users or implement impersonation when possible.

Now, let’s say that the permissions of the application’s folder are properly set and that your code is clean, but you want to go the extra mile. There are still a few things you can do in order to reduce the risk of DLL hijacking in the %PATH% directories.

You’ve probably noticed that I used the simple LoadLibrary() function in my example but I didn’t say anything about the second option: LoadLibraryEx(). As a reminder, here is its prototype:

HMODULE LoadLibraryEx(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);

The first parameter is still the name (or the path) of the DLL but there are two other arguments. According to the documentation, the second one - hFile - is reserved and should be set to NULL. The third argument, however, allows you to specify some flags that will affect the behavior of the function. In our case, the three most interesting flags are:

  • LOAD_LIBRARY_SEARCH_APPLICATION_DIR - If this value is used, the application’s installation directory is searched for the DLL and its dependencies. Directories in the standard search path are not searched.

Indeed, if this flag is used, the search is limited to C:\MyCustomApp.

  • LOAD_LIBRARY_SEARCH_SYSTEM32 - If this value is used, %windows%\system32 is searched for the DLL and its dependencies. Directories in the standard search path are not searched.

Indeed, if this flag is used, the search is limited to C:\Windows\System32.

  • LOAD_LIBRARY_SEARCH_USER_DIRS - If this value is used, directories added using the AddDllDirectory() or the SetDllDirectory() function are searched for the DLL and its dependencies.

Enough with the theory, let’s check a real-life example. :slightly_smiling_face:

You probably know or you’ve probably heard about the IKEEXT DLL hijacking, that was originally published here in 2012 as far as I can tell. Starting with Windows Vista and up to Windows 8, the IKEEXT service loaded the missing wlbsctrl.dll library upon startup without specifying its full path and without using a safe DLL search order. Here is what it looked like back then:

Of course, the researcher who initially reported this to Microsoft was given the same usual answer:

Microsoft has thoroughly investigated the claim and found that this is not a product vulnerability. In the scenario in question, the default security configuration of the system has been weakened by a third-party application. Customers who are concerned with this situation can remove the directory in question from PATH or restrict access to the third-party’s application directory to better protect themselves against these scenarios.

This is the official answer but then, starting with Windows 8.1, this DLL hijacking magically disappeared. Have you ever wondered how and why? Well, let me tell you that IKEEXT still tries to load this missing DLL, even in the latest version of Windows 10. But why don’t we talk about it anymore? First things first, here is what it looks like now on Windows 10:

See? The service tries to load the DLL from C:\Windows\System32, doesn’t find it and then stops. Do you recognize this behavior? :smirk: At this point, and based on what I’ve explained so far, you probably see where I’m going with this.

Let’s take a look at the two versions of the ikeext.dll file…

Of course, there is nothing magical about this. It turns out that Microsoft just silently patched this particular DLL hijacking by modifying the code of ikeext.dll. LoadLibraryEx() is now called instead of LoadLibrary() with the flag LOAD_LIBRARY_SEARCH_SYSTEM32, thus restricting the search to %windir%\System32.

LoadLibraryW(L"wlbsctrl.dll");                                          // Windows 7
LoadLibraryExW(L"wlbsctrl.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);    // Windows 10

What is the cost of this change: one line of code, yes ONLY ONE LINE OF CODE!!! :expressionless:

With that in mind, I want you to think about a particular communication from Microsoft Security Response Center (MSRC). In a blog post, entitled Triaging a DLL planting vulnerability, they explicitly define what is considered a vulnerability and what is not:

Did you read that? Microsoft won’t address DLL hijacking scenarios involving %PATH% directories. I’ll let you draw your own conclusions from this… :roll_eyes:

Conclusion

In the end, DLL hijacking (in the %PATH% directories) is not a vulnerability. It’s what Microsoft keeps replying over and over again to people who report them. OK, we get that and now what?

In this post, I discussed two versions of this problem:

  • DLL sideloading - If the permissions of an application’s folder are not properly configured, that’s the responsibility of this application only and, most of the time, the impact is limited to this application. So, there’s nothing special to say about it.
  • DLL hijacking in the %PATH% directories - Again, if the permissions of an application’s folder are not properly configured, that’s the responsibility of this application. However, if it adds itself to the system’s %PATH%, that’s another story. In this case, the entire system is put at risk. Any Windows service that attempts to load a missing DLL without using a secure DLL search order can then be leveraged for privilege escalation. Is this a normal situation? I don’t think so.

When you know that this second scenario can easily be prevented simply by changing one line of code, I find it really hard to accept Microsoft’s answer to this issue. It’s even harder to accept considering that they patch them silently in the end. Unfortunately, I know that there are some people who keep relaying Microsoft’s argument blindly. The problem is that this leads us nowhere. Do we want to improve security or do we just want to spend our time figuring out who’s responsible for what?

In my opinion, a honest and constructive reply to people who report these issues would be something like: “Thank you for your report, we don’t consider this a critical or important security issue but we will address this in a future public release”. Perhaps I’m a bit naive and my point of view is biased because I don’t have the big picture. I don’t know. Anyway, I’ll conclude this post with an approximate translation of a quote from a French humorist: “If you’re absolutely one hundred percent sure about something, there’s a high chance you are wrong.

Links & Resources

Exploiting Feedback Hub in Windows 10

Feedback Hub is  a feature in Windows 10 which allows users to report problems or suggestions to Microsoft. It relies ond he “diagtrack” service, running as SYSTEM, or better known as “Connected User Experiences and Telemetry”

When the Feedback Hub gathers info in order to send them to MS, it does a lot of file operations, most of them performed by the SYSTEM user. It turns out that this application and the related services/executables which are run during the collection have a lot of logical bugs which can be exploited by “Directory Junctions” or Symbolic links via RPC Control.

These “bugs” could permit a malicious user to perform the following operations:

  • Arbitrary File Read (Information Disclosure)
  • Arbitrary File Overwite with contents not controlled by the Attacker (Tampering)
  • Arbitrary File Overwite/Write with contents  controlled by the Attacker (Elevation of Privilege)
  • Arbitrary File/Folder Deletion (Elevation of Privilege)

In my investigations, I was able o perform all these operations in various circumstances.

Today I’m going to show you how it is possible to perform an Arbitrary File Overwite/Write which could easily lead to EoP.  I found this issue in Windows 10 Preview up to Build v10.0.19592.1001. In Windows 10 “standard” version, the bug was much easier to exploit.

This issue was fixed in an “unintended” way in CVE-2020-0942  and sequent in latest WIP Build (10.0.19608.1006).

Prerequisites

  • Standard Windows 10 / domain user with Windows 10 computer (virtual or physical)
  • Diagnostics & Feedback has to be set to “Full” mode
    • If “Basic” was selected at the first logon this can be changed by the logged on user in settings->privacy->diagnostics& feedback by switching from “required” to “optional”

Description

When an attachment is sent via the Feedback Hub App and you choose to “Save a local copy…”, several file operations are performed by the diagtrack service, mostly using SYSTEM user privileges.

cattura.JPG

 

Here is an overview of the most significant operations.

First, diagtrack service by impersonating the current user creates a temporary random folder name diagtracktempdir<XX..X> under the “c:\Users\user\Appdata\local\temp” directory:

cattura.JPG

During the creation of the directory, the impersonated user also sets new permissions. These are not inherited from the parent folder and are very restrictive. In fact, as we can see in the following screenshot, permissions in the current directory do not include the current user:

cattura.JPG

While, for the subdirectories and files inside, the current user has some privileges.

cattura.JPG

In the next screenshot we can observe that files and folders are created without user impersonation and therefore as SYSTEM. It should also be noticed that even the file uploaded as feedback attachment (windowscoredeviceinfo.dll in this case) is now copied in the temporary folder. Additionally, all the files and folders created or copied in the temporary path will inherit these new permissions.

cattura.JPG

Once the process is complete, diagtracktempdir<XX..X> is renamed and moved into the current user FeedbackHub path. Sequent, restrictive permissions are again set on first directory of the renamed folder:

cattura.JPG

 

So the question is:

Is it still possible to abuse from special crafted “junction”?

Theoretically yes: the primary folder diagtracktempdir<XX..X> is created by the current user. Even though the permissions are sequent changed in a more restrictive way, such as granting the user only READ permissions, he could still modify because the user is the owner of the directory.

Practically, there is a race condition to win. Specifically, permissions on diagtracktempdir<XX..X> have to be changed before the subdirectories are created by SYSTEM without impersonation. In this way, the new permissions will be propagated, and the attacker will have full access on all of content.

Winning such a race conditions is hard. The time between the two events is in the order of milliseconds and you have to inject your “malicious” code for altering the permissions…

cattura.JPG

 

Nevertheless, I found a couple  solutions to win the race conditions and developing a POC/tool in VS2019 C++

Note: In order to speed up the tests, always choose “Suggest a feature” in Feedback Hub.

Only 1 Hard disk present

I tested this procedure on both physical and virtual machines. Depending on the HW, performance and current workload, I was able to perform the exploitation at first run. In some cases, it took up to 10/15 attempts.

First of all, run the “Feedback Hub” app and exit. This will create the initial directory and settings if never launched before.

This is the “Logical Flow” of my Poc/Tool:

  • Thread 1: Run a file watcher for the directory: “c:\users\<user>\AppData\Local\Packages\Microsoft.WindowsFeedbackHub_8wekyb3d8bbwe\LocalState\DiagOutputDir”
    this will intercept the creation of a directory and save the directory {GUID} useful for later
  • Thread 2: Run a file watcher for the directory “c:\users\<user>\appdata\local\temp”
    this will intercept the creation of folder diagtracktempdir<XX..X>
    • When the directory is created, change immediately the permissions, e.g.: everyone:full.
      Note: SetSecurityInfo API function is not suitable because slow (it does a lot of useless work “under the hood”), NtSetSecurityObject is faster because more “atomic”
    • We know how the final path name will look like:
      “diagtracktempdir<XX..X>\get info T0FolderPath\<{GUID}>”
  • Loop to create a “junction point” from the identified path to our target directory. Loop until the error is “path/file not found”.

If everything works fine, we should have copied our file in the destination directory. Alternatively, the loop will exit with an access denied error which means that we were too late.

The following screenshots are the PoC of how I was able to write the previous attached WindowsCoreDeviceInfo.dll in c:\windows\system32:

cattura.JPG

 

cattura.JPG

The following screenshot shows that the SetSecurity executed by the exploit happened before the creation of the directory “get info T0FolderPath

cattura.JPG

And directory successfully mounted:

cattura.JPG

Finally, file copied in target directory and EoP is just one step away 😉cattura.JPG

Two or more hard disk/partitions are present (including the possibility to mount an external USB disk)

I tested this procedure on both physical and virtual machines. In my test environment (physical and virtual machine with 2 partitions) I was able to perform the exploitation at first run. This solution is much more reliable.

Mounting an external USB disk on a physical machine can be accomplished by a standard user

First of all, run the “Feedback Hub” app and exit. This will create the initial directory and settings if never launched before.

In this scenario, we will create a junction from the directory “c:\user\<user>\documents\feedbackhub” to a directory located on another drive. This will force “reparse” operations whenever a file is opened, and this introduces delays, especially if the junction points to a different drive/partition.

When a junction is in place, the user’s “…appdata\local\temp” directory is no more used and the diagtracktempdir<XX..X> directory is directly written under the feedbackhub.

The only prerequisite is that the feedbackhub folder has to be empty, which means that that no previous Feedback Hub with Local Saving Attachments have to be done, because once the folders and  files are created, the user cannot delete them.

The following steps are required to win the race condition:

  1. Create the junction: cattura.JPG
  2. Use the junction directory instead of “…appdata\local\temp” in the C++ exploit:cattura.JPG
  3. Submit a new feedback and load a malicious DLL as attachmentcattura.JPG

Et voilà! Our dll was copied in System32 folder:

cattura.JPG

 

Conclusions

This is just  one of the still many possibilities to perform privileged file operations by abusing the generic “error reporting” functionalities in Windows 10.

If you’re hunting for for CVE’s maybe this might be worth a try. All you need is Procmon, time and patience 😉

 

POC can be downloaded here

 

 

Cattura

decoderblogblog

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

cattura.JPG

PrintSpoofer - Abusing Impersonation Privileges on Windows 10 and Server 2019

Over the last few years, tools such as RottenPotato, RottenPotatoNG or Juicy Potato have made the exploitation of impersonation privileges on Windows very popular among the offensive security community. Though, recent changes to the operating system have intentionally or unintentionally reduced the power of these techniques on Windows 10 and Server 2016/2019. Today, I want to introduce a new tool that will allow pentesters to easily leverage these privileges again.

Foreword

Please note that I used the term “new tool” and not “new technique”. If you read this article in the hope of learning a new leet technique, you will be disappointed. In fact, I’m going to discuss two very well-known techniques that can be combined together in order to achieve privilege escalation from LOCAL SERVICE or NETWORK SERVICE to SYSTEM. To my knowledge, I think there hasn’t been any public mention about using this particular trick in this context but, of course, I might be wrong. :roll_eyes:

Note: I developed the tool and started preparing this blog post prior to the publication of this blog post by James Forshaw: Sharing a Logon Session a Little Too Much. I could have chosen to cancel the publication of my post but I eventually realized that it was still worth it. Please keep this in mind as you read this post.

Impersonation Privileges

I want to start things off with this quote from @decoder_it: “if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM”. That’s a deliberately provocative shortcut obviously, but it’s not far from the truth. :smile:

These two privileges are very powerful indeed. They allow you to run code or even create a new process in the context of another user. To do so, you can call CreateProcessWithToken() if you have SeImpersonatePrivilege or CreateProcessAsUser() if you have SeAssignPrimaryTokenPrivilege.

Before talking about these two particular functions, I want to quickly remind you what the standard CreateProcess() function looks like:

The first two parameters allow you to specify the application or the command line you want to execute. Then, a lot of settings can be specified in order to customize the environment and the security context of the child process. Finally, the last parameter is a reference to a PROCESS_INFORMATION structure which will be returned by the function upon success. It contains the handles to the target process and thread.

Let’s take a look at CreateProcessWithToken() and CreateProcessAsUser() now:

As you can see, they are not much different than the standard CreateProcess() function. However, they both require a handle to a token. According to the documentation, hToken must be “a handle to the primary token that represents a user”. Further, you can read “To get a primary token that represents the specified user, […] you can call the DuplicateTokenEx function to convert an impersonation token into a primary token. This allows a server application that is impersonating a client to create a process that has the security context of the client.

Of course, the documenation doesn’t tell you how to get this token in the first place because that’s not the responsibility of these two functions. Though, it tells you in what type of scenario they are used. These functions allow a server application to create a process in the security context of a client. This is indeed a very common practice for Windows services that expose RPC/COM interfaces for example. Whenever you invoke an RPC function exposed by a service running as a highly privileged account, this service might call RpcImpersonateClient() in order to run some code in your security context, thus lowering the risk of privilege escalation vulnerablities.

As a summary, provided that we have the SeImpersonatePrivilege or SeAssignPrimaryTokenPrivilege privilege, we can create a process in the security context of another user. What we need though is a token for this user. The question is: how to capture such a token with a custom server application?

Impersonating a User with a Named Pipe

Exploit tools of the Potato family are all based on the same idea: relaying a network authentication from a loopback TCP endpoint to an NTLM negotiator. To do so, they trick the NT AUTHORITY\SYSTEM account into connecting and authenticating to an RPC server they control by leveraging some peculiarities of the IStorage COM interface.

During the authentication process, all the messages are relayed between the client - the SYSTEM account here - and a local NTLM negotiator. This negotiator is just a combination of several Windows API calls such as AcquireCredentialsHandle() and AcceptSecurityContext() which interact with the lsass process through ALPC. In the end, if all goes well, you get the much desired SYSTEM token.

Unfortunately, due to some core changes, this technique doesn’t work anymore on Windows 10 because the underlying COM connection from the target service to the “Storage” is now allowed only on TCP port 135.

Note: as mentionned by @decoder_it in this blog post, this restriction can actually be bypassed but the resulting token cannot be used for impersonation.

Now, what are the alternatives? RPC isn’t the only protocol that can be used in such a relaying scenario, but I won’t discuss this here. Instead, I’ll discuss an old school technique involving pipes. As I said in the Foreword, there is nothing groundbreaking about this but, as always, I like to present things my own way, so I’ll refresh some basic knowledge even though that may sound trivial for most people.

According to the documentation, “a pipe is a section of shared memory that processes use for communication. The process that creates a pipe is the pipe server. A process that connects to a pipe is a pipe client. One process writes information to the pipe, then the other process reads the information from the pipe.” In other words, pipes are one of the many ways of achieving Inter-Process Communications (IPC) on Windows, just like RPC, COM or sockets for example.

Pipes can be of two types:

  • Anonymous pipes - Anonymous pipes typically transfer data between a parent process and a child process. They are usually used to redirect standard input and output between a child process and its parent.
  • Named pipes - Named pipes on the other hand can transfer data between unrelated processes, provided that the permissions of the pipe grant appropriate access to the client process.

In the first part, I mentionned the RpcImpersonateClient() function. It can be used by an RPC server to impersonate an RPC client. It turns out that Named pipes offer the same capability with the ImpersonateNamedPipeClient() function. So, let’s do some named pipe impersonation! :sunglasses:

I realize that what I’ve explained so far is a bit too theoretical. What we need is a concrete example so, let’s consider the following code. Explanations will follow.

HANDLE hPipe = INVALID_HANDLE_VALUE;
LPWSTR pwszPipeName = argv[1];
SECURITY_DESCRIPTOR sd = { 0 };
SECURITY_ATTRIBUTES sa = { 0 };
HANDLE hToken = INVALID_HANDLE_VALUE;

if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
{
    wprintf(L"InitializeSecurityDescriptor() failed. Error: %d - ", GetLastError());
    PrintLastErrorAsText(GetLastError());
    return -1;
}

if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:(A;OICI;GA;;;WD)", SDDL_REVISION_1, &((&sa)->lpSecurityDescriptor), NULL))
{
    wprintf(L"ConvertStringSecurityDescriptorToSecurityDescriptor() failed. Error: %d - ", GetLastError());
    PrintLastErrorAsText(GetLastError());
    return -1;
}

if ((hPipe = CreateNamedPipe(pwszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa)) != INVALID_HANDLE_VALUE)
{
    wprintf(L"[*] Named pipe '%ls' listening...\n", pwszPipeName);
    ConnectNamedPipe(hPipe, NULL);
    wprintf(L"[+] A client connected!\n");

    if (ImpersonateNamedPipeClient(hPipe)) {

        if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken)) {

            PrintTokenUserSidAndName(hToken);
            PrintTokenImpersonationLevel(hToken);
            PrintTokenType(hToken);

            DoSomethingAsImpersonatedUser();

            CloseHandle(hToken);
        }
        else
        {
            wprintf(L"OpenThreadToken() failed. Error = %d - ", GetLastError());
            PrintLastErrorAsText(GetLastError());
        }
    }
    else
    {
        wprintf(L"ImpersonateNamedPipeClient() failed. Error = %d - ", GetLastError());
        PrintLastErrorAsText(GetLastError());
    }
    
    CloseHandle(hPipe);
}
else
{
    wprintf(L"CreateNamedPipe() failed. Error: %d - ", GetLastError());
    PrintLastErrorAsText(GetLastError());
}

The first two function calls are used to create a custom Security Descriptor that will be applied to the pipe. These functions are not specific to pipes and they don’t play a role in impersonation but I have to mention them briefly. Indeed, pipes are securable objects just like files or registry keys. This means that if you don’t set the appropriate permissions on the named pipe you create, clients running with a different identity might not be able to access it at all. Here, I chose the easy way by granting Everyone generic access to the pipe.

Here are the required functions for impersonating a client through a named pipe:

  • CreateNamedPipe() - The name speaks for itself. As a server application, this function allows you to create a named pipe with a name of the form \\.\pipe\PIPE_NAME.
  • ConnectNamedPipe() - Once the pipe is created, this function is used for accepting connections. Unless specified otherwise, the call is synchronous by default, so the thread is paused untill a client connects.
  • ImpersonateNamedPipeClient() - This is where the magic happens!

Of course, some rules apply to the use of this last function. According to the documentation, here are two of the four cases where impersonation is allowed:

  • The authenticated identity is same as the caller - In other words, you can impersonate yourself. Surprisingly, there are some exploitation scenarios where this is actually useful.
  • The caller has the SeImpersonatePrivilege privilege - That’s us! :slightly_smiling_face:

Just one last thing before seeing this code in action. I implemented a few functions that will print some information about the client’s token and I also implemented a function that I called DoSomethingAsImpersonatedUser(). The purpose of this function is to check whether we can actually execute code in the context of the client. This will be particularly relevant for the last part of this post.

PrintTokenUserSidAndName(hToken);
PrintTokenImpersonationLevel(hToken);
PrintTokenType(hToken);
DoSomethingAsImpersonatedUser();

And here we go! After starting my server application as a local administrator (administrators have the SeImpersonatePrivilege prvivilege by default), I use a normal user account and try to write to the named pipe.

Once the client is connected, you get an impersonation token with an impersonation level of 2, i.e. SecurityImpersonation. In addition, DoSomethingAsImpersonatedUser() returned successfully, which means that we can run arbitrary code in the security context of this client. :ok_hand:

Note: perhaps you noticed that I used the path \\localhost\pipe\foo123, instead of \\.\pipe\foo123, which is the real name of the pipe. For the impersonation to succeed, the server must first read data from the pipe. If the client opens the path using \\.\pipe\foo123 as the pipe’s path, no data is written and ImpersonateNamedPipeClient() fails. On the other hand, if the client opens the pipe using \\HOSTNAME\pipe\foo123, ImpersonateNamedPipeClient() succeeds. Don’t ask me why, I have no idea…

To summarize, we know that in order to create a process in the context of another user we need a token. Then, we saw that we could get that token thanks to a server application which leverages named pipe impersonation. So far, that’s common knowledge but the question is: how can we trick the NT AUTHORITY\SYSTEM account into connecting to our named pipe?

Getting a SYSTEM Token

At then end of last year (2019-12-06), @decoder_it published a blog post entitled We thought they were potatoes but they were beans (from Service Account to SYSTEM again), where he demonstrated how the Background Intelligent Transfer Service (BITS) could be leveraged to get a SYSTEM token in a local NTLM relaying scenario which is quite similar to the technique used in the Potato exploits. @decoder_it and @splinter_code implemented this technique in a tool called RogueWinRM, which you can find here.

Although this method is perfectly valid, it comes with a significant drawback. It relies on a WinRM request that is performed by BITS on the local TCP port 5985, the default WinRM port. If this port is available, you can create a malicious WinRM server that will reply to this request and thus capture the credentials of the SYSTEM account. Although the WinRM service is usually stopped on workstations, it is quite the opposite when it comes to server instances, so it wouldn’t be exploitable in this case.

When the results of this research and the associated PoC came out, I was also searching for a generic way of achieving the same objective: capturing a SYSTEM token via a local NTLM relay. Although that wasn’t my top priority, I did find a similar trick but, in the end, it had the same limitations. It wouldn’t work on most installations of Windows Server, so I left it aside. And then, a few months later, during a chat, @jonaslyk gave me the answer: the Printer Bug (with a slight twist).

Does it ring a bell? :wink:

The Printer Bug was introduced as a tool called SpoolSample by Lee Christensen (a.k.a. @tifkin_). According to the description of the tool on GitHub, its purpose is to “coerce Windows hosts authenticate to other machines via the MS-RPRN RPC interface”. The idea behind this tool is to provide a simple and effective mechanism for exploiting Active Directory environments, by tricking a Domain Controller into connecting back to a system configured with unconstrained delegation. Based on this simple concept, an attacker can compromise another forest in a 2-way trust for example, but I digress…

This exploit is based on a single RPC call to a function exposed by the Print Spooler service.

DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( 
    /* [in] */ PRINTER_HANDLE hPrinter,
    /* [in] */ DWORD fdwFlags,
    /* [in] */ DWORD fdwOptions,
    /* [unique][string][in] */ wchar_t *pszLocalMachine,
    /* [in] */ DWORD dwPrinterLocal,
    /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)

According to the documentation, this function creates a remote change notification object that monitors changes to printer objects and sends change notifications to a print client using either RpcRouterReplyPrinter or RpcRouterReplyPrinterEx.

Do you know how these notifications are sent to the client? The answer is “via RPC… over a named pipe”. Indeed, the RPC interfaces of the Print Spooler service are exposed over a named pipe: \\.\pipe\spoolss. You can see the pattern now? :slightly_smiling_face:

Let’s try a few things with the PoC provided by Lee Christensen.

The tool was originally designed to let you specify two server names: the one to connect to (a Domain Controller) and the one you control, for capturing the authentication. Here we want to connect to the local machine and receive the notification on the local machine as well. The problem is that if we do that, the notification is sent to \\DESKTOP-RTFONKM\pipe\spoolss. This pipe is controlled by NT AUTHORITY\SYSTEM and we cannot create our own pipe with the same name, that doesn’t make any sense. On the other hand, if we specify an arbitrary path and append an arbitrary string, the call just fails because of a path validation check.

Though, I did say that there was a twist. Here is the second trick that @jonaslyk shared with me. If the hostname contains a /, it will pass the path validation checks but, when calculating the path of the named pipe to connect to, normalization will transform it into a \. This way, we can partially control the path used by the server! :open_mouth:

See? The final path that is being used by the service is now \\DESKTOP-RTFONKM\foo123\pipe\spoolss. Of course, this is not a valid path for a named pipe, but with a slight adjustment, we can make it a valid one. If we specify the value \\DESKTOP-RTFONKM/pipe/foo123 in our RPC call, the service will transform it into \\DESKTOP-RTFONKM\pipe\foo123\pipe\spoolss, which is perfectly valid.

Thanks to our server application, we can quicky test this scenario. The following screenshot shows that we do get a connection and that we can then successfully impersonate NT AUTHORITY\SYSTEM.

I implemented this trick in a tool I called PrintSpoofer. As a prerequisite, the only required privilege is SeImpersonatePrivilege. I tested it successfully on default installations of Windows 8.1, Windows Server 2012 R2, Windows 10 and Windows Server 2019. It might work as well on older versions of Windows under certain circumstances.

The screenshot below shows the execution of the tool in a real-life scenario. A shell is opened as a subprocess of the CDPSvc service on Windows Server 2019. This concrete example is particularly interesting because this service runs as NT AUTHORITY\LOCAL SERVICE with only two privileges: SeChangeNotifyPrivilege and SeImpersonatePrivilege.

How to Prevent Named Pipe Impersonation

First of all, I don’t know if it’s common knowledge but named pipe impersonation can be prevented. As a client, you can specify that you don’t want to be impersonated or, at least, that you don’t want the server to run code in your security context. In fact, there is a place which I already discussed in a previous post where this protection was implemented by Microsoft as a fix for a “vulnerability”.

But before we discuss this, we need a dummy client application for communicating with our named pipe server. This will help me illustrate what I’m going to explain. Named pipes are part of the filesystem so how do we connect to a pipe? The answer is “with a simple CreateFile() function call”.

HANDLE hFile = CreateFile(
    argv[1],                        // pipe name
    GENERIC_READ | GENERIC_WRITE,   // read and write access 
    0,                              // no sharing 
    NULL,                           // default security attributes
    OPEN_EXISTING,                  // opens existing pipe 
    0,                              // default attributes 
    NULL                            // no template file 
);

if (hFile != INVALID_HANDLE_VALUE) {
    wprintf(L"[+] CreateFile() OK\n");
    CloseHandle(hFile);
} else {
    wprintf(L"[-] CreateFile() failed. Error: %d - ", GetLastError());
}

If we run this code, we can see that we get a connection on our named pipe and the client is successfully impersonated. There is nothing surprising because I called CreateFile() with default values.

Though, in the documentation of the CreateFile() function, we can see that a lot of attributes can be specified. In particular, if the SECURITY_SQOS_PRESENT flag is set, we can control the impersonation level of our token.

So, in the source code of the dummy client application, I modified the CreateFile() function call as follows. The value SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION is now specified as part of the dwFlagsAndAttributes parameter.

HANDLE hFile = CreateFile(
    argv[1],                        // pipe name
    GENERIC_READ | GENERIC_WRITE,   // read and write access 
    0,                              // no sharing 
    NULL,                           // default security attributes
    OPEN_EXISTING,                  // opens existing pipe 
    SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, // impersonation level: SecurityIdentification
    NULL                            // no template file 
);

We still get some info about the token but, this time, if we try to execute code in the security context of the client, an error is returned: Either a required impersonation level was not provided, or the provided impersonation level is invalid. Indeed, as highlighted on the screenshot, the impersonation level of the token is now SecurityIdentification which prevents our malicious server application from fully impersonating the client.

That being said, that’s still a bit theoretical but, I did mention that Microsoft implemented this protection as a fix for a vulnerability. In a previous post, I discussed a vulnerability in the Service Tracing feature. As a reminder, this feature allows you to collect some debug information about a particular service simply by editing a registry key in the HKLM hive. Any Authenticated User can specify the destination folder of the log file in the FileDirectory value. For example, if you specify C:\test, the debugged program will write to C:\test\MODULE.log and this operation is performed in the security context of the target application or service.

Since you have control over the file path, nothing prevents you from using the name of a pipe as a path for the target directory. Well, that’s exactly what the CVE-2010-2554 or the MS10-059 bulletin is about.

This vulnerability was reported to Microsoft by @cesarcer. He implemented this in a tool called Chimichurri. I didn’t find the original source of the code but you can find it in this repo. The idea is to trick a service running as NT AUTHORITY\SYSTEM into connecting to a malicious named pipe and thus capture its token. Provided that you had the SeImpersonatePrivilege, this method worked perfectly well.

Let’s see what happens now if we try to do the same thing on Windows 10:

Although we have the SeImpersonatePrivilege privilege, we get the exact same error when we try to execute code in the context of the SYSTEM account. If we take a look at the CreateFile() call used in rtutils.dll to open the log file, we can see the following:

The hexadecimal value 0x110080 is actually SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION | FILE_ATTRIBUTE_NORMAL.

Note: it should be noted that this protection isn’t bulletproof though. It just makes things harder for an attacker.

As a conclusion, Microsoft treated this case as a regular vulnerability, assigned it a CVE ID and even wrote a detailed security bulletin. Though, times have changed a lot! Nowadays, if you try to report such a vulnerability, they will reply that elevation of privilege by leveraging impersonation privileges is an expected behavior. They probably realized that it’s a fight they cannot win, at least not this way. Like James Forshaw once said about this kind of exploit on Twitter: “they’d argue that you might as well be SYSTEM if you’ve got impersonate privilege as that’s kind of the point. They can make it harder to get a suitable token but it’s just a game of whack-a-mole as there will always be something else you can exploit” (source).

Conclusion

In this post, I explained how the impersonation privileges could be leveraged on Windows 10 in order to execute code in the context of the SYSTEM account. A lot of Windows services which run as LOCAL/NETWORK SERVICE have these capabilities. Though, sometimes they don’t. In this case, you can still recover impersonation privileges either using this tool - FullPowers - or with the method which was illustrated by James Forshaw in this blog post: Sharing a Logon Session a Little Too Much.

Last but not least, I want to say a special thank you to @jonaslyk. Over the past few weeks, I had the chance to chat with him on multiple occasions and, I have to say that he’s always willing to share and explain some cool tips and tricks. These conversations sometimes even turn into very productive brainstorming sessions.

Links & Resources

From NETWORK SERVICE to SYSTEM

In the last period, there have been several researches on how to escalate privileges by abusing generic impersonation privileges usually assigned to SERVICE accounts.

Needless to say,  a SERVICE account is required in order to abuse the privileges.

The last one, in order of time, “Printer Spoofer” is probably the most dangerous/useful because it only relies on the “Spooler” service which is enabled by default on all Windows versions.

In Windows 10 and Windows Server where WinRM is not enabled, you can use our “Rogue WinRM listener” in order  to capture a SYSTEM token.

And of course, in Windows Server versions 2016 and Windows 10 up to 1803, our Rotten/Juicy Potato is still kicking!

In this post I’m going to show you how it is possible to get SYSTEM privileges from the Network Service account, as described in Forshaw’s post “Sharing a Logon Session a Little Too Much“. I suggest you to read it before if not already done as I won’t detail the internal mechanism.

In short, if you can trick the “Network Service” account to write to a  named pipe over the “network” and are able to impersonate the pipe, you can access the tokens stored in RPCSS service (which is running as Network Service and contains a pile of treasures) and “steal” a SYSTEM token. This is possible because of some “weird” cheks/ assignments  in token’s Authentication ID.  The token of the caller (coming from RPCSS service) will have assigned the Authentication ID of the  service and if you impersonate this  token you will have complete access to RPCSS process, including his tokens. (because the impersonated  token belongs to group “NT Service\RpcSs “)

Side note: here you can find some other “strange” behaviors based on AuthID.

Given that the local loopback interface (127.0.0.1) is considered a network logon/access, it’s possible to exploit this (mis)behavior locally with an all-in-one tool.

The easiest way is a compromised “Network Service” account with a shell access and this will be our starting point. In this situation, we can directly write via the loopback interface to the named pipe, impersonate and access RPCSS process tokens.

Note: For testing purpose, you can impersonate the “Network Service” account using psexec from an admin shell:

  • psexec64 -i -u “NT AUTHORITY\Network Service” cmd.exe

There are many ways to accomplish this task, for example with Forshaw’s NTOBJECTMANAGER library in Powershell (keep in mind that the latest MS Defender updates marks this library as malicious!??)

But my goal was to create a standalone executable in old plain vanilla style and given that I’m very lazy, I found most of the functions needed in the old but always good incognito2 project. The source code is very useful and educational, it’s worth the study.

I reused the most relevant parts of code and did some minor changes in order to adpapt it to my needings and also to evade AV’s.

Basically this is what it does:

  • start a Named Pipe Server listening on a “random” pipe
  • start a Pipe Client thread, connect to  the random pipe via “localhost” and write some data
  • In the pipe server, once the client connects, impersonate the client (coming from “RPCSS”)
  • List tokens of all processes:Capture
  • If a SYSTEM token is available , impersonate it  and execute your shell or whatever you prefer:

Cattura

 

The “adapted” source code for my POC can be found here: https://github.com/decoder-it/NetworkServiceExploit

That’s all 😉

 

Capture

decoderblogblog

Capture

Cattura

No more JuicyPotato? Old story, welcome RoguePotato!

 

After the hype we (@splinter_code and me) created with our recent tweet , it’s time to reveal what we exactly did in order to get our loved JuicyPotato kicking again… (don’t expect something disruptive 😉 )

We won’t explain how the *potato exploits works, how it was fixed etc etc, there is so much literature about this and google is always your best friend!

Our starting point will be the “RogueWiRM” exploit.

In fact, we were trying to bypass the OXID resolver restrictions when we came across this strange behavior of BITS. But what was our original idea?

“(…) we found a solution by doing some redirection and implementing our own “Oxid Resolver” . The good news is that yes, we got a SYSTEM token! The bad one: it was only an identification token, really useless

Short recap

  • You try to initialize a DCOM object identified by a particular CLSID from an “IStorage Object” via standard marshalling (OBJREF_STANDARD)
  • The “IStorage” obect contains, among other things, the OXID, OID, and IPID parameters needed for the OXID resolution in order to get the needed bindings to connect to our DCOM object interfaces. Think about it like sort of DNS query
  • From “Inside COM+: Base Services”: The OXID Resolver is a service that runs on every machine that supports COM+. It performs two important duties:
    • It stores the RPC string bindings that are necessary to connect with remote objects and provides them to local clients.
    • It sends ping messages to remote objects for which the local machine has clients and receives ping messages for objects running on the local machine. This aspect of the OXID Resolver supports the COM+ garbage collection mechanism.
  • OXID resolver is part of “rpcss” service and runs on port 135. Starting from Windows 10 1809 & Windows Server 2019, its no more possible to query the OXID resolver on a port different than 135
  • OXID resolutions are normally authenticated.  We are only interested in the authentication part, which occurs via NTLM in order to steal the token, so we can specify in our case for OXID, OID and IPID just 32 random bytes.
  • If you specify a remote OXID resolver, the request will be processed with an “ANONYMOUS LOGON“.

So what was our idea in our previous experiments?

  • Instruct the DCOM server to perform a remote OXID query by specifying a remote IP in the binding strings instead of “127.0.0.1”. This is done in the “stringbinding” structure
  • On the remote IP where is running a machine under our control, setup a “socat” listener (or whatever you prefer) for redirecting the OXID resolutions requests to a “fake” OXID RPC Server, created by us and  listening on a port different from 135. The fake server can run on the same Windows machine we are trying to exploit, but not necessary.
  • The fake OXID RPC server implements the “ResolveOxid2” server procedure
  • This function will resolve the client (ANONYMOUS LOGON) query  by returning the  RPC bindings which will point to another “fake” RPC server listening on a dedicated port under our control hosted on the victim machine.
  • The DCOM server will connect to the RPC server in order to perform the IRemUnkown2 interface call. By connecting tho the second RPC Server, an “Autentication Callback” is triggered and if we have SE_IMPERSONATE_NAME privileges, we can impersonate the caller via RpcImpersonateClient() call
  • And yes, it worked! We got a SYSTEM token coming from BITS service in this case, but unfortunately it was an Identification token, really useless.

The good news

So we abandoned the research until some days ago, when two interesting exploits/bugs had been published:

  1. Sharing a Logon Session a Little Too Much published by @tiraniddo . You can also find in this blog  the  post along with a working POC in order to demonstrate how it was possible for the NETWORK SERVICE Account to elevate privileges (bug/feature?)
  2. The marvellous “Print Spoofer” exploit published by @itm4n. In this case (and for our scenario) not for the exploit itself but for the “bug” in the named pipe path validation checks.

On a late evening of some days ago, @splinter_code sent me a message:

(@splinter_code) “If we resolve the fake OXID request by providing an RPC binding with  our named pipe instead of the TCP/IP sequence, we should in theory be able to impersonate the NETWORK SERVICE Account and after that… SYSTEM is just a step beyond…”

(me) “Yeah, sounds good, let’s give it a try!”

And guess what? IT WORKED!!!

Let’s dig into the details.

The “fake” OXID Resolver

First of all a  little bit of context on how the OXID resolution works is required to understand the whole picture.
This is how the flow looks like, taken from msdn :

OXID resolution sequence

The client will be the RPCSS service that will try to connect to our rogue oxid resolver (server) in order to locate the binding information to connect to the specified dcom.

Consider that all the above requests done by the client in the OXID resolution sequence are authenticated (RPC Security Callback) impersonating the user running the COM behind the CLSID we choose (usually the one running as SYSTEM are of interests).
What *potato exploits does is intercepting this authentication and steal the token (well technically is a little bit more complicated because the authentication is forwarded to the local system OXID resolver that will do the real authentication and *potato will just alter some packets to steal the token… a classic MITM attack).
When this is done over the network (due to the MS fix) this will be an Anonymous Logon so quite useless for an EoP scenario.

Our initial idea was to exploit one of the OXID resolver (technically called IObjectExporter) method and forge a response that could trigger a privileged authentication to our controlled listener.

Looking at the scheme above 2 functions caught our attention:

So the first request was not of our interest because we couldn’t  specify endpoint information (that’s what we needed) so we were always answering with a response of RPC_S_OK. This is an example of what a standard response looks like:

serveralive2_pcap

So we focused in the next one (ResolveOxid2) and what we saw is something interesting that we could abuse:

resolveoxid2_pcap

Observing the above traffic, that is a standard response captured on the real OXID resolver on a windows machine, we quickly spot that we could abuse this call by forging our own ResolveOxid2 response and specify as an endpoint our controlled listener.
We can  specify endpoint information in the format ipaddress[endpoint] and also the TowerId (more on that later).

But… Why not registering the binding information we require on the system OXID resolver, getting the OID and OXID from the system and ask the unmarshalling of that?
Well as our knowledge the only way to do that is by registering a CLSID on the system and to do that you need Administrator privileges.

So our idea was to implement a whole OXID resolver that would answer always with our listener binding for any oxid oid it will receive, just a Fake OXID resolver.

Let’s analyze the function ResolveOxid2 to understand how to forge our response:

[idempotent] error_status_t ResolveOxid2(
   [in] handle_t hRpc,
   [in] OXID* pOxid,
   [in] unsigned short cRequestedProtseqs,
   [in, ref, size_is(cRequestedProtseqs)] 
     unsigned short arRequestedProtseqs[],
   [out, ref] DUALSTRINGARRAY** ppdsaOxidBindings,
   [out, ref] IPID* pipidRemUnknown,
   [out, ref] DWORD* pAuthnHint,
   [out, ref] COMVERSION* pComVersion
 );

What we need to do is to fill the DUALSTRINGARRAY ppdsaOxidBindings with our controlled listener.
Well that was a little bit tricky, but it is well documented by MS (in 1 and 2) so it was just a matter of trial and errors to forge the right offsets/sizes for the packet.

Let’s recap, we can redirect the client to our controlled endpoint (aNetworkAddr) and we can choose the protocol sequence identifier constant that identifies the protocol to be used in RPC calls (wTowerId).

There are multiple protocols supported by RPC (here a list).

In the first attempt we tried ncacn_ip_tcp, as TowerId, that allows RPC straight over TCP.
We run an RPC server (keep in mind that a “standard” user can setup and register an RPC Server) with the IRemUnknown2 interface and tried to authenticate the request in our SecurityCallback calling RpcImpersonateClient.
Returning ncacn_ip_tcp:localhost[9998] in the ResolveOxid2 response, an authentication to our RPC server (IRemUnknown2) were triggered, but unfortunately was just an identification token…

iremunkown2

 

So that was a dead end… But what about “Connection-oriented named pipesncacn_np ?

The named pipe Endpoint mapper

First of all what is the “epmapper” ? Well it’s related to the “RpcEptMapper” service. This service resolves “RPC interfaces identifiers to transport endpoints”.  Same concept as OXID Resolver, but for RPC.

The mapper runs also on TCP port 135 but can also be queried via a special named pipe “epmapper”.

This service shares the same process space as the “rpcss” service and both of them run under the NETWORK SERVICE account.  So if we are able to impersonate the account under this process we can steal the SYSTEM token (because this process hosts juicy tokens)…

The first try we gave was to return a named pipe under our control in the ResolveOxid2 response:

ncacn_np:localhost[\pipe\roguepotato]

But observing the network traffic the RPCSS still connected to the \pipe\epmapper . This is because, by protocol design, the named pipe epmapper must be used, and this is obviously a “reserved” name:

Capture

No way…

When we read the PrintSpoofer post we were impressed by clever trick of the named pipe validation path bypass (credits to @itm4n and @jonasLyk) where inserting ‘/’ in the hostname will be converted in partial path of the named pipe!

With that in mind we returned the following binding information:

ncacn_np:localhost/pipe/roguepotato[\pipe\epmapper]

And you know what? The RPCSS were trying to connect to nonexistent named pipe \roguepotato\pipe\epmapper :

named_pipe_not_found

Great! So we setup a pipe listener on \\.\pipe\roguepotato\pipe\epmapper, got the connection and impersonated the client. Then we inspected the token with TokenViewer :

token_viewer

So we had an Impersonation token of the NETWORK SERVICE account and also the LUID of the RPCSS service! 

Why that? Well, the answer was in the post mentioned above:

if you can trick the “Network Service” account to write to a  named pipe over the “network” and are able to impersonate the pipe, you can access the tokens stored in RPCSS service

All the “magic” about the fake ResolveOxid2 response is happening in this piece of code:

resolveoxid2

The Token “Kidnapper”

Perfect! Now we just needed to perform the old and well known Token Kidnapping technique to steal some SYSTEM token from the RPCSS service.

We setup a minimalist “stealer” which performs the following operations:

  • get the PID of the “rpcss” service
  • open the process, list all handles and for each handle try to duplicate it and get the handle type
  • if handle type is “Token” and token owner is SYSTEM, try to impersonate and launch a process with CreatProcessAsUser() or CreateProcessWithToken()
  • In order to get rid of the occasional “Access Denied” when trying to launch a processes in Session 0, we also setup the correct permissions for the Windows Station/Desktop 

Well, let’s unleash RoguePotato and see our SYSTEM shell popping 😀

system_privs

Final Thoughts

Probably you are a little bit confused at this point, let’s do a recap. Under which circumstances can I use this exploit to get SYSTEM privileges?

  • You need a compromised account on the victim machine with Impersonate privileges (but this is the prerequisite for all these type of exploits). A common scenario is code execution on default IIS/MSSQL configuration.
  • If you are able to impersonate NETWORK SERVICE account or if the “spooler” service is not stopped you can first try the other exploits mentioned before…
  • You need to have  a machine under your control where you can perform the redirect and this machine must be accessible on port 135 by the victim  
  • We setup a POC with 2 exe files. In fact it is also possible to launch the fake OXID Resolver in standalone mode on a Windows machine  under our control when the victim’s firewall won’t accept incoming connections.
  • Extra mile: In fact, in the old *potato exploit, an alternate way for stealing the token was just to setup a local RPC listener on dedicated port instead of forging the local NTLM Authentication,  RpcImpersonateClient() would have done the rest.

To sum it up:

flow_graph_2

You can find the source code and POC of RoguePotato here.

Final note: We didn’t bother to report this “misbehavior?” to MS because they stated that elevating from a Local Service process (with SeImpersonate) to SYSTEM is an “expected behavior”. (RogueWinRm – Disclosure Timeline)

That’ all 😉

@slpinter_code, @decoder_it

Capture

decoderblogblog

OXID resolution sequence

serveralive2_pcap

resolveoxid2_pcap

iremunkown2

Capture

named_pipe_not_found

token_viewer

resolveoxid2

system_privs

flow_graph_2

The impersonation game

I have to admit, I really love Windows impersonation tokens! So when it comes to the possibility to “steal” and/or impersonate a token I never give up!

This is also another chapter of the never ending story of my loved “JuicyPotato”.

 So, here we are (refer to my previous posts in order to understand how we arrived a this point):

  • You cannot specify a custom port for OXID resolver address in latest Windows versions
  • If you redirect the OXID resolution requests to a remote server on port 135 under your control and the forward the request to your local Fake RPC server, you will obtain only an ANONYMOUS LOGON.
  • If you resolve the OXID Resolution request to a fake “RPC Server”, you will obtain an identification token during the IRemUnkown2 interface query.

The following image should hopefully describe the “big picture”

 

But why this identification token and not an impersonation one? And are we sure that we will always get only an identification token? We need at least an impersonation token in order to elevate our privileges!

But first of all we need to understand who/what is generating this behavior. A network capture of the NTLM authentication could help:  

The verdict is clear: in the first NTLM message, the client sets the “Identify” flag, which means that the server should not impersonate the client.

Side Note: Don’t even try to change this flag (remember, we are doing kind of MITM attack in local NTLM Authentication, so we could alter the value), the client would not answer to our NTLM type 2 message!

But why does the client activate this flag? Well, probably it’s all in this RPC API client call setup:

RPC_STATUS RpcBindingSetAuthInfoExA( 
RPC_BINDING_HANDLE Binding, 
RPC_CSTR ServerPrincName, 
unsigned long AuthnLevel, 
unsigned long AuthnSvc, 
RPC_AUTH_IDENTITY_HANDLE AuthIdentity, 
unsigned long AuthzSvc, 
RPC_SECURITY_QOS *SecurityQos 
);

More precisely in this structure:

typedef struct _RPC_SECURITY_QOS {
unsigned long Version; 
unsigned long Capabilities; 
unsigned long IdentityTracking; 
unsigned long ImpersonationType; 
} RPC_SECURITY_QOS, *PRPC_SECURITY_QOS;

The client can define the desidered impersonation level before connecting to server. At this point we can assume that this is the default:

ImpersonationType=RPC_C_IMP_LEVEL_IDENTIFY

for the COM security settings when svchost services starts the DLL inside svchost.exe.

This is also controlled in a registry key (thanks to @tiraniddo for remembering me this)  located in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost where the impersonation level can be defined overriding the default value.

In the following screen shot we can observe that all services belonging to “print” group will call the CoInitializeSecurity function with Impersonation level =3 (Impersonate)

And which service belongs to this group?

It’s the “PrintNotify” service, which can be instantiated and accessed by the following DCOM server:

Ok, let’s see what happens on a fully patchted Win10 1909 if we try to initialize this object via the IStorageTrigger, redirect to our fake OXID resolver and then intercept he authentication callbacks.

The first request with ANONYMOUS logon is relative to the redircted OXID resolution request. The two subsequent are generated by the IremUnknown2 queries, and this time we have impersonation tokens from SYSTEM!

And according to the registry configuration, we can see that the Impersonation parameter is read from the registry:

But wait, there is still another problem:

The SERVICE user’s group (which is typically the users we will “target” because they have impersonation privileges) cannot start/launch the sevice/dll and does not belong to the INTERACTIVE group.

Again, no way? Well not exactly.

With my modified JuicyPotato – juicy_2 (see previous screenshot) running as “NT AUTHORITY\Local Service”, I tested all CLISD’s (7268 in my Windows 10 1909) in order to find something else and this is the result:

There were 2 other CLSIDS which impersonate LOCAL SYSTEM:

  1. {90F18417-F0F1-484E-9D3C-59DCEEE5DBD8} which is hosted by the obsolete ActiveX Installer service:


This service belongs the “AxInstSVGroup” group, but surprisingly this group is not listed in the registry, so I would expect an Identification token! This means that the previous assumption is not always true, the impersonation level is not exclusively controlled via registry configuration…

2. {C41B1461-3F8C-4666-B512-6DF24DE566D1} This one does not belongs to a specific service but is related to an Intel Graphics Driver “IntelCpHeciSvc.exe” which hosts a DCOM server.




Clearly, the presence of this driver depends on the HW configuration of your machine.

The other CLISD’s impersonates the interactive user connected to first Session:

{354ff91b-5e49-4bdc-a8e6-1cb6c6877182 }- SpatialAudioLicenseServerInteractiveUser

{38e441fb-3d16-422f-8750-b2dacec5cefc}- ComponentPackageSupportServer

{f8842f8e-dafe-4b37-9d38-4e0714a61149} – CastServerInteractiveUser

So if you have impersonation privileges and an admin is connected interactively you could obtain his token and impersonate.

I did the same test on the CLSID’s of a Windows 2019 Server but this time was not able to get a SYSTEM impersonation token. The ActiveX Installer service is disabled by default on this OS. The only valid CLSID’s were the 3 relative to interactive user.

Well that’s all for now, the Impersonation game never ends 😉

.. and thanks to @splinter_code

Source code can be found here

Capture.PNG

decoderblogblog

Chimichurri Reloaded - Giving a Second Life to a 10-year old Windows Vulnerability

This is a kind of follow-up to my last post, in which I discussed a technique that can be used for elevating privileges to SYSTEM when you have impersonation capabilities. In the last part, I explained how this type of vulnerability could be fixed and I even illustrated it with a concrete example of a workaround that was implemented by Microsoft 10 years ago in the context of the Service Tracing feature. Though, I also insinuated that this security measure could be bypassed. So, let’s see how we can make a 10-year old vulnerability great again…

What Are We Talking About?

I won’t assume that you’ve read all of my previous blog posts, so I’ll start things off with a brief recap.

Around 10 years ago, Cesar Cerrudo (@cesarcer) found that it was possible to use the Service Tracing feature of Windows as a way of capturing a SYSTEM token using a named pipe. As long as you had the SeImpersonatePrivilege privilege, you could then execute arbitrary code in the security context of this user. Back then, this was acknowledged by Microsoft as a vulnerability and it got the CVE ID CVE-2010-2554.

Let’s take the Service Tracing key corresponding to the RASMAN service as an example. The idea is simple, you first have to start a local named pipe server. Then, instead of setting a simple directory path as the target log file’s folder in the registry, you can specify the path of this named pipe.

In this example, \\localhost\pipe\tracing is set as the target directory. Then, as soon as EnableFileTracing is set to 1, the service will try to open its log file using the path \\localhost\pipe\tracing\RASMAN.LOG. So, if we create a named pipe with the name \\.\pipe\tracing\RASMAN.LOG, we will receive a connection and we can impersonate the service account by calling the ImpersonateNamedPipeClient function. Since RASMAN is running as NT AUTHORITY\SYSTEM, we eventually get a SYSTEM impersonation token.

Note: depending on the version of Windows, the log file open event sometimes doesn’t occur immediately when EnableFileTracing is set to 1. One way to trigger it reliably is to start the service. Note that the RASMAN service can be started by low-privileged users via the Service Control Manager (SCM).

Of course, if you try to do this on a version of Windows that is less than 10 years old, you’ll run into the same problem that is shown on the above screenshot. If you try to execute arbitrary code in the security context of the impersonated user, you’ll get the error code 1346, i.e. Either a required impersonation level was not provided, or the provided impersonation level is invalid.. You could also get the error code 5, i.e. Access denied. It’s the result of a counter-measure that was enforced by Microsoft in this particular Windows feature.

I detailed this security update in my previous post. To put it simple, the flags SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION are now specified in the CreateFile function call, which is responsible for getting the initial handle on the target log file. Because of these flags, the resulting impersonation level of the token we get is now SecurityIdentification (level 2/4). Though, SecurityImpersonation (level 3/4) or SecurityDelegation (level 4/4) is required in order to fully impersonate the user.

Note: as a side note, you probably noticed that the message Unknown error 0x80070542 is printed on the command prompt instead of the actual Win32 error message. The reason for this is that I try to get the error message correspondnig to the error code while impersonating the user. Because of the limited impersonation level, this code/message lookup fails.

Does it mean we are screwed? Short answer, yes, we are because the token we get is kinda useless. Long answer, well, you’ll have to read the next parts… :wink:

A UNC Path?

In the previous part, we saw that if we specify the name of a pipe as the log file directory, we do get an impersonation token but we cannot use it to create a new process. OK but did you notice how I glossed over an important detail here? How did I specify the name of a pipe in the first place?

First you need to know how the final log file path is calculated. That’s trivial, it’s a simple string concatenation: <DIRECTORY>\<SERVICE_NAME>.LOG, where <DIRECTORY> is read from the registry (FileDirectory value). So, if you specify C:\Temp as the output directory for a service called Bar, the service will use C:\Temp\BAR.LOG as the path of its log file.

In this exploit though, we specified the name of a pipe rather than a regular directory, by using a UNC path. On Windows, there are many ways to specify a path and this is only one of them but this post isn’t about that. Actually, UNC (Universal Naming Convention) is exactly what we need, but we need to use it a slightly differently.

UNC paths are commonly used for accessing remote shares on a local network. For example, if you want to access the folder BAR on the volume FOO of the machine DUMMY, you’ll use the UNC path \\DUMMY\FOO\BAR. In this case, the client machine connects to the TCP port 445 of the target server and uses the SMB protocol to exchange data.

There is a slight variant of this example that you probably already know of. You can use a path such as \\DUMMY@4444\FOO\BAR in order to access a remote share on an arbitrary port (4444 in this example) rather than the default TCP port 445. Although the difference in the path is small, the implications are huge. The most obvious one is that the SMB protocol is no longer used. Instead, the client uses an extended version of the HTTP protocol, which is called WebDAV (Web Distributed Authoring and Versioning).

On Windows, WebDAV is handled by the WebClient service. If you check the description of this service, you can read the following:

Enables Windows-based programs to create, access, and modify Internet-based files. If this service is stopped, these functions will not be available. If this service is disabled, any services that explicitly depend on it will fail to start.

Although, WebDAV uses a completely different protocol, one thing remains: authentication. So, what if we create a local WebDAV server and use such a path as the output directory?

Hello, I’m Officer Dave. May I See Your ID, Please?

First things first, we need to edit the regsitry and change the value of FileDirectory. We will set \\127.0.0.1@4444\tracing instead of a named pipe path.

Thanks to netcat, we can easily open a socket and listen on the local TCP port 4444. After enabling the service tracing, here is what we get:

Interesting! We get an HTTP OPTIONS request. The User-Agent header also shows us that this is a WebDAV request but, apart from that, there’s not much to learn. Though, now that we know that the service is willing to communicate using WebDAV, we should reply and send an authentication request. :wink:

To do so, I created a simple PowerShell script that uses a System.Net.Sockets.TcpListener object to listen on an arbitrary port and send a hardcoded HTTP response to the first client that connects to the socket. Here is the content of the HTTP response we will send to the client:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: NTLM

If you are familiar with web application pentesting, there is a high chance you’ve encountered the WWW-Authenticate header with the value Basic quite a lot (e.g.: Apache htpasswd), but not necessarily NTLM. This value allows us to indicate that the client must authenticate using the 3-way NTLM authentication scheme. By the way, if you feel like you need to fill some gaps about NTLM, I highly recommend you to read this excellent post about NTLM relaying by @HackAndDo. These blog posts are always very well written. :slightly_smiling_face:

With the script running in the background, we get the following result:

At first glance, it looks like the service accepted to initiate the NTLM authentication and sent us its NTLM NEGOTIATE request. This is confirmed by the output of Wireshark.

This is a good start, we are definitely on the right track but there is one last thing to verify. This one last thing will decide whether all of this was for nothing or not: the Negotiate Identify flag.

The NTLM authentication protocol documentation is quite exhaustive. You can see the detailed structure of each message that is used in the protocol. In particular, this section explains how the NegotiateFlags value is calculated in the NEGOTIATE message. About the Identify flag, you can read:

So, if this bit is set, the client tells the server that it can request a token with an impersonation level of SecurityIdentification only. In other words, if this flag is set, we are back to square one. We won’t be able to get a proper impersonation token.

Thanks to the amazing Wireshark dissectors, we can easily check the value of this flag.

The Identify flag is not set!!! :tada:

This means that we can bypass the patch and get an impersonation token that we can use to execute arbitrary code in the context of NT AUTHORITY\SYSTEM. What we need to do next is simply complete the NTLM authentication thanks to an NTLM negotiator. This will allow us to transform this raw NTLM exchange into a token that we can then use if we have impersonation privileges. Though, I won’t talk about this here because this subject is already covered by the *Potato exploits.

The below screenshot shows the result of the PoC I implemented.

A Quick Bug Analysis

There is still some work to do in order to fully understand why this trick works. I won’t write a detailed analysis here but I’ll share some insight.

First, there is one detail that you may have noticed while reading the previous part. The service is requesting the resource /tracing rather than /tracing/RASMAN.LOG. Weird, isn’t it? :thinking:

Since we specified \\127.0.0.1@4444\tracing as the directory path, you’d expect that the service uses the path \\127.0.0.1@4444\tracing\RASMAN.LOG and therefore requests /tracing/RASMAN.LOG. Of course, there is an explanation. If you take a quick look at the code of the TraceCreateClientFile function in rtutils.dll, you’ll see that, before trying to open the log file, it begins by checking whether the directory specified in the FileDirectory value exists.

On the below screenshot, you can see that if CreateDirectory succeeds, i.e. if the target directory didn’t exist and was successfully created, then the function immediately returns. In this case, the log file is never opened and you’d have to disable the tracing and re-enable it. In other words, for the TraceCreateClientFile function to complete, this CreateDirectory call must fail.

The first HTTP request we receive on our WebDAV server is actually the result of this initial check. Moreover, CreateDirectory is nothing more than a user-friendly wrapper for the NtCreateFile function. Yes, everything is a file, even on Windows! :upside_down_face:

This CreateDirectory function is very convenient but there is a problem: it doesn’t allow you to specify custom flags, such as the ones used to restrict the impersonation level of the token. This explains why I was able to get a proper impersonation token using this trick.

BOOL CreateDirectoryW(
  LPCWSTR               lpPathName,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

Does it mean that I was lucky? What would happen if the server actually requested /tracing/RASMAN.LOG via the hardened CreateFile function call? To answer this question, I compiled the following code:

HANDLE hFile = CreateFile(
    argv[1], 
    GENERIC_READ | GENERIC_WRITE, 
    0, 
    NULL, 
    OPEN_EXISTING, 
    SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, // <- Limited token
    NULL);
if (hFile)
{
    wprintf(L"CreateFile OK\n");
    CloseHandle(hFile);
}
else
{
    PrintLastErrorAsText(L"CreateFile");
}

Then, instead of waiting for the RASMAN service to connect, I manually triggered the WebDAV access using this dummy application as a low-privileged user. And… here is the result:

Although the flags SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION are specified in the CreateFile function call, we still get a token with the impersonation level SecurityImpersonation! As a conclusion, yes, I was a bit lucky but it’s not that simple as it turns out the WebClient service is also flawed.

Conclusion

In this post, I explained how it is possible to easily bypass a security patch that was originally implemented to prevent a malicious server application from getting a usable impersonation token by leveraging the Service Tracing feature. Though I have to give part of the credit to this MS16-075 exploit that was written by a colleague of mine a couple of years ago. It served as a great inspiration.

I don’t know yet if I’ll publish this as a tool or not. There is still some work to do in order to make it a usable tool because of the many Service Tracing keys that can be triggered. Moreover, there is still a major prerequisite for this trick to work: the WebClient service must be installed and enabled. Although this is the default on Workstations, it’s not the case for Servers. On a server, you would need to install/enable the WebDAV component as an additional feature.

Lastly, I didn’t take the time to push further the investigation. There is a lot more work to do in order to understand why the request that is coming from the WebClient service doesn’t take the SECURITY_IDENTIFICATION flag of the CreateFile call into consideration. In my opinion, this is a vulnerability but, who cares? :pensive:

Links & Resources

CVE-2020-1170 - Microsoft Windows Defender Elevation of Privilege Vulnerability

Here is my writeup about CVE-2020-1170, an elevation of privilege bug in Windows Defender. Finding a vulnerability in a security-oriented product is quite satisfying. Though, there was nothing groundbreaking. It’s quite the opposite actually and I’m surprised nobody else reported it before me.

Introduction

Before diving into the technical details of this vulnerability, I want to say a quick word about the timeline. I initially reported this vulnerability through the Zero Day Initiative (ZDI) program around 8 months ago. After sending them my report, I received a reply stating that they weren’t interested in purchasing this vulnerability. At the time, I had only a few weeks of experience in Windows security research so I kind of relied on their judgement and left this finding aside. I even completely forgot about it in the following months.

Five months later, in late March 2020, I eventually went through my notes again and saw this report but, this time, my mindset was different. I had gained some experience because of a few other reports that I had sent to Microsoft directly. So, I knew that it was potentially eligible and I decided to spend a bit more time on it. It was a good decision because I even found a better way of triggering the vulnerability. I reported it to Microsoft in early April and it was acknowledged a few weeks later.

The Initial Thought Process

In the advisory published by Microsoft, you can read:

An elevation of privilege vulnerability exists in Windows Defender that leads to arbitrary file deletion on the system.

As usual, the description is quite generic. You’ll see that there is more to it than just an “arbitrary file deletion”.

The issue I found is related to the way Windows Defender log files are handled. In case you don’t know, Windows Defender uses 2 log files - MpCmdRun.log and MpSigStub.log - which are both located in C:\Windows\Temp. This directory is the default temp folder of the SYSTEM account but that’s also a folder where every user has Write access.

Althouth that may sound bad, it isn’t that bad because the permissions of the files are properly set (obviously?). By default, Administrators and SYSTEM have Full Control over these files, whereas normal users can’t even read them.

Here is an extract from a sample log file. As you can see, it is used to log events such as Signature Updates, but you can also find some entries related to Antivirus scans.

Signatures updates are automatically done on a regular basis but they can also be triggered manually using the Update-MpSignature PowerShell command for example, which doesn’t require any particular privileges. Therefore, these updates can be triggered as a normal user, as shown on the screenshot below.

During the process, we can see that some information is being written to C:\Windows\Temp\MpCmdRun.log by Windows Defender (as NT AUTHORITY\SYSTEM).

What this means is that, as a low-privileged user, we can trigger a log file write operation by a process running as NT AUTHORITY\SYSTEM. Though we don’t have access to the file and we can’t control its content. We don’t even have Write access on the Temp folder itself so we wouldn’t be able to set it as a mount point either. I can’t think of a more useless attack vector right now. :laughing:

Though, following my experience with CVE-2020-0668 (again…) - A Trivial Privilege Escalation Bug in Windows Service Tracing - I knew that there was potentially more to it than just a log file write.

Think about it for a second, each time a Signature Update is done (which happens quite often), a new entry is added to the file, which represents around 1 KB. That’s not much, right? But how much would it represent after several months or even years? In such case, log rotation mechanisms are often implemented so that old logs are compressed, archived or simply deleted. So, I wondered if such mechanism was also implemented to handle the MpCmdRun.log file. If so, there is probably some place for a privileged file operation abuse…

Searching for a Log Rotation Mechanism

In order to find a potential log rotation mechanism, I began by reversing the MpCmdRun.exe executable itself. After opening the file in IDA, the very first thing I did was search for occurrences of the MpCmdRun string. My initial objective was to see how the log file was handled. Looking at the Strings window is usually a good way to start.

Not surprisingly, the first result was MpCmdRun.log. Though, another very interesting result came out of this search: MpCmdRun_MaxLogSize. I was looking for a log rotation mechanism and this string was clearly the equivalent of “Follow the white rabbit”. Obviously, I took the red pill and went down the rabbit hole. :sunglasses:

Looking at the Xrefs of MpCmdRun_MaxLogSize , I found that it was used in only one function: MpCommonConfigGetValue().

The MpCommonConfigGetValue() function itself is called from MpCommonConfigLookupDword().

Finally, MpCommonConfigLookupDword() is called from the CLogHandle::Flush() method.

The following part of CLogHandle::Flush() is particularly interesting because it’s responsible for writing to the log file.

First, we can see that GetFileSizeEx() is called on hObject (1), which is a handle pointing to the log file (MpCmdRun.log) at this point. The result of this function is returned in FileSize, which is a LARGE_INTEGER structure.

typedef union _LARGE_INTEGER {
  struct {
    DWORD LowPart;
    LONG  HighPart;
  } DUMMYSTRUCTNAME;
  struct {
    DWORD LowPart;
    LONG  HighPart;
  } u;
  LONGLONG QuadPart;
} LARGE_INTEGER;

Since MpCmdRun.exe is a 64-bit executable here, QuadPart is used to get the file size as a LONGLONG directly. This value is stored in v11 and is then compared against the value returned by MpCommonConfigLookupDword() (2). If the file size is greater than this value, the PurgeLog() function is called.

So, before going any further, we need to get the value returned by MpCommonConfigLookupDword(). To do so, the easiest way I found was to put a breakpoint right after this function call and get the result from the RAX register.

Here is what it looks like once the breakpoint is hit:

Therefore, we now know that the maximum file size is 0x1000000, i.e. 16,777,216 bytes (16MB).

The next logical question would then be: what happens when the log file size exceeds this value? As we saw earlier, when the size of the log file exceeds 16MB, the PurgeLog() function is called. Based on the name, we may assume that we will probably find the answer to this second question inside this function.

When this function is called, a new filename is first prepared by concatenating the original filename and .bak. Then, the original file is moved, which means that MpCmdRun.log is renamed as MpCmdRun.log.bak. Now we have our answer: a log rotation mechanism is indeed implemented.

Here, I could have continued the reverse engineering process but I had something else in mind. Considering that knowledge, I wanted to adopt a simpler approach. The idea was to check the behavior of this mechanism under various conditions and observe the result using Procmon.

The Vulnerability

Here is what we know and what we have learnt so far:

  • Windows Defender writes some log events to C:\Windows\Temp\MpCmdRun.log (e.g.: Signature Updates).
  • Any user can create files and directories in C:\Windows\Temp\.
  • A log rotation mechanism prevents the log file from exceeding 16MB by moving it to C:\Windows\Temp\MpCmdRun.log.bak and creating a new one.

Do you see where I’m going here? Can you spot the potential issue? What would happen if there was already something named MpCmdRun.log.bak in C:\Windows\Temp\? :thinking:

To answer this question, I considered the following test protocol:

  1. Create something called MpCmdRun.log.bak in C:\Windows\Temp\.
  2. Fill C:\Windows\Temp\MpCmdRun.log with arbitrary data so that its size is close to 16MB.
  3. Trigger a Signature Update
  4. Observe the result with Procmon

If MpCmdRun.log.bak is an existing file, it is simply overwritten. We may assume that it’s the intendend behavior so this test isn’t very conclusive. Rather, the very first test case scenario that came to my mind was: what if MpCmdRun.log.bak is a directory?

If MpCmdRun.log.bak isn’t a simple file, we may assume that it cannot be simply overwritten. So, my initial assumption was that the log rotation would simply fail. Instead of creating a backup of the original log file, it would just be overwritten. Though, the behavior I observed with Procmon was far more interesting than that. Defender actually deleted the directory and then proceeded with the normal log rotation. So, I decided to redo this test but, this time, I also created files and directories inside C:\Windows\Temp\MpCmdRun.log.bak\. It turns out that the deletetion was actually recursive!

That’s interesting! Now, the question is: can we redirect this file operation with a junction?

Here is the initial setup for this test case:

  • A dummy target directory is created: C:\ZZ_SANDBOX\target.
  • MpCmdRun.log.bak is created as a directory and is set as a mountpoint to this directory.
  • The MpCmdRun.log file is filled with 16,777,002 bytes of arbitrary data.

The target directory of the mountpoint contains a folder and a file.

And here is what I observed in Procmon after executing the Update-MpSignature PowerShell command:

Defender followed the mountpoint, deleted each files and folders recursively and finally deleted the folder C:\Windows\Temp\MpCmdRun.log.bak\ itself. Nice! This means that, as a normal user, we can trick this service into deleting any file or folder we want on the filesystem…

Exploitability

As we saw in the previous part, exploitation is quite straightforward. The only thing we would have to do is create the directory C:\Windows\Temp\MpCmdRun.log.bak\ and set it as a mountpoint to another location on the filesystem.

Note: actually it’s not that simple because an additional trick is required if we want to perform a targeted file or directory deletion but I won’t discuss this here.

We face one practical issue though: how much time would it require to fill the log file until its size exceeds 16MB, which is quite a high value for a simple log file. Therefore, I did several tests and measured the time required by each command. Then, I extrapolated the results in order to estimate the overall time it would take. It should be noted that the Update-MpSignature command cannot be run multiple times in parallel (which makes sense for an update process).

Test #1

As a first test, I chose a naive approach. I ran the Update-MpSignature command a hundred times and measured the overall time it would take.

Here is the result of this first test. With this technique, it would take more than 22 hours to fill the file and trigger the vulnerability if we ran the Update-MpSignature command in a loop.

DESCRIPTION TIME FILE SIZE # OF CALLS
Raw data for 100 calls 650s (10m 50s) 136,230 bytes 100
Estimated time to reach the target file size 80,050s (22h 14m 10s) 16,777,216 bytes 12,316

That’s not very practical, to say the least.

Test #2

After test #1, I checked the documentation of the Update-MpSignature command to see if it could be tweaked in order to speed up the entire process. This command has a very limited set of options but one of them caught my eye.

This command accepts an UpdateSource as a parameter, which is actually an enumeration as we can see on the previous screenshot. When using most of the available values, an error message is immediately returned and nothing is written to the log file so they would be useless for this exploit scenario.

Though, when using the InternalDefinitionUpdateServer value, I observed an interesting result.

Since my VM is a standalone installation of Windows, it isn’t configured to use an “internal server” for the updates. Instead, they are received directly from MS servers, hence the error message.

The main benefit of this method is that the error message is returned almost instantly but the event is still written to the log file, which makes it a very good candidate for the exploit in this particular scenario.

Therefore, I ran this command a hundred times as well and observed the result.

This time, the 100 calls took less than 4 seconds to complete. This wasn’t enough for calculating relevant stats so I ran the same test with 10,000 calls this time.

Here is the result of this second test.

DESCRIPTION TIME FILE SIZE # OF CALLS
Raw data for 10,000 calls 363s (6m 2s) 2,441,120 bytes 10,000
Estimated time to reach the target file size 2,495s (41m 35s) 16,777,216 bytes 68,728

With this slight adjustment, the overall operation would take around 40 minutes, instead of more than 22 hours with the previous command. This would therefore drastically reduce the amount of time required to fill the log file. It should also be noted that these values correspond to the worst-case scenario, where the log file would initially be empty.

I considered that this value was acceptable so I implemented this trick in a Proof-of-Concept. Here is a screenshot showing the result.

Starting from an empty log file, the PoC took around 38 minutes to complete, which is very close to the estimation I had made previously.

As a side note, if you paid attention to the last screenshot, you probably noticed that I specified C:\ProgramData\Microsoft\Windows\WER as the target directory to delete. I didn’t choose this path randomly. I chose this one because, once this folder has been removed, you can get code execution as NT AUTHORITY\SYSTEM as explained by @jonaslyk in this post: From directory deletion to SYSTEM shell.

Conclusion

This is probably the last blog post in which I write about this kind of privileged file operation abuse. In an email sent to all vulnerability researchers, Microsoft announced that they changed the scope of their bounty. Therefore, such exploit is no longer eligible. This decision is justified by the fact that a generic patch is in development and will address this entire bug class.

I have to say that this decision sounds a bit premature. It would be understandable if this patch was already implemented in the latest Insider Preview build of Windows 10 but that’s not the case. I would assume that this is more of an economic decision than a pure technical matter as such bounty program probably represents a cost of hundreds of thousands of dollars per month.

Links & Resources

Windows .Net Core SDK Elevation of Privilege

There was a weird bug in the DotNet Core Toolset installer that allowed any local user to elevate their privileges to SYSTEM. In this blog post, I want to share the details of this bug that was silently (but only partially) fixed despite not being acknowledged as a vulnerability by Microsoft.

Introduction

In March 2020, jonaslyk told me about a weird bug he encountered on his personal computer. The SYSTEM’s PATH environment variable was populated with a path that was seemingly related to DotNet. The weird thing was that this path pointed to a non-admin user folder. So, I checked on my own machine but, although there was a DotNet-related path, it pointed to a local admin folder. Anyway, if the path of a user-owned folder can be appended to this environment variable, that means code execution as SYSTEM. So, we decided to work together on this strange case and see what we could come up with.

The Initial Setup

We started with a clean and fully updated installation of Windows 10. In this initial state, here is the default value of the SYSTEM account’s PATH environment variable. As a reminder S-1-5-18 is the Security Identifier (SID) of the LocalSystem account.

reg query "HKU\S-1-5-18\Environment" /v Path

Then we installed Visual Studio Community 2019 (link). Once installed, we selected the .Net desktop development component in the Installer.

After clicking the “Install” button, the packages are downloaded and installed.

We are looking for a registry key modification so we can use Process Monitor to easily monitor what’s going on in the background.

Things get interesting when the “.Net Core toolset” is installed. We can see a RegSetValue operation originating from an executable called dotnet.exe on HKU\.DEFAULT\Environment\PATH. After this event, we can see that the PATH value in HKU\S-1-5-18\Environment is indeed different.

We may notice two potential issues here:

  1. The variable %USERPROFILE% is resolved to the current user’s home folder instead of the SYSTEM account’s home folder.
  2. Another path, pointing to a user-owned folder once again, is appended to the the SYSTEM account’s PATH.

In these two cases, the current user is a local administrator so the consequences of such modifications are somewhat limited. Though, they shouldn’t occur because they may have unintended side effects (e.g.: UAC bypass).

After reading this, you might have a feeling of déja vu. If so, it means that you probably stumbled upon this post by @RedVuln at some point: .Net System Persistence / bypassuac / Privesc. It looks like he found this bug almost at the same time Jonas and I were working on it. But there is a problem, all of this can be achieved only as an administrator because the installation of the DotNet SDK requires such privileges. Or does it?

The Actual Privilege Escalation Vulnerability

In the previous part, we saw that the installation process of the .Net SDK had some potentially unintended consequences on the Path Environment variable of the SYSTEM account. Though, strictly speaking, this doesn’t lead to an Elevation of Privilege.

But, what if I told you that the exact same behavior could be reproduced while being logged in as a normal user with no admin rights?

When Visual Studio is installed, several MSI files seem to be copied to the C:\Windows\Installer folder. Since we observed that the RegSetValue operation originated from an executable called dotnet.exe, we can try to search for this string in these files. Here is what we get using the findstr command.

cd "C:\Windows\Installer"
findstr /I /M "dotnet.exe" "*.msi"

Great! We have two matches. What we can do next is try to run each of these files as a normal user with the command msiexec /qn /a <FILE> and observe the result on the SYSTEM account’s Environment Path variable in the registry.

Running the first MSI file, we don’t see anything. However, running the second MSI file, we observe the exact same operation which initially occurred when we installed the DotNet SDK as an administrator.

This time though, because the MSI file was run by the user lab-user, the path C:\Users\lab-user\.dotnet\tools is appended to the SYSTEM account’s PATH environment variable. As a result, this user can now get code execution as SYSTEM by planting a DLL and waiting for a service to load it. This can be achieved - on Windows 10 - by hijacking the WptsExtensions.dll DLL which is loaded by the Task Scheduler service upon startup, as described by @RedVuln in his post.

Root Cause Analysis

The exploitation of this bug is trivial so I will focus on the root cause analysis instead, which turned out to be quite interesting for me.

The question is: where do we start? Well, let’s start at the beginning… :slightly_smiling_face:

We have a Process Monitor dump file that contains the RegSetValue event we are interested in. That’s a good starting point. Let’s see what we can learn from the Stack Trace.

We can see that the dotnet.exe executable loads several DLLs and then loads several .Net assemblies.

Looking at the details of the Process Start operation, we can see the following command line:

"C:\Program Files\dotnet\\dotnet.exe" exec "C:\Program Files\dotnet\\sdk\3.1.200\dotnet.dll" internal-reportinstallsuccess ""

From this command line, we may assume that the Win32 dotnet.exe executable is actually a wrapper for the dotnet.dll assembly, which is loaded with the following arguments: internalreportinstallsuccess "".

Therefore, reversing this assembly should provide us with all the answers we are looking for:

  1. How does the executable evaluate the .Net Core tools path?
  2. How does the executable add the path to the SYSTEM account’s PATH in the registry?

To inspect this assembly, I used dnSpy. It’s definitely the best tool I’ve used so far for this kind of task.

The Program’s Main starts by calling Program.ProcessArgs().

Several things happen in this function but the most important part is framed in red on the below screenshot.

Indeed, the function with the name ConfigureDotNetForFirstTimeUse() looks like a good candidate to continue the investigation.

This assumption is confirmed when looking at the content of the function because we are starting to see some references to the “Environment Path”.

The method CreateEnvironmentPath() creates an instance of an object implementing the IEnvironmentProvider interface, depending on the underlying Operating System. Thus, it would be a new WindowsEnvironmentPath object here.

The object is instantiated based on a dynamically generated path, which is formed by the concatenation of some string and "tools".

This DotnetUserProfileFolderPath string itself is the concatenation of some other string and ".dotnet".

The DotnetHomePath string is generated based on the value of an Environment variable.

The name of the variable depends on PlateformHomeVariableName, which would be "USERPROFILE" here because the OS is Windows.

To conclude this first part of the analysis, we know that the DotNet tools’ path follows the following scheme: <ENV.USERPROFILE>\.dotnet\tools, where the value of ENV.USERPROFILE is returned by Environment.GetEnvironmentVariable(). So far, that’s consistent with what we observed with Process Monitor so we must be on the right track.

If we check the documentation of Environment.GetEnvironmentVariable(), we can read that, by default, the value is retrieved from the current process if an EnvironmentVariableTarget isn’t specified.

Now, if we take another look at the details of the Process Start operation in Process Monitor, we can see that the process uses the current user’s environment, although it’s running as NT AUTHORITY\SYSTEM. Therefore, the final tools path is resolved to C:\Users\lab-user\.dotnet\tools.

We now know how the path is determined so we have the answer to our first question. We now need to find out how this path is handled afterwards.

To answer the second question, we may go back to the Program.ConfigureDotNetForFirstTimeUse() method and see what’s executed after the CreateEnvironmentPath() function call.

Once the tools path has been determined, a new DotnetFirstTimeUseConfigurer object is created and the Configure() method is immediately called. At this point, the path information is stored in the EnvironmentPath object identified by the pathAdder variable.

In this method, the most relevant piece of code is framed in red on the below screenshot, where the AddPackageExecutablePath() method is invoked.

This method is very simple. The AddPackageExecutablePathToUserPath() method is called on the EnviromentPath object.

The content of the AddPackageExecutablePathToUserPath() method finally gives us the answer to our second question.

First, this method retrieves the value of the PATH environment variable but, this time, it uses a slightly different way to do so. It invokes GetEnvironmentVariable with an additional EnvironmentVariableTarget parameter, which is set to 1.

From the documentation, we can read that if this parameter is set to 1, the value is retrieved from the HKEY_CURRENT_USER\Environment registry key. The current user being NT AUTHORITY\SYSTEM here, the value is retrieved from HKU\S-1-5-18\Environment.

The problem is that this applies to the SetEnvironmentVariable() method as well. Therefore, C:\Users\lab-user\.dotnet\tools\ is appended to the Path Environment variable of the LOCAL SYSTEM account in the registry.

As a conclusion, the .Net Core toolset path is created based on the current user’s environment but is applied to the LOCAL SYSTEM account in the registry because the process is running as NT AUTHORITY\SYSTEM, hence the vulnerability.

Conclusion

The status of this vulnerability is quite unclear. Since it wasn’t officially acknowledged by Microsoft, there is no CVE ID associated to this finding. Though, as mentioned in the introduction, it has partially been fixed. Namely, the C:\Users\<USER>\.dotnet\tools path is no longer appended to the Path if you use one of the latest .NET Core installers.

Now, what can you do to make sure your machine isn’t affected by this vulnerability?

First, check the following value in the registry.

C:\Windows\System32>reg query HKU\S-1-5-18\Environment /v Path

HKEY_USERS\S-1-5-18\Environment
    Path    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;

If you see something that is different from what is shown above, you may restore the default value using the following command as an administrator:

C:\Windows\System32>reg ADD HKU\S-1-5-18\Environment /v Path /d "%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;" /F
The operation completed successfully.

Then, you can update Visual Studio or the .Net SDK and check the registry once again. The “tools” folder should no longer be present.

Unfortunately, according to my latest tests, the %USERPROFILE% variable still gets resolved to the current user’s “home” folder. This means that the Path is still altered when installing the .Net SDK. Thankfully, this one cannot be exploited for local privilege escalation because the corresponding folder is owned by an administrator.

Links & Resources

Abusing Group Policy Caching

In this post I will show you how I discovered a severe vulnerability in the so-called “Group Policy Caching” which was fixed (among other GP vulnerabilities) in CVE-2020-1317

A standard domain user can perform, via the “gpsvc” service,  arbitrary file overwrite with SYSTEM privileges  by altering behavior of “Group Policy Caching”.

Cool, isn’t it?

The “Group Policy caching” feature is  configurable since Windows 2012/8, here you can learn more about it: https://specopssoft.com/blog/things-work-group-policy-caching/

For our goal, all you need is just a domain user and at least the “Default Domain” policy with no special configuration and no special settings in “Group Policy Caching” are needed. Only default configurations 😉

The “Group Policy caching files” are stored under subfolders:

  • C:\users\<domain_user>\AppData\Local\GroupPolicy\datastore\0

Each time a standard domain user performs ” a gpupdate /force”, the directory is created (if it doesn’t exist) and then populated depending on the group policies which are applied.

The domain user has full control over the “Datastore” directory but only read access under the “0” subfolder.

This can be easilly overriden, if we just rename the “Datastore” folder and create a new “Datastore” folder with the “0” subfolder.

Now let’s create a file in the “0” folder:

and then run a “gpupdate /force” watching what happens in procmon:

As we can see, the file is opened without impersonation (SYSTEM) and 2 SetSecurity calls are made. The resulting perms on the test file are:

Very strange, we lost the full control over the file, probably the second SetSecurity call, but the first one?

Let’s move a step forward, now we create a junction under the “0” folder pointing to a protected directory, for example: “c:\users\administrator\documents”

Note: We obviously need to rename the actual “Datastore” folder and create a new one with the “0” subfolder given that now we have again only read access

Again, in procmon let’s see what happens with a new “gpupdate /force”

A first SetSecurity call is done on “desktop.ini” and then the processing stops due to an access denied on “MyMusic”.

What will be the resulting perms of “desktop.ini”.. ? Guess what, full control for our user!

Now we have a clear vision of the whole picture:

When Group Policy Caching is processed, the “gpsvc” service, running as SYSTEM, lists all the files and folders starting from the local “DataStore” root directory and performs several “SetSecurity” operations on subfolders and files. The first “SetSecurity” will always grant “Full control” to the current user, the second one only read access.

Being able to obtain full control over a SYSTEM protected file (during the first SetSecurity call) EoP should be super-easy, don’t you agree?

For example, we can alter the contents of “printconfig.dll”, start an XPS Print job and gain a SYSTEM shell, as described in my post: https://decoder.cloud/2019/11/13/from-arbitrary-file-overwrite-to-system/

To achieve my goal, I created a very simple POC, here the relevant parts:

When the targetfile is accessed, an “opportunistic lock” is set and after the new junction is moved to “\RPC Control” which will in fact stop the processing and the second SetSecurity call.

Our domain user now has full control over the file!

Microsoft fixed this security “bug” by simply moving the “Group Policy” folders under c:\programdata\microsoft\grouppolicy\users\<sid> which is readonly for standard users….

That’s all 🙂

gp0

decoderblogblog

When ntuser.pol leads you to SYSTEM

This is a super short writeup following my previous post . My last sentence was a kind of provocation because I already knew that there were at least 2 “bypasses” for CVE-2020-1317.

I did not submit them because I totally disagree with recent MSRC changes in their policies, so when I discovered that they were fixed with latest security updates I decided to share my findings.

So here we are, this is the first one (next to come hopefully soon). I won’t dig too much in details. Let’s start from Group Policy caching again. This “feature” is enabled by default but can be turned off and in Windows Servers it’s disabled by default.

When GP Caching is disabled a new privilege escalation is possible (more on this later). If certain policies are configured, the file “ntuser.pol” is written in c:\programdata\microsoft\grouppolicy\users\<sid>.

The domain user has Full Control (!) over this directory and on ntuser.pol too. The file has hidden and system attributes but we can of course remove them..

Let’s see what happens with procmon..

This is interesting!

The file ntuser.pol is renamed to tempntuser.pol by the group policy service running as SYSTEM. Got it? At the end “tempntuser.pol” is deleted:

So we can obviously perform an “arbitrary delete” by abusing our so loved symlinks via “\RPC Control” but the rename operation can permit us to write a file (with filename and contents under our control) with SYSTEM privileges and that’s a real EoP! We also need to inhibit the file deletion, otherwise our file won’t exist any more at the end of the process.

The procedure is rather simple, I think that this simple batch file is more than sufficient 🙂

test.dll” is our “source” file that we will put in a directory where we have full control, because during move operations, the file maintains his source ACL

The “poc.exe” simply waits until the file is created in our target directory and then places an oplock in order to prevent the deletion (which will fail because of sharing violations)

And at the very end we have our file written in System32 with full control over it!!

But why do we have full control under the <SID> directory? Well, what I observed is that when group policy caching is enabled, a “Datastore” sub-directory is created and correct permissions are then set.. (see also my previous post)

That’s all 😉

When a stupid oplock leads you to SYSTEM

As promised in my previous post, I’m going to show you the second method I found in order to circumvent CVE-2020-1317.

Prerequisites

  1. Domain user has access to a domain joined Windows machine
  2. Domain user must be able to create a subdirectory under “Datastore\0” which is theoretically no more possible. But as we will see there are at least two method to bypass the limitation.

First Method: “Domain Group Policy” abuse

We will implement a Domain User Group Policy which produces “files”, typically script files such as Logon/Logoff scripts or configuration preferences xml files. This is a very common scenario in AD enterprise domains

Steps to reproduce

  • In this case we are going to configure user preferences under “User Configuration\Preferences\Windows Setting” in Group Policy Management. But it’s just an example, logon/logoff scripts will work too.
  • On the Domain Controller create a Domain preferences policy (for example “Ini Files”)

  • Login with domain user credentials on a domain joined computer
  • Check if the “Group Policy Cache” has been saved under the directory:
    • C:\programdata\microsoft\GrouPolicy\users\<SID>\Datastore (the entire directory, subdirectories and files are read-only for users after CVE-2020-1317)
    • If not, perform a “gpupdate /force” via command prompt
  • We can observe that the preference “inifiles.xml” has been created  
  • Now we will place an exclusive “oplock” [1] on this file:

  • On a new command prompt, we will force a new “gpupdate /force”. Oplock will be triggered and policy processing hangs:
  • Here comes the fun part(!), when Group Policy Caching is processed, the “gpsvc” service, running as SYSTEM, lists all the files and folders starting from the local “DataStore” root directory and performs several “SetSecurity” operations on subfolders and files. The first “SetSecurity” will always grant “Full control” to the current user, the second one only read access. This is the first run and we should have full control over the “Datastore” folders. In a new command prompt, we will try to create a directory under “Datastore\0” folder:
  • Once created, we disable inheritance and remove permissions to SYSTEM and Administrator Group. Why? Because when we release the “oplock”, the “second run” will occur during which the domain user will be granted read only access. Given that the process is running as SYSTEM, this operation will be denied. This can be observed in following procmon screenshot:
  • Now we have a directory with full control where we can place a symlink via \RPC Control to a SYSTEM protected file… and, just as an example, alter the contents of “printconfig.dll”, start an XPS Print job and gain a SYSTEM shell, as described in previous blog posts [3]

Second Method: redirect Windows Error Processing “ReportQueue” directory

In this case we are going to redirect the “C:\ProgramData\Microsoft\Windows\WER\ReportQueue” folder to the “Datastore\0” directory. This folder is normally empty if no error reporting process is running.

Steps to reproduce

  • Create the junction. In this case we are using CreateMountPoint.exe[3] , we can also use the built-in “mklink /j “ command but we need to remove the directory before

  • Ensure that optional diagnostics data etc..  has been turned on in Settings->Privacy->Diagnostics&feedback
  • Launch “Feedback Hub” and report a problem. Remember to check “Save a local copy…”
  • You will observe that the redirected “ReportQueue” directory will be populated. As soon as this happens, perform a “gpupdate /force”
  • Once the update has finished, you will be able to create a directory under the “Datastore\0” folder. At this point, you can proceed with same technique explained before

Conclusions:

I think to have successfully demonstrated that even after CVE-2020-1317 bug fix, there were still several possibilities to abuse junctions and Set Security calls in group policy client in order to perform an elevation of privilege starting from a low privileged domain user.

That’s all 😉

❌