Windows .Net Core SDK Elevation of Privilege
There was a weird bug in the DotNet Core Toolset installer that allowed any local user to elevate their privileges to SYSTEM. In this blog post, I want to share the details of this bug that was silently (but only partially) fixed despite not being acknowledged as a vulnerability by Microsoft.
Introduction
In March 2020, jonaslyk told me about a weird bug he encountered on his personal computer. The SYSTEM’s PATH
environment variable was populated with a path that was seemingly related to DotNet. The weird thing was that this path pointed to a non-admin user folder. So, I checked on my own machine but, although there was a DotNet-related path, it pointed to a local admin folder. Anyway, if the path of a user-owned folder can be appended to this environment variable, that means code execution as SYSTEM. So, we decided to work together on this strange case and see what we could come up with.
The Initial Setup
We started with a clean and fully updated installation of Windows 10. In this initial state, here is the default value of the SYSTEM account’s PATH
environment variable. As a reminder S-1-5-18
is the Security Identifier (SID) of the LocalSystem
account.
1
reg query "HKU\S-1-5-18\Environment" /v Path
Then we installed Visual Studio Community 2019 (link). Once installed, we selected the .Net desktop development component in the Installer.
After clicking the “Install” button, the packages are downloaded and installed.
We are looking for a registry key modification so we can use Process Monitor to easily monitor what’s going on in the background.
Things get interesting when the “.Net Core toolset” is installed. We can see a RegSetValue
operation originating from an executable called dotnet.exe
on HKU\.DEFAULT\Environment\PATH
. After this event, we can see that the PATH
value in HKU\S-1-5-18\Environment
is indeed different.
We may notice two potential issues here:
- The variable
%USERPROFILE%
is resolved to the current user’s home folder instead of the SYSTEM account’s home folder. - Another path, pointing to a user-owned folder once again, is appended to the the SYSTEM account’s
PATH
.
In these two cases, the current user is a local administrator so the consequences of such modifications are somewhat limited. Though, they shouldn’t occur because they may have unintended side effects (e.g.: UAC bypass).
After reading this, you might have a feeling of déja vu. If so, it means that you probably stumbled upon this post by @RedVuln at some point: .Net System Persistence / bypassuac / Privesc. It looks like he found this bug almost at the same time Jonas and I were working on it. But there is a problem, all of this can be achieved only as an administrator because the installation of the DotNet SDK requires such privileges. Or does it?
The Actual Privilege Escalation Vulnerability
In the previous part, we saw that the installation process of the .Net SDK had some potentially unintended consequences on the Path Environment variable of the SYSTEM account. Though, strictly speaking, this doesn’t lead to an Elevation of Privilege.
But, what if I told you that the exact same behavior could be reproduced while being logged in as a normal user with no admin rights?
When Visual Studio is installed, several MSI files seem to be copied to the C:\Windows\Installer
folder. Since we observed that the RegSetValue
operation originated from an executable called dotnet.exe
, we can try to search for this string in these files. Here is what we get using the findstr
command.
1
2
cd "C:\Windows\Installer"
findstr /I /M "dotnet.exe" "*.msi"
Great! We have two matches. What we can do next is try to run each of these files as a normal user with the command msiexec /qn /a <FILE>
and observe the result on the SYSTEM account’s Environment Path variable in the registry.
Running the first MSI file, we don’t see anything. However, running the second MSI file, we observe the exact same operation which initially occurred when we installed the DotNet SDK as an administrator.
This time though, because the MSI file was run by the user lab-user
, the path C:\Users\lab-user\.dotnet\tools
is appended to the SYSTEM account’s PATH
environment variable. As a result, this user can now get code execution as SYSTEM by planting a DLL and waiting for a service to load it. This can be achieved - on Windows 10 - by hijacking the WptsExtensions.dll
DLL which is loaded by the Task Scheduler service upon startup, as described by @RedVuln in his post.
Root Cause Analysis
The exploitation of this bug is trivial so I will focus on the root cause analysis instead, which turned out to be quite interesting for me.
The question is: where do we start? Well, let’s start at the beginning…
We have a Process Monitor dump file that contains the RegSetValue
event we are interested in. That’s a good starting point. Let’s see what we can learn from the Stack Trace.
We can see that the dotnet.exe
executable loads several DLLs and then loads several .Net assemblies.
Looking at the details of the Process Start
operation, we can see the following command line:
1
"C:\Program Files\dotnet\\dotnet.exe" exec "C:\Program Files\dotnet\\sdk\3.1.200\dotnet.dll" internal-reportinstallsuccess ""
From this command line, we may assume that the Win32 dotnet.exe
executable is actually a wrapper for the dotnet.dll
assembly, which is loaded with the following arguments: internalreportinstallsuccess ""
.
Therefore, reversing this assembly should provide us with all the answers we are looking for:
- How does the executable evaluate the .Net Core tools path?
- How does the executable add the path to the SYSTEM account’s
PATH
in the registry?
To inspect this assembly, I used dnSpy. It’s definitely the best tool I’ve used so far for this kind of task.
The Program’s Main
starts by calling Program.ProcessArgs()
.
Several things happen in this function but the most important part is framed in red on the below screenshot.
Indeed, the function with the name ConfigureDotNetForFirstTimeUse()
looks like a good candidate to continue the investigation.
This assumption is confirmed when looking at the content of the function because we are starting to see some references to the “Environment Path”.
The method CreateEnvironmentPath()
creates an instance of an object implementing the IEnvironmentProvider
interface, depending on the underlying Operating System. Thus, it would be a new WindowsEnvironmentPath
object here.
The object is instantiated based on a dynamically generated path, which is formed by the concatenation of some string and "tools"
.
This DotnetUserProfileFolderPath
string itself is the concatenation of some other string and ".dotnet"
.
The DotnetHomePath
string is generated based on the value of an Environment variable.
The name of the variable depends on PlateformHomeVariableName
, which would be "USERPROFILE"
here because the OS is Windows.
To conclude this first part of the analysis, we know that the DotNet tools’ path follows the following scheme: <ENV.USERPROFILE>\.dotnet\tools
, where the value of ENV.USERPROFILE
is returned by Environment.GetEnvironmentVariable()
. So far, that’s consistent with what we observed with Process Monitor so we must be on the right track.
If we check the documentation of Environment.GetEnvironmentVariable()
, we can read that, by default, the value is retrieved from the current process if an EnvironmentVariableTarget
isn’t specified.
Now, if we take another look at the details of the Process Start
operation in Process Monitor, we can see that the process uses the current user’s environment, although it’s running as NT AUTHORITY\SYSTEM
. Therefore, the final tools path is resolved to C:\Users\lab-user\.dotnet\tools
.
We now know how the path is determined so we have the answer to our first question. We now need to find out how this path is handled afterwards.
To answer the second question, we may go back to the Program.ConfigureDotNetForFirstTimeUse()
method and see what’s executed after the CreateEnvironmentPath()
function call.
Once the tools path has been determined, a new DotnetFirstTimeUseConfigurer
object is created and the Configure()
method is immediately called. At this point, the path information is stored in the EnvironmentPath
object identified by the pathAdder
variable.
In this method, the most relevant piece of code is framed in red on the below screenshot, where the AddPackageExecutablePath()
method is invoked.
This method is very simple. The AddPackageExecutablePathToUserPath()
method is called on the EnviromentPath
object.
The content of the AddPackageExecutablePathToUserPath()
method finally gives us the answer to our second question.
First, this method retrieves the value of the PATH
environment variable but, this time, it uses a slightly different way to do so. It invokes GetEnvironmentVariable
with an additional EnvironmentVariableTarget
parameter, which is set to 1
.
From the documentation, we can read that if this parameter is set to 1
, the value is retrieved from the HKEY_CURRENT_USER\Environment
registry key. The current user being NT AUTHORITY\SYSTEM
here, the value is retrieved from HKU\S-1-5-18\Environment
.
The problem is that this applies to the SetEnvironmentVariable()
method as well. Therefore, C:\Users\lab-user\.dotnet\tools\
is appended to the Path Environment variable of the LOCAL SYSTEM account in the registry.
As a conclusion, the .Net Core toolset path is created based on the current user’s environment but is applied to the LOCAL SYSTEM account in the registry because the process is running as NT AUTHORITY\SYSTEM
, hence the vulnerability.
Conclusion
The status of this vulnerability is quite unclear. Since it wasn’t officially acknowledged by Microsoft, there is no CVE ID associated to this finding. Though, as mentioned in the introduction, it has partially been fixed. Namely, the C:\Users\<USER>\.dotnet\tools
path is no longer appended to the Path if you use one of the latest .NET Core installers.
Now, what can you do to make sure your machine isn’t affected by this vulnerability?
First, check the following value in the registry.
C:\Windows\System32>reg query HKU\S-1-5-18\Environment /v Path
HKEY_USERS\S-1-5-18\Environment
Path REG_EXPAND_SZ %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
If you see something that is different from what is shown above, you may restore the default value using the following command as an administrator:
C:\Windows\System32>reg ADD HKU\S-1-5-18\Environment /v Path /d "%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;" /F
The operation completed successfully.
Then, you can update Visual Studio or the .Net SDK and check the registry once again. The “tools” folder should no longer be present.
Unfortunately, according to my latest tests, the %USERPROFILE%
variable still gets resolved to the current user’s “home” folder. This means that the Path is still altered when installing the .Net SDK. Thankfully, this one cannot be exploited for local privilege escalation because the corresponding folder is owned by an administrator.
Links & Resources
.Net System Persistence / bypassuac / Privesc
https://redvuln.com/net_privesc/dnSpy
dnSpy