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.
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 “184.108.40.206”:
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:
A valid Avira signed executable that has a ProductVersion greater than the installed version of Avira.VPNService.exe
When this executable starts, it looks for “VERSION.dll” in the current working directory
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.
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”.
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.
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.
Version: Avira Optimizer < 220.127.116.117 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:
Prepare our payload. In this case, it is a .NET executable named Avira.SystemSpeedup.RealTime.Client.exe that starts cmd.exe
Clone the certificate off of a valid Avira file and apply it to our payload
Write code that injects into a valid Avira process, loads up Avira.Optimizer.Common.Tools.dll and instantiates an instance of the OptimizerHostClient class
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 18.104.22.1687. After glancing at the fix, Avira now utilizes WinVerifyTrust() and an apparent path whitelist to ensure started processes aren’t influenced.
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
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:
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.
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
Version: Razer Surround 22.214.171.124 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:
Installed services (both the service permissions and the service executable/path permission)
Named pipes (and their ACLs)
Log file permissions in folders like C:\ProgramData
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
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.
When the Avira VPN service (Avira.VPNService.exe, version 126.96.36.199160) 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:
“AviraVPNInstaller.exe” has to be signed by Avira
The signature on “AviraVPNInstaller.exe” has to be valid (any modification of that file would invalidate the signature
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:
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.
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:
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.
Write a custom assembly that implements the IPackage interface from the SimpleInjector project
Add malicious logic in the “RegisterServices()” method inside the IPackage interface
Compile the assembly and use a tool such as SigPirate to clone the certificate chain from a valid Razer assembly
Drop the final malicious assembly into “C:\ProgramData\Razer\Synapse3\Service\bin”
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.
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.
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:
Find bug->Disclose bug->Vendor’s eyes open widely at bug->Bug is fixed and CVE possibly issued (with relevant acknowledgement)->case closed
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:
Internal vendor politics/reorg
Case handler experience/work ethic/communication (!!!!)
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):
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:
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:
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
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.
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.
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:
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.