Windows Installer arbitrary content manipulation Elevation of Privilege (CVE-2020-0911)

Published on Thu 06 July 2023 by @clavoillotte

Product: Windows 10 1909, Windows Server 2019 (2004 and older versions also affected but not tested)

Type: Local Privilege Escalation

Summary: The Windows Installer accesses the MSI files in C:\Windows\Installer while impersonating the user (and using the impersonated user's device map), and trusts these files to perform elevated/privileged operations such as registry key creation. This can be abused by an unprivileged user to obtain SYSTEM privileges.

This vulnerability was patched with Windows September 2020 security updates.

Introduction

A few months ago, an interesting vulnerability in Windows Installer (from Adrian Denkiewicz) reminded Jonas Lykkegård of a vulnerability he discovered in 2020, for the exploitation of which I lent a hand. While a bit old (patched in September 2020), this vulnerability is still interesting, in part because instead of a somewhat common "sensitive resource is user-writable" or "privileged process accesses user resource without impersonation", it lies in the operations msiexec performs while impersonating the user. Its exploitation was also a bit tricky - and fun.

So, better late than never, here is the writeup for CVE-2020-0911.

Note: this article assumes some knowledge of filesystem redirection attacks, most required notions are introduced in this article.

Description

The Windows Installer (msiexec.exe) creates MSI files for managed packages in C:\Windows\Installer at install times. These packages are not user-writable, and are somewhat trusted by Installer when maintenance operations need to be performed on the associated product.

However, when an unprivileged user triggers a maintenance operation from an MSI file in C:\Windows\Installer, the Windows Installer accesses the MSI file while impersonating the user and using the impersonated user's device map.

A detailed explanation of what a device map is can be found in James Forshaw's article on a TrueCrypt/ driver vulnerability (it's also mentioned in several places like his article on symlinks and his research on DefineDosDevice), and another one in this LSA PPL bypass article from itm4n.

The (over)simplified summary is: a device map is a set of symbolic links (in the object manager, not on the filesystem) that map MS-DOS device names (such as C: or D:) to the corresponding device object (e.g. \Device\HarddiskVolume1), so that when a path like C:\Dir\File.ext is used, the C: is resolved to the appropriate device. While there is a "global" device map for the system, there is also a per-session device map that allows a user to do things like mapping a network drive to Z:, that mapping will only exists in the user's session. Turns out, an unprivileged user process can create a C: object symbolic link in the device directory of its session, thus redirecting subsequent file accesses from other processes in the session - as well as processes impersonating it.

Coming back to the Windows Installer behavior in CVE-2020-0911, we see the privileged msiexec process accessing the .msi file while impersonating the unprivileged user:

Procmon showing msiexec accessing the MSI file while impersonating the user

However, later operations performed by the privileged process (without impersonation) are presumably based on data it has read in the .msi file, such as the creation/overwrite of registry keys:

Procmon showing msiexec setting registry keys as SYSTEM

So the reading/parsing of the MSI file is performed while impersonating the unprivileged user, but later operations based on data read from the MSI file are performed with full privileges.

This can be abused by an unprivileged user to make the Windows Installer perform arbitrary maintenance operations - such as registry key creation - with SYSTEM privileges, by changing the user device map and using object directories and object symlinks to redirect the MSI file accesses to an arbitrary file.

Exploitation

Exploiting this bug is not as straightforward as just creating a symlink however, as there are some hurdles to overcome:

  • changing the C: mapping for the current session has side effects on other processes in that session that can make things misbehave (and ultimately make the session unusable)
  • same thing with large file redirections in directories that are often accessed such as C:\ and C:\Windows: this may break the target process before it reaches the interesting point, as well as other processes (if they are subject to the same device map)
  • the modified MSI file cannot be too different from the original one (still need to match product ID etc.) or the Installer will just error out and stop

To address the first two limitations, Jonas came up with a clever way of performing the device map redirection:

  • create a directory structure in the object manager that replicates the target file structure:
    • use object directories and symlinks to point to original files
    • use an object symlink to redirect the target file to the desired file
    • use shadow object directories so that accesses to other files in the structure transparently fall back to original files/folders
  • apply the global device map from \GLOBAL?? to all current (non-target) processes, so that they are not affected by the redirection
  • redirect the session's device map to the fake directory structure
    • all new processes will have the "fake" device map
  • trigger the target process

The redirection of the target MSI file C:\Windows\installer\c63eb.msi has multiple parts, so it's easier to see step by step. Initially, the DosDevice object directory for the session only contains a link to the Global?? object directory:

WinObj showing the DosDevice directory for the session with only a Global link

By creating a C: object link in this directory, accesses (in this session) to C:\whatever are redirected to a temporary folder on the filesystem:

WinObj showing the DosDevice directory for the session with an additional C: link

This temporary folder contains junctions named after all folder in the original C:\ root directory:

View of the temporary directory

Each of these junctions points to the corresponding original folder (using the Global device map) except one: the one that is part of the path we want to redirect (here Windows, for C:\Windows):

Listing of the temporary directory showing junctions

This junction points to an object directory in \RPC Control. This "primary" object directory only contains a single entry: a link corresponding to the next item in the target path (here installer, for C:\Windows\installer):

WinObj showing an object directory with an installer link

This "primary" object directory has a shadow object directory (called here the "secondary" directory) that is used as a fallback if a resource is not found in the "primary" directory. The "secondary" directory has links for all the items of the original C:\Windows filesystem folder, each link pointing to the original item on the filesystem:

WinObj showing an object directory with multiple links

This combination of object directory and shadow object directory allows redirecting specific file(s) or folder(s) in the primary directory, while redirecting all other entries to their respective original filesystem counterparts in the shadow/secondary directory: when an item is not found in the primary directory, the secondary one will be used. While not strictly necessary (we could just use a single object directory and change the target symlinks inside), it makes adding and changing links a bit easier in some scenarios.

The installer symlink in the primary directory points to another pair of object directories (primary and secondary), this time reflecting the original C:\Windows\installer filesystem folder:

WinObj showing an object directory with 2 links

WinObj showing an object directory with multiple links

In the primary directory of this pair, the c63eb.msi link points to the malicious file we want the Windows Installer process to load (while thinking it's loading the one from the trusted system directory), here C:\Temp\c63eb.msi. (Note: inprogressinstallinfo.ipi was initially used for oplocks during the tests, but ended up not being necessary.)

The following schema sums up the redirection setup:

Schema showing the chain of mountpoints, object directories and symlinks used for the redirection

Before this redirection is put into place, existing processes in the current session are made to use the \Global?? directory as their device map using an undocumented call to NtSetInformationProcess that allows setting the device map for the process:

bool SetProcessDeviceMap(HANDLE hDir, HANDLE hProc = GetCurrentProcess()) {
    PROCESS_DEVICEMAP_INFORMATION DeviceMap = { hDir };
    NTSTATUS status = NtSetInformationProcess(hProc,
        ProcessDeviceMap,
        &DeviceMap,
        sizeof(DeviceMap));
    return status == 0;
}

This allows others processes to continue operating normally using the global device map (as long as they do not need access to a sessions-specific device) but a new process (in the session or impersonating it) will use the fake directory structure. Also, for this process, accesses to existing files and folders will work somewhat normally, except for the specific target file C:\Windows\installer\c63eb.msi that gets redirected to C:\Temp\c63eb.msi.

As for the MSI file, in the PoC we chose to just "binary patch" the existing MSI file and replace a registry key that gets created, to create an Image File Execution Options registry key for WerFault.exe, and then trigger a crash to execute the payload as SYSTEM (WerFault.exe is run as SYSTEM and a second time as the user whose process crashed).

The only requirement for this (which is always almost fulfilled) is to have at least one installed product / managed package on the machine. For our PoC we targeted the Minimum Runtime package from the Visual C++ 2019 redistributable x64, as it is very commonly installed. Note however, that the vulnerability is obviously not specific to this package, and the same approach can be used with almost any MSI package.

Because the target registry path is longer than the one being replaced, a registry key symlink is used to keep the registry path short:

$RegSymlink = [NtApiDotNet.NtKey]::CreateSymbolicLink("\Registry\Machine\SOFTWARE\Microsoft\Tracing\a\abcdefghijklmnop", $null, "\REGISTRY\Machine\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options")

This creates an alternative path HKLM:\SOFTWARE\Microsoft\Tracing\a\abcdefghijklmnop\WerFault.exe to the desired registry path, with the alternative path being the same length than the existing path HKLM:\SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum used in the MSI file. It simplifies the binary patching needed, as simple string match/replace can now be used.

Summing up, the chosen exploitation method is the following:

  • Find and copy the existing MSI file
  • Create a registry key symlink to the target registry key and value (the Debugger property of the Image File Execution Options registry key for WerFault.exe)
  • Patch the binary MSI file to replace the registry key/entry path, name and value
  • Change the user device map to redirect C:, C:\Windows and C:\Windows\Installer through object directories and object symlinks, and change the device map of existing processes so they continue running undisturbed
  • Run msiexec on the MSI file from C:\Windows\Installer (but the modified file will be accessed instead because of the redirection)
  • The elevated msiexec will the use the fake MSI file (believing it's the trusted one) and create the desired registry key
  • Trigger a crash to execute WerFault.exe - and its "debugger" a.k.a. the payload - as SYSTEM

Procmon can be used to confirm the process accesses the redirected path:

Procmon showing msiexec accessing the fake MSI file

And creates the desired registry key:

Procmon showing msiexec creating the debugger registry key

Proof of Concept

The PoC exploits this vulnerability to create the desired registry key, then triggers a crash to execute the debugger associated with WerFault.exe (C:\x\z.exe which is a copy of payload.exe) as SYSTEM.

The relevant parts of the source code can be found here.

Note: source code provided in this repository is the original one sent to Microsoft; it may need to be adapted, as the C++ code requires Jonas' exploit toolkit which was not public at the time and may have slight differences with the published version.

The PoC targets the VC++ 2019 Minimum Runtime x64 package. As explained above, the vulnerability has nothing to do with this particular package - but this one was chosen for the demonstration because it is very commonly installed.

Here is a video of that PoC in action on an unpatched Windows 10 1909:

Fix

Microsoft has released a patch and an advisory. Users should apply updates through usual channels.

References

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-0911

Timeline

2020-05-29: Initial report sent to vendor

2020-05-29: Vendor acknowledges reception of report

2020-10-09: Vendor publishes fix and advisory

2023-07-06: Publication of this article