ActiveBreach

CVE-2024-20656 – Local Privilege Escalation in the VSStandardCollectorService150 Service

Overview

Visual Studio is a complex and powerful IDE developed by Microsoft and comes with a lot of features that can be interesting from a red team perspective.

During this blog post we will explore the VSStandardCollectorService150 service which used for diagnostic purposes by Visual Studio and is running in NT AUTHORITY\SYSTEM context, and how it can be abused to perform arbitrary file DACL reset in order to escalate privileges.

Vulnerability Discovery

When Visual Studio is installed with C/C++ support VSStandardCollectorService150 service is created and is configured to run in NT AUTHORITY\SYSTEM context, as shown below:

PS C:\\Users\\doom> sc.exe qc VSStandardCollectorService150
[SC] QueryServiceConfig SUCCESS

SERVICE_NAME: VSStandardCollectorService150
        TYPE               : 10  WIN32_OWN_PROCESS
        START_TYPE         : 3   DEMAND_START
        ERROR_CONTROL      : 0   IGNORE
        BINARY_PATH_NAME   : "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\Common\\DiagnosticsHub.Collection.Service\\StandardCollector.Service.exe"
        LOAD_ORDER_GROUP   :
        TAG                : 0
        DISPLAY_NAME       : Visual Studio Standard Collector Service 150
        DEPENDENCIES       :
        SERVICE_START_NAME : **LocalSystem**
PS C:\\Users\\doom>

The service is configured to run on demand which usually means that some form of IPC will be implemented to serve as a trigger to start service (spoiler alert it was COM 🤮).

When looking for file operation vulnerabilities in any software, it is good practice to start by simply using the software for intended purpose and analyse the behaviour of any privileged services.

In this instance we will start Sysinternal’s Procmon tool and specify Process Name to be StandardCollector.Service.exe

Next we will create a simple C/C++ application that we will debug which will trigger the VSStandardCollectorService150 service to collect necessary data about application that is being debugged:

When we start debugging the process, either by pressing F5 or via GUI the VSStandardCollectorService150 service is started:

As soon as the debugging started, multiple directories and files as created inside the C:\Windows\Temp directory as we can see in screenshot below:

It is usually a good sign when privileged services create directories inside c:\\windows\\temp\\ as new directories will by default inherit permissions from the parent folder, which in this case would allow low privilege users to create sub-folders, files and more importantly in this case, junction folders.

Unfortunately for us the collector service would change the default inherited permissions granting only read permissions to the user that is using visual studio:

PS C:\\Windows\\system32> .\\cacls.exe C:\\Windows\\Temp\\76914557-4A42-4586-B0D9-C8904F9BFEFF.scratch
C:\\Windows\\Temp\\76914557-4A42-4586-B0D9-C8904F9BFEFF.scratch BUILTIN\\Administrators:F
BUILTIN\\Administrators:(OI)(CI)(IO)F
NT AUTHORITY\\SYSTEM:F
NT AUTHORITY\\SYSTEM:(OI)(CI)(IO)F
doompc\\doom:(OI)(CI)(special access:)
READ_CONTROL
SYNCHRONIZE
FILE_GENERIC_READ
FILE_READ_DATA
FILE_READ_EA
FILE_READ_ATTRIBUTES

Moreover, the service would implement another layer of protection by creating a temporary file that cannot be deleted by a standard user.

This file is created with the DELETE_ON_CLOSE flag in CreateFile API and does not share any access to other processes. In short, this will prevent other processes from opening a handle on the file, and file will be removed once handle is closed.

So far, we cannot exploit any of these file operations. The next step is to see what will happen when we stop debugging.

After ending the debugging session, a new folder is created which uses the same GUID but prepending the string Report. In our case, it looks like this C:\\Windows\\Temp\\Report.76914557-4A42-4586-B0D9-C8904F9BFEFF

A lot of file operations are performed inside this folder including file move, delete and dacl resets.

Unfortunately for us, the same protection mechanisms that we saw previously prevent us from exploiting this behaviour.

Digging deeper

Whilst researching the VSStandardCollectorService150 service we have found this article from Microsoft where they describe the VSDiagnostics.exe command-line tool and how it can be used to interact with the collector service.

Running the command-line tools gives a help menu with a few interesting options:

PS C:\\Program Files\\Microsoft Visual Studio\\2022\\Community> & 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe'

Microsoft (R) VS Standard Collector

For a detailed description of each command:
VSDiagnostics <command> /help

Commands:
start <sessionID> [/attach:<pid>[;<pid>;...]] [/launch:<executable> /launchArgs:<executableArgs>] [/loadAgent:<agentCLSID>;<agentName>[;<config>]] [/loadConfig:<configFile>] [/monitor] [/scratchLocation:<folderName>] [/package:[opt | dir]] [/symbolCachePath:<folderName>]
Start a collection session

update <sessionID> [/attach:<pid>[;<pid>;...]] [/detach:<pid>[;<pid>;...]] [/loadAgent:<agentCLSID>;<agentName>[;<config>] ...] [/lifetimeProcess:<pid>]
Update a collection session. This allows addition and removal of target processes and collector agents.

stop <sessionID> /output:<fileName>
Stop a collection session

pause <sessionID>
Pause a collection session

resume <sessionID>
Resume a collection session

status <sessionID>
Display the status of a collection session

postString <sessionID> "<messageString>" /agent:<agentCLSID>
Post a string to a collection agent

expandDiagSession <diagSession>
Expand the DiagSession archive into a subdirectory adjacent to the DiagSession

help
Print out this help message

Running the start option with /help flag gives us explanation of each option we have

PS C:\\Program Files\\Microsoft Visual Studio\\2022\\Community> & 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe' start /help

Microsoft (R) VS Standard Collector

Start a collection session

start <sessionID> [/attach:<pid>[;<pid>;...]] [/launch:<executable> /launchArgs:<executableArgs>] [/loadAgent:<agentCLSID>;<agentName>[;<config>]] [/loadConfig:<configFile>] [/monitor] [/scratchLocation:<folderName>] [/package:[opt | dir]] [/symbolCachePath:<folderName>]

  <sessionID>
      ID of the collection session - this should be a number in the range [0, 255] or a parsable Guid.

  /attach:<pid>[;<pid>;...]
      Semi-colon-delimited list of process IDs to target

  /launch:<executable>
      Path to the executable to launch

  /launchArgs:<executableArgs>
      Arguments for the executable to launch

  /loadAgent:<agentCLSID>;<agentName>[;<config>]
      Agent to be loaded - this option may be specified multiple times to load multiple agents

  /loadConfig:<configFile>
      Loads the agents and their configurations specified in the file

  /monitor
      Monitor the session after it is started - status updates will be displayed and the command will block until the session ends

  /scratchLocation:<folderName>
      Path to the desired output folder

  /package:[opt | dir]
      Options for the package flag: 'opt' - Optimized archive format, 'dir' - Directory format

  /symbolCachePath:<folderName>
      Path to the desired symbol cache folder

The /scratchLocation seems to specify the location where files/folders will be created, this is very interesting and will come handy later.

Running the following command confirmed our assumptions and the service created files in a folder that is specified by the /scratchLocation parameter:

PS C:\\Program Files\\Microsoft Visual Studio\\2022\\Community> & 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe' start 1 /scratchLocation:C:\\expl

Microsoft (R) VS Standard Collector

Session 1: {0197e42f-003d-4f91-a845-6404cf289e84}
  Running
PS C:\\Program Files\\Microsoft Visual Studio\\2022\\Community>

When we stop the diagnostic session we can notice another interesting thing, the VSDiagnostics.exe tool will by default use an optimised archive format as output while visual studio was using the directory output:

From the screenshot above we can see that file is first moved from the child directory (which is created with restrictive permissions) to the parent directory (the folder we specified in /scratchLocation) and then DACL is changed using SetNamedSecurityInfoW():

This is interesting as the DACL reset is done outside of the directory with restrictive DACLs and the file is moved to folder we control.

Finding An Exploit Primitive

At this point we have the privileged service changing permissions on a file inside a directory we control, if we can find a way to turn this directory in to a junction point we can redirect SetNamedSecurityInfoW to an arbitrary file. This is where ability to specify the scratchLocation comes handy.

If we can create a junction point that will point to some random folder we created and point the scratchLocation to the junction point then the SetNamedSecurityInfoW should follow it and redirect to a arbitrary file.

PS C:\\> cmd /c mklink /j c:\\expl c:\\expl2
Junction created for c:\\expl <<===>> c:\\expl2
PS C:\\> mkdir C:\\expl2

    Directory: C:\\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         1/10/2024   8:38 AM                expl2

We can confirm this by starting a new diagnostic session and stopping it afterwards:

If we see the blue marked line in procmon we can see that service when through our junction point as the result of CreateFile api call is REPARSE and we confirmed that we can redirect it to any file on the system.

Building An Exploit

Now that we have found a way to redirect permission changes to arbitrary files on the system, all we need to do is change the permissions of PrintConfig.dll and load it in a privileged service, right?

Well not so fast, as it turns out the call to SetNamedSecurityInfo will only propagate parent folder permissions to file as we can see below:

PS C:\\> cacls.exe C:\\expl2\\Report.0197E42F-003D-4F91-A845-6404CF289E84.diagsession
C:\\expl2\\Report.0197E42F-003D-4F91-A845-6404CF289E84.diagsession BUILTIN\\Administrators:(ID)F
NT AUTHORITY\\SYSTEM:(ID)F
BUILTIN\\Users:(ID)R
NT AUTHORITY\\Authenticated Users:(ID)C

This limits our options as the all public techniques of abusing the arbitrary dacl resets leverage some DLL that is located in system32 or sub-folders which we cannot abuse as the file would receive restrictive dacls and we will not be able to modify it.

After looking though the file system the only directory that could be abused in our scenario was c:\\programdata , because parent folder is the C: drive which grants Authenticated Users group Modify permissions

PS C:\\> icacls c:\\
c:\\ BUILTIN\\Administrators:(OI)(CI)(F)
    NT AUTHORITY\\SYSTEM:(OI)(CI)(F)
    BUILTIN\\Users:(OI)(CI)(RX)
    NT AUTHORITY\\Authenticated Users:(OI)(CI)(IO)(M)
    NT AUTHORITY\\Authenticated Users:(AD)
    S-1-15-3-65536-1888954469-739942743-1668119174-2468466756-4239452838-1296943325-355587736-700089176:(S,RD,X,RA)
    Mandatory Label\\High Mandatory Level:(OI)(NP)(IO)(NW)

Abusing the Windows Error Reporting Service

The first attempt to weaponise this vulnerability was to abuse the WER service to create an arbitrary folder for us that we can later abuse by loading SxS assemblies. This technique was documented by @jonaslyk where he abused an arbitrary file delete bug to delete the C:\\ProgramData\\Microsoft\\Windows\\WER directory and force the WER service to recreate it and create child folders too.

In our case we can gain control of the WER directory using the arbitrary file dacl reset vulnerability, direct it to our junction point, create an object manager symbolic link which will allows us to create an arbitrary directory (in the first PoC I created it was the msiexec.exe.local directory), drop combase.dll inside and load it in the MSIServer service.

While this PoC was successful locally, the MSRC could not confirm the vulnerability as they were testing on an insider version in which the WER service already had the junction protection and could not be abused.

Abusing MSI Repairs

After MSRC failed to confirm the vulnerability, I had to find a different method to abuse this dacl reset.

While looking through ProgramData directory I remembered that Visual Studio will create the MofCompiler.exe binary inside C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI directory.

This binary is related to WMI integration inside Visual Studio. The interesting bit here is that this binary will be executed with SYSTEM privileges via MSI repairs.

Many programs when installed will store their installer package inside C:\\windows\\installer directory and more then often not these installers will allow low privilege users to run them in repair mode in order to repair a broken installation.

This was also case with the Setup WMI Provider installer that comes by default with Visual Studio.

This installer also has custom actions defined and that custom action is configured to execute the C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe binary.

With this we have all pieces for our exploit, to summarise:

  • Create a dummy directory where the VSStandardCollectorService150 will write files.
  • Create a junction directory that points to a newly created directory.
  • Trigger the VSStandardCollectorService150 service by creating a new diagnostic session.
  • Wait for the <GUID>.scratch directory to be created and create new object manager symbolic link Report.<GUID>.diagsession that points to C:\\ProgramData .
  • Stop the diagnostic session.
  • Wait for the Report.<GUID>.diagsession file to be moved to the parent directory and switch the junction directory to point to \\RPC Control where our symbolic link is waiting.
  • Sleep for 5 seconds (not really important but left it there).
  • Switch the junction directory to point to a dummy directory.
  • Start a new diagnostic session.
  • Wait for <GUID>.scratch directory to be created and create a new object manager symbolic link Report.<GUID>.diagsession that points to C:\\ProgramData\\Microsoft
  • Stop the diagnostic session.
  • Wait for the Report.<GUID>.diagsession file to be moved to parent directory and switch the junction directory to point to \\RPC Control where our symbolic link is waiting.
  • After the permissions are changed we delete the C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe binary.
  • Locate and run the Setup WMI provider in repair mode.
  • Wait for our new MofCompiler.exe binary to be created by the installer and replace it with cmd.exe
  • Enjoy SYSTEM shell 🙂

Fix

After the January fix we can no longer redirect the SetNamedSecurityInfo API call and DACL reset is performed inside restricted directory:

Furthermore, the file is moved while impersonating the client:

Timeline

  • 07/20/2023 – Sent report with PoC to MSRC
  • 07/28/2023 – MSRC notifies me that they cannot they cannot reproduce vulnerability
  • 07/28/2023 – Sent additional information regarding the exploit
  • 08/14/2023 – MSRC notifies me again that they cannot reproduce vulnerability
  • 08/14/2023 – Send second exploit that works on later Windows Insider Preview
  • 08/15/2023 – MSRC confirms vulnerability
  • 08/15/2023 – Bounty awarded
  • 09/08/2023 – MSRC schedule fix for January 2024
  • 01/09/2024 – Fix released

This blog post is written by Filip Dragovic.

written by

MDSec Research

Ready to engage
with MDSec?

Copyright 2025 MDSec