If you aren't interested in the adventure behind this bug hunt, ATREDIS-2018-0004 is a good TL;DR and here is the Proof-of-Concept.
Process Monitor has become a favorite tool of mine for both research and development. During development of offensive security tools, I frequently use it to monitor how the tools interact with Windows and how they might be detected. Earlier this year I noticed some interesting behavior while I was debugging some code in Visual Studio and monitoring with Procmon. Normally I setup exclusion filters for Visual Studio processes to reduce the noise, but prior to setting up the filters I notice a SYSTEM process writing to a user owned directory:
When a privileged service writes to a user owned resource, it opens up the possibility of symlink attack vector, as previously shown in the Cylance privilege escalation bug I found. With the goal of identifying how I can directly influence the service's behavior, I began my research into the Standard Collector Service by reviewing the service's loaded libraries:
The library paths indicated the Standard Collector Service was a part of Visual Studio's diagnostics tools. After reviewing the libraries and executables in the related folders, I identified several of the binaries were written in .NET, including a standalone CLI tool named VSDiagnostics.exe, here is the console output:
Loading VSDiagnostics into dnSpy revealed a lot about the tool as well as how it interacts with the Standard Collector Service. First, an instance of IStandardCollectorService
is acquired and a session configuration is used to create an ICollectionSession
:
Next, agents are added to the ICollectionSession
with a CLSID and DLL name, which also stood out as an interesting user controlled behavior. It also made me remember previous research that exploited this exact behavior DLL loading behavior. At this point, it looked like the Visual Studio Standard Collector Service was very similar or the same as the Diagnostics Hub Standard Collector Service included with Windows 10. I began investigating this assumption by using OleViewDotNet to query the services for their supported interfaces:
Viewing the proxy definition of the IStandardCollectorService
revealed other familiar interfaces, specifically the ICollectionSession
interface seen in the VSDiagnostics source:
Taking note of the Interface ID ("IID"), I returned to the .NET interop library to compare the IIDs and found that they were different:
Looking deeper into the .NET code, I found that these Visual Studio specific interfaces are loaded through the proxy DLLs:
A quick review of the ManualRegisterInterfaces
function in the DiagnosticsHub.StandardCollector.Proxy.dll
showed a simple loop that iterates over an array of IIDs. Included in the array of IIDs is one belonging to the ICollectionSession
:
After I had a better understanding of the Visual Studio Collector service, I wanted to see if I could reuse the same .NET interop code to control the Windows Collector service. In order to interact with the correct service, I had to replace the Visual Studio CLSIDs and IIDs with the correct Windows Collector service CLSIDs and IIDs. Next, I used the modified code to build a client that simply created and started a diagnostics session with the collector service:
Starting Procmon and running the client resulted in several files and folders being created in the specified C:\Temp
scratch directory. Analyzing these events in Procmon showed that the initial directory creation was performed with client impersonation:
Although the initial directory was created while impersonating the client, the subsequent files and folders were created without impersonation:
After taking a deeper look at the other file operations, there were several that stood out. The image below is an annotated break down of the various file operations performed by the Standard Collector Service:
The most interesting behavior is the file copy operation that occurs during the diagnostics report creation. The image below shows the corresponding call stack and events of this behavior:
Now that I've identified user influenced behaviors, I construct a possible arbitrary file creation exploit plan:
- Obtain op-lock on merged ETL file (
{GUID}.1.m.etl
) as soon as service callsCloseFile
- Find and convert report sub-folder as mount point to
C:\Windows\System32
- Replace contents of
{GUID}.1.m.etl
with malicious DLL - Release op-lock to allow ETL file to be copied through the mount point
- Start new collection session with copied ETL as agent DLL, triggering elevated code execution
To write the exploit, I extended the client from earlier by leveraging James Forshaw's NtApiDotNet C# library to programmatically create the op-lock and mount point. The images below shows code snippet used to acquire the op-lock and the corresponding Procmon output illustrating the loop and op-lock acquisition:
Acquiring an op-lock on the file essentially stops the CopyFile
race, allows the contents to be overwritten, and provides control of when the CopyFile
occurs. Next, the exploit looks for the Report folder and scans it for the randomly named sub directory that needs to be converted to a mount point. Once the mount point is successfully created, the contents of the .etl are replaced with a malicious DLL. Finally, the .etl file is closed and the op-lock is released, allowing the CopyFile
operation to continue. The code snippet and Procmon output for this step is shown in the images below:
There are several techniques for escalating privileges through an arbitrary file write, but for this exploit, I chose to use the Collector service's agent DLL loading capability to keep it isolated to a single service. You'll notice in the image above, I did not use the mount point + symlink trick to rename the file to a .dll because DLLs can be loaded with any extension. For the purpose of this exploit the DLL simply needed to be in the System32 folder for the Collector service to load it. The image below demonstrates successful execution of the exploit and the corresponding Procmon output:
I know that the above screenshots show the exploit was run as the user "Admin", so here is a GIF showing it being ran as "bob", a low-privileged user account:
Feel free to try out the SystemCollector PoC yourself. Turning the PoC into a portable exploit for offensive security engagements is a task I'll leave to the reader. The NtApiDotNet
library is also a PowerShell module, which should make things a bit easier.
After this bug was patched as part of the August 2018 Patch Tuesday, I began reversing the patch, which was relatively simple. As expected, the patch simply added CoImpersonateClient
calls prior to the previously vulnerable file operations, specifically the CommitPackagingResult
function in DiagnosticsHub.StandardCollector.Runtime.dll
:
As previously mentioned in the Cylance privilege escalation write-up, protecting against symlink attacks may seem easy, but is often times overlooked. Any time a privileged service is performing file operations on behalf of a user, proper impersonation is needed in order to prevent these types of attacks.
Upon finding this vulnerability, MSRC was contacted with the vulnerability details and PoC. MSRC quickly triaged and validated the finding and provided regular updates throughout the remediation process. The full disclosure timeline can be found in the Atredis advisory link below.
If you have any questions or comments, feel free to reach out to me on Twitter: @ryHanson
Atredis Partners has assigned this vulnerability the advisory ID: ATREDIS-2018-0004
The CVE assigned to this vulnerability is: CVE-2018-0952