Weaponizing Privileged File Writes with Windows Collector Service

In my previous post, I described how one could leverage CVE-2019-0841 to backdoor the LAPS AdmPwd.dll for EoP to NT AUTHORITY\SYSTEM. The obvious question is that if a machine is not using LAPS, what can you do…? Well Rich Warren provided one solution, by using the Windows Diagnostics Hub Standard Collector Service.

Design Ethos

Before we dive into any code, let’s think about the situation more generically.

CVE-2019-0841 allows low privileged users to overwrite the DACL on files that are owned by NT AUTHORITY\SYSTEM. Successful exploitation results in Full Control over the target file for the low priv user. This, in isolation, doesn’t result in code exec as SYSTEM, pop a SYSTEM shell or anything like that. But having Full Control over a file means that you can modify it in some way, e.g. write or delete.

The great aspect of using the Collector Service to load and run DLLs, is that it’s compatible with any similar arbitrary privileged file write vulnerability - so if you can write content into System32, regardless how, you’re golden. To that end, I decided to create a .NET Class Library to house the Collector Service code.

The idea being - you write your .NET PoC for arbitrary file write, add the CollectorService DLL as a dependancy, and call a single function within it to handle the magic of:

  1. Backing up the target file
  2. Overwriting the target file with your malicious DLL
  3. Loading and running the malicious DLL


To test the idea, I ported the original C++ PoC for CVE-2019-0841 to C#, using James Forshaw’s NtApiDotNet package. This makes creating the required hardlink relatively simple:

NtFile ntFile;
ntFile = NtFile.Open([email protected]"\??\{path}", null, FileAccessRights.MaximumAllowed);
ntFile.CreateHardlink([email protected]"\??\{settings}");

Where path is the target file to take Full Control over (C:\Windows\System32\license.rtf) and settings is the path to the users’ MSEdge settings.dat file.

My project is in the same Solution as SystemCollector, so I can a project reference.

After the exploit has verified that the user has taken control of the file, we simply add the following line:


Where filename is license.rtf.


99.9% of this code comes from Ryan Hanson’s CVE-2018-0952-SystemCollector PoC, so I won’t explain it here. For a detailed description, see his blog post. I will just conver my modifications.

The first line I added is:

var dll = DecompressDLL(Convert.FromBase64String("blah"));

Which is the same GZip (de)compression method I used in TikiTorch. This allows you to embedd your malicious DLL as a compressed, base64 encoded string, which is expanded and written to disk later.

I changed the scratch directory to [email protected]"C:\Users\{Environment.UserName}\AppData\Local\Temp".

First, we backup the original target file.

// backup
Console.WriteLine([email protected]" [>] Backing up {filename} to {scratch}");
File.Copy([email protected]"C:\Windows\System32\{filename}", [email protected]"{scratch}\{filename}");

We then overwrite the file with our DLL.

// overwrite
Console.WriteLine([email protected]" [>] Overwriting C:\Windows\System32\{filename} with malicious DLL");
File.WriteAllBytes([email protected]"C:\Windows\System32\{filename}", dll);

The final change was to replace Console.ReadLine(); with Thread.Sleep(3000);. I found that if the application closes too quickly, the Collector Service does not load the DLL.

The DLL is loaded by DiagnosticsHub.StandardCollector.Service.exe which, in my testing, closes after about 5 minutes and subsequently kills the Beacon.


In this demo, we have a foothold as a standard user. Let’s priv esc with a Cobalt Strike SMB DLL.

Solution available on GitHub.