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.
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.
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.
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.
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)
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.
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:
<GUID>.scratch
directory to be created and create new object manager symbolic link Report.<GUID>.diagsession
that points to C:\\ProgramData
.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.<GUID>.scratch
directory to be created and create a new object manager symbolic link Report.<GUID>.diagsession
that points to C:\\ProgramData\\Microsoft
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.C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe
binary.Setup WMI provider
in repair mode.MofCompiler.exe
binary to be created by the installer and replace it with cmd.exeAfter 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:
This blog post is written by Filip Dragovic.