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.
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:
|
|
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.
Symlink attack
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:
|
|
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.
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.
The file follows existing access rights rules - no extra permissions provided, but we just proved the arbitrary write. What’s inside?
|
|
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.
|
|
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.
|
|
- 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:
|
|
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
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:
|
|
UAC prompt. So it must be admin only after all… However, if we take a look at procmon - it looks like write already happened.
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:
|
|
Since your system should already be patched, here’s a quick video PoC of it in action:
Thanks for reading!