Windows Installer, Exploiting Custom Actions
18 Jul 2024 - Posted by Adrian DenkiewiczOver a year ago, I published my research around the Windows Installer Service. The article explained in detail how the MSI repair process executes in an elevated context, but the lack of impersonation could lead to Arbitrary File Delete and similar issues. The issue was acknowledged by Microsoft (as CVE-2023-21800), but it was never directly fixed. Instead, the introduction of a Redirection Guard mitigated all symlink attacks in the context of the msiexec
process. Back then, I wasn’t particularly happy with the solution, but I couldn’t find any bypass.
The Redirection Guard turned out to work exactly as intended, so I spent some time attacking the Windows Installer Service from other angles. Some bugs were found (CVE-2023-32016), but I always felt that the way Microsoft handled the impersonation issue wasn’t exactly right. That unfixed behavior became very useful during another round of research.
This article describes the unpatched vulnerability affecting the latest Windows 11 versions. It illustrates how the issue can be leveraged to elevate a local user’s privileges. The bug submission was closed after half-a-year of processing, as non-reproducible. I will demonstrate how the issue can be reproduced by anyone else.
Custom Actions
Custom Actions in the Windows Installer world are user-defined actions that extend the functionality of the installation process. Custom Actions are necessary in scenarios where the built-in capabilities of Windows Installer are insufficient. For example, if an application requires specific registry keys to be set dynamically based on the user’s environment, a Custom Action can be used to achieve this. Another common use case is when an installer needs to perform complex tasks like custom validations or interactions with other software components that cannot be handled by standard MSI actions alone.
Overall, Custom Actions can be implemented in different ways, such as:
- Compiled to custom DLLs using the exposed C/C++ API
- Inline VBScript or JScript snippets within the WSX file
- Explicitly calling system commands within the WSX file
All of the above methods are affected, but for simplicity, we will focus on the last type.
Let’s take a look at an example WSX file (poc.wsx
) containing some Custom Actions:
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="{12345678-9259-4E29-91EA-8F8646930000}" Language="1033" Manufacturer="YourCompany" Name="HelloInstaller" UpgradeCode="{12345678-9259-4E29-91EA-8F8646930001}" Version="1.0.0.0">
<Package Comments="This installer database contains the logic and data required to install HelloInstaller." Compressed="yes" Description="HelloInstaller" InstallerVersion="200" Languages="1033" Manufacturer="YourCompany" Platform="x86" ReadOnly="no" />
<CustomAction Id="SetRunCommand" Property="RunCommand" Value=""[%USERPROFILE]\test.exe"" Execute="immediate" />
<CustomAction Id="RunCommand" BinaryKey="WixCA" DllEntry="WixQuietExec64" Execute="commit" Return="ignore" Impersonate="no" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="HelloInstaller" ShortName="krp6fjyg">
<Component Id="ApplicationShortcut" Guid="{12345678-9259-4E29-91EA-8F8646930002}" KeyPath="yes">
<CreateFolder Directory="INSTALLFOLDER" />
</Component>
</Directory>
</Directory>
</Directory>
<Property Id="ALLUSERS" Value="1" />
<Feature Id="ProductFeature" Level="1" Title="Main Feature">
<ComponentRef Id="ApplicationShortcut" />
</Feature>
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." Schedule="afterInstallValidate" />
<InstallExecuteSequence>
<Custom Action="SetRunCommand" After="InstallInitialize">1</Custom>
<Custom Action="RunCommand" After="SetRunCommand">1</Custom>
</InstallExecuteSequence>
</Product>
</Wix>
This looks like a perfectly fine WSX file. It defines the InstallExecuteSequence
, which consists of two custom actions. The SetRunCommand
is queued to run right after the InstallInitialize
event. Then, the RunCommand
should start right after SetRunCommand
finishes.
The SetRunCommand
action simply sets the value of the RunCommand
property. The [%USERPROFILE]
string will be expanded to the path of the current user’s profile directory. This is achieved by the installer using the value of the USERPROFILE
environment variable. The expansion process involves retrieving the environment variable’s value at runtime and substituting [%USERPROFILE]
with this value.
The second action, also called RunCommand
, uses the RunCommand
property and executes it by calling the WixQuietExec64
method, which is a great way to execute the command quietly and securely (without spawning any visible windows). The Impersonate="no"
option enables the command to execute with LocalSystem’s full permissions.
On a healthy system, the administrator’s USERPROFILE
directory cannot be accessed by any less privileged users. Whatever file is executed by the RunCommand
shouldn’t be directly controllable by unprivileged users.
We covered a rather simple example. Implementing the intended Custom Action is actually quite complicated. There are many mistakes that can be made. The actions may rely on untrusted resources, they can spawn hijackable console instances, or run with more privileges than necessary. These dangerous mistakes may be covered in future blogposts.
Testing the Installer
Having the WiX Toolset at hand, we can turn our XML into an MSI file. Note that we need to enable the additional WixUtilExtension
to use the WixCA
:
candle .\poc.wxs
light .\poc.wixobj -ext WixUtilExtension.dll
The poc.msi
file should be created in the current directory.
According to our WSX file above, once the installation is initialized, our Custom Action should run the "[%USERPROFILE]\test.exe"
file. We can set up a ProcMon filter to look for that event. Remember to also enable the “Integrity” column.
![Procmon filters settings](../../../public/images/custom-actions-procmon-filters.png)
We can install the application using any Admin account (the Almighty
user here)
msiexec /i C:\path\to\poc.msi
ProcMon should record the CreateFile
event. The file was not there, so additional file extensions were tried.
![NAME NOT FOUND event for the almighty user](../../../public/images/custom-actions-procmon-almighty.png)
The same sequence of actions can be reproduced by running an installation repair process. The command can point at the specific C:/Windows/Installer/*.msi
file or use a GUID that we defined in a WSX file:
msiexec /fa {12345678-9259-4E29-91EA-8F8646930000}
The result should be exactly the same if the Almighty
user triggered the repair process.
On the other hand, note what happens if the installation repair was started by another unprivileged user: lowpriv
.
![NAME NOT FOUND event for the lowpriv user](../../../public/images/custom-actions-procmon-lowpriv.png)
It is the user’s environment that sets the executable path, but the command still executes with System
level integrity, without any user impersonation! This leads to a straightforward privilege escalation.
As a final confirmation, the lowpriv
user would plant an add-me-as-admin type of payload under the C:/Users/lowpriv/test.exe
path. The installation process will not finish until the test.exe
is running, handling that behavior is rather trivial, though.
![Event details](../../../public/images/custom-actions-event-properties.png)
Optionally, add /L*V log.txt
to the repair command for a detailed log. The poisoned properties should be evident:
MSI (s) (98:B4) [02:01:33:733]: Machine policy value 'AlwaysInstallElevated' is 0
MSI (s) (98:B4) [02:01:33:733]: User policy value 'AlwaysInstallElevated' is 0
...
Action start 2:01:33: InstallInitialize.
MSI (s) (98:B4) [02:01:33:739]: Doing action: SetRunCommand
MSI (s) (98:B4) [02:01:33:739]: Note: 1: 2205 2: 3: ActionText
Action ended 2:01:33: InstallInitialize. Return value 1.
MSI (s) (98:B4) [02:01:33:740]: PROPERTY CHANGE: Adding RunCommand property. Its value is '"C:\Users\lowpriv\test.exe"'.
Action start 2:01:33: SetRunCommand.
MSI (s) (98:B4) [02:01:33:740]: Doing action: RunCommand
MSI (s) (98:B4) [02:01:33:740]: Note: 1: 2205 2: 3: ActionText
Action ended 2:01:33: SetRunCommand. Return value 1.
The Poisoned Variables
The repair
operation in msiexec.exe
can be initiated by a standard user, while automatically elevating its privileges to execute certain actions, including various custom actions defined in the MSI file. Notably, not all custom actions execute with elevated privileges. Specifically, an action must be explicitly marked as Impersonate="no"
, be scheduled between the InstallExecuteSequence
and InstallFinalize
events, and use either commit
, rollback
or deferred
as the execution type to run elevated.
In the future, we may publish additional materials, including a toolset to hunt for affected installers that satisfy the above criteria.
Elevated custom actions may use environment variables as well as Windows Installer properties (see the full list of properties). I’ve observed the following properties can be “poisoned” by a standard user that invokes the repair process:
- “AdminToolsFolder”
- “AppDataFolder”
- “DesktopFolder”
- “FavoritesFolder”
- “LocalAppDataFolder”
- “MyPicturesFolder”
- “NetHoodFolder”
- “PersonalFolder”
- “PrintHoodFolder”
- “ProgramMenuFolder”
- “RecentFolder”
- “SendToFolder”
- “StartMenuFolder”
- “StartupFolder”
- “TempFolder”
- “TemplateFolder”
Additionally, the following environment variables are often used by software installers (this list is not exhaustive):
- “APPDATA”
- “HomePath”
- “LOCALAPPDATA”
- “TEMP”
- “TMP”
- “USERPROFILE”
These values are typically utilized to construct custom paths or as system command parameters. Poisoned values can alter the command’s intent, potentially leading to a command injection vulnerability.
Note that the described issue is not exploitable on its own. The MSI file utilizing a vulnerable Custom Action must be already installed on the machine. However, the issue could be handy to pentesters performing Local Privilege Elevation or as a persistence mechanism.
Disclosure Timeline
The details of this issue were reported to the Microsoft Security Response Center on December 1, 2023. The bug was confirmed on the latest Windows Insider Preview build at the time of the reporting: 26002.1000.rs_prerelease.231118-1559
.
Disclosure Timeline | Status |
---|---|
12/01/2023 | The vulnerability reported to Microsoft |
02/09/2024 | Additional details requested |
02/09/2024 | Additional details provided |
05/09/2024 | Issue closed as non-reproducible: “We completed the assessment and because we weren’t able to reproduce the issue with the repro steps provide _[sic]_. We don’t expect any further action on the case and we will proceed with closing out the case.” |
We asked Microsoft to reopen the ticket and the blogpost draft was shared with Microsoft prior to the publication.
As of now, the issue is still not fixed. We confirmed that it is affecting the current latest Windows Insider Preview build 10.0.25120.751
.