DCOM Again: Installing Trouble
Sep 29 2025
By: Craig Wright • 12 min read
TL;DR I am releasing a DCOM lateral movement beacon object file (BOF) that uses the Windows Installer Custom Action server to install and configure an ODBC driver. If you just want the code: https://github.com/werdhaihai/msi_lateral_mv
Intro
SpecterOps provided me with an opportunity to research the Windows Installer. In this blogpost, we’ll discuss how I went about discovering an additional execution primitive that can be used for lateral movement using the Windows Installer Custom Action Server. Before starting, I’m going to briefly lay out a background on COM and the Windows Installer.

What is COM?
I am not going to write a deep dive into what the component object model (COM) is because there are many other existing resources for a better understanding of it. I recommend Don Box’s Essential COM.
COM, or Component Object Model, is an object-oriented standard that allows developers to interact with other components without needing to understand their inner workings. COM is language and platform-agnostic, though it is best known for its use in the Windows operating system.
As an example, a developer could write an encryption/decryption COM server. Application developers that want to use the encryption software don’t need to be cryptography experts; they just need to know which arguments to pass to the COM server’s functions. The COM server developer can modify the encryption and decryption routines, but as long as the exposed functions retain their original definitions, the application developer doesn’t need to make any changes to their code.
It’s worth noting that a COM server and client can exist in the same executable, though it seems to be more common for them to be separate. Regardless, COM allows for interprocess communication, which means it has the potential to allow for privilege escalation or lateral movement. COM can also be utilized across machines in what is called DCOM (Distributed COM). COM servers generally support self-registration. The registration of a COM server amounts to the creation of several registry keys.
COM is rather complicated and, in my opinion, a bit annoying to reverse engineer. I believe this is some of why it does not get quite as much attention as it should for a technology with roots in the ‘90s.
COM is built on top of RPC. The COM security model relies on the security built into Windows and underlying RPC security. COM objects are securable objects, like most of the rest of the objects on Windows (i.e., files, services, registry keys). This securable object can have a security descriptor that defines which identities can activate the COM object. After activation, COM objects can also control who can access the server’s objects. COM supports a variety of authentication protocols.
DCOM has been used quite a bit in the red team space for lateral movement. The earliest information I could find on the use of DCOM in lateral movement was from Matt Nelson (enigma0x3) in 2017. In two blogposts (Lateral Movement Using the MMC20 Application COM Object and Lateral Movement via DCOM Round 2), Matt discusses the identification of the DCOM methods that allow for lateral movement. Also an honorable mention to Raj Patel (grayhatkiller) for his work with Lateral Movement: Abuse the Power of DCOM Excel Application
Over the years, Microsoft has added security features to make DCOM lateral movement harder and more controllable. The hardening focused on changing some insecure defaults and providing more granular system-wide control over the remote launch and activation settings for COM servers. Microsoft also gradually rolled out authentication-level protections to enforce verification for RPC authentication, intended to prevent relay attacks5. On updated Windows hosts, COM servers now require RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, mitigating NTLM relay attacks.
Background on MSI and MSI Server
Before the days of the MSI, developers would make use of the SetupAPI to install applications and drivers. The SetupAPI would parse INF files. It would move files around, modify the registry, and had some logging functionality, but the SetupAPI kind of sucked. It didn’t track any of the changes made, it didn’t have the ability to recover or rollback from failures, and it didn’t have any uninstall functionality. There was no standard for writing these, and bad setup developers could leave systems in a bad state.
Microsoft developed the Microsoft Installer (internally code-named Darwin) to solve a lot of these problems. A few months after its initial release, in true Microsoft form, they decided to rename it to the Windows Installer. The new name never really caught on, which is why we use .msi and not .wsi extensions. The Darwin team decided to go with a custom database format (why not?) to house the logic of the installations. This continues to cause problems for MSI developers.
“… building this custom relational database to store all the setup logic was unnecessary and generated a lot of overhead over the years (especially for those of us that have to create the flipping MSI files)” –
Rob Mensching, Inside the MSI file format – 2003
How does this all relate to COM? The Windows Installer isn’t just an installation framework; it’s a Windows service that runs as NT AUTHORITY\SYSTEM and exposes its functionality through COM interfaces. Microsoft exposed the Windows Installer via COM so that client processes like msiexec.exe can access the installation functionality that needs to execute in a privileged context.
Custom Actions and Custom Action Server
In some cases, an installation needs to make changes to the system as a privileged user (NT AUTHORITY\SYSTEM), that’s the reason the Windows Installer service exists. In order to interact with the server, the msiexec.exe command-line utility will instantiate the Windows Installer service via COM. The details of the components get passed along to perform actions that require elevation.
The Windows Installer supports many standard actions, but at a certain point, developers of installers had edge cases and began requesting more flexibility. The team at Microsoft added Custom Actions. These Custom Actions allowed MSI developers to execute scripts, EXEs, and DLLs embedded in the MSI, as well as execute shell commands on the system. These Custom Actions are configurable by bitmask to determine when and by whom they will execute. If the Custom Action has the msidCustomActionTypeInScript and the msidbCustomActionTypeNoImpersonate options set, the Custom Action will execute in the context of NT AUTHORITY\SYSTEM. The Custom Action server seems like an obvious target for privilege escalation.
Leaked Source and Reverse Engineering
Knowing that the Windows Installer has been around for a while, I decided to take a look at the leaked source code and use it to assist my reverse engineering. I found four Interface Definition Language (IDL) files in the leaked source code that defined the COM interfaces in the Windows Installer. The server.idl file defines the interface for the IMsiServer. We can see that a CLSID of the same value exists. The MSI install server points to an AppId of the same value. The AppId is a service. The service being the MSIServer, or Windows Installer service.
PS C:\> reg query "HKCR\CLSID\{000C101C-0000-0000-C000-000000000046}" /s
HKEY_CLASSES_ROOT\CLSID\{000C101C-0000-0000-C000-000000000046}
(Default) REG_SZ Msi install server
AppId REG_SZ {000C101C-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOT\CLSID\{000C101C-0000-0000-C000-000000000046}\ProgId
(Default) REG_SZ IMsiServer
PS C:\> reg query "HKCR\AppId\{000C101C-0000-0000-C000-000000000046}" /s
HKEY_CLASSES_ROOT\AppId\{000C101C-0000-0000-C000-000000000046}
LocalService REG_SZ MSIServer
ServiceParameters REG_SZ
To verify what is going on here, we can actually trigger the start of the service using the following PowerShell. After executing the following commands, you should see the msiexec.exe service running as NT AUTHORITY\SYSTEM. It’s worth noting that this can be triggered as a low-privilege user.
$clsid = "{000C101C-0000-0000-c000-000000000046}"
$type = [Type]::GetTypeFromCLSID($clsid)
$comObject = [Activator]::CreateInstance($type)
It’s great that we can start the service, but what if we actually want to interact with the IMsiServer interface? Well, at this point, we should probably switch to unmanaged code. The reason is that the IMsiServer is not a public interface, meaning that Microsoft does not really intend for us to use it. In order to actually use the IMsiServer interface, we will need to define it in our code. This is much less complicated if you actually have the IDL used to define the interface, but what if you didn’t have this IDL? You would need to identify the interface via reverse engineering.
To do this, we can open the COM server in our favorite decompiler/disassembler. The COM server is implemented in the msi.dll. All COM servers must inherit from the IUnknown class. The IUnknown class is defined as:
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
The QueryInterface function accepts a Reference ID (REFIID), an Interface ID (IID) and a pointer to a pointer object, which is modified in the code to return the COM object to the caller. What this means for us is that we should be able to search for our IID and find the implementation of the IMsiServer interface.

After searching, we see two QueryInterface functions that match our search criteria. Looking at the implementation of the CMsiServerProxy::QueryInterface, we see some comparison to the top portion of the IMsiServer IID (0xc101c).

If we then search for cross references to CMsiServerProxy::Release, we should find a data reference where the virtual table for CMsiServerProxy lives. We should see something familiar from the leaked source IDL.

After clicking into one of the functions, we see they all almost immediately call into NdrClientCall3. For the unaware, this means we are looking at the client-side code, which calls into RPC to access the COM server remotely.

Going back to our original search results, and following the same workflow with the CMsiConfigurationManager::QueryInterface, we can follow the cross references to the CMsiConfigurationManager::Release to land in the virtual table for the CMsiConfigurationManager interface.

This time, clicking into one of the functions, we can see the server-side implementation of the function.

Great! It was at this point that I started renaming and retyping all of the variables in the CMsiConfigurationManager interface. During the process, several interesting functions stood out to me. I started looking into the Windows Installer more and came across a very interesting article titled Forget PSEXEC: DCOM Upload & Execute Backdoor by eliran_nissan. As I started reading the blog post, I quickly realized I was duplicating work.

I highly recommend reading their blog post; it details the discovery and use of the Custom Action Server to perform a file write and execution trigger for lateral movement. Using the method Eliran laid out, I was able to obtain a pointer to the Custom Action Server interface.
Discovering ODBC Functions
As we’ve already discussed, the purpose of custom actions is to execute script, executables (.exe), and dynamic-link libraries (DLLs). I was not super interested in the scripts for execution, since script execution receives more scrutiny by security products in my experience. I did notice several functions that were not mentioned in Eliran’s original blogpost, however, which made me think there may be something else here.
The functions I found particularly interesting were the SQL-related functions.

I started by looking at the SQLInstallDriverEx function. The function really didn’t do much. It checks for the existence of an argument, calls a function to keep the thread alive, then calls into ODBCCP32.DLL!SQLInstallDriverEx.

On its own, the SQLInstallDriverEx function won’t actually execute any code. After reading the documentation for the SQLInstallDriverEx function, I noticed a line that said:

The Custom Action Server also exposed a SQLConfigDriver function. Similar to the SQLInstallDriverEx function, the SQLConfigDriver function did nothing more than hold the thread open while performing a call to ODBCCP32!SQLConfigDriver.

Looking at the documentation for SQLConfigDriver, we see a comment that tells us everything we need to know.

Time for a quick proof of concept. Knowing which APIs are being called by the Windows Installer, before going through all the trouble of developing a COM client for the Custom Action server, we can just develop a small application to call the ODBCCP32 functions. We are also going to need a DLL that exports a function named ConfigDriver. Inside the ConfigDriver function, we will call MessageBox to verify that we have successfully called the function.

Next, we need to write a small executable to call the two SQL functions.



DCOM Lateral Movement BOF
We have now identified a mechanism in the Custom Action Server that can be used to load a DLL. To tie it all together, if we are a member of the Administrators group on the remote system, and we write a DLL to the remote system, we can get a pointer to the Custom Action Server interface and call the CMsiCustomAction::SQLInstallDriverEx and CMsiCustomAction::SQLConfigDriver functions to trigger the execution of our DLL. The ODBC actions will execute from the host process of msiexec.exe in the user context we are moving laterally with. Modifying the DLL a bit, we can prove this out by having it write some details to a file. I have developed and released a lateral movement BOF to perform the call. The BOF takes all wide strings and can be used locally or remotely by the current user or an alternative user, if you have credentials.


After executing our lateral movement BOF, we should see a file appear on the Desktop with our information.

Defensive Considerations
Registry Monitoring
Calls to SQLInstallDriverEx writes to the registry under HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\ODBC Driver. Adding a SACL to this key can generate high-fidelity alerts whenever a new ODBC driver is installed. I do not have a ton of insight into this, but I don’t expect this happens very frequently.