claude-news
blogGoogle Project Zero·21 Sept 2026·James Forshaw

Windows Exploitation Techniques: Dangling COM Object Registrations

Project Zero shows privilege escalation via dangling COM object registrations in Windows, an incomplete fix tracked as CVE-2026-66804.

Extract

This short blog post is about abusing a privilege escalation bug that Microsoft recently fixed in Windows, CVE-2026-66804 , that I and 14 others reported. This issue is an incomplete fix for CVE-2026-50343, a bug dubbed “Dark Elevator” by Calif . The root cause of the bug was a dangling COM object registration for the CrossDevice COM object with the CLSID {E9F83CF2-E0C0-4CA7-AF01-E90C70BEF496} . A COM registration typically needs two parts: a server executable, which for in-process components is a DLL and a CLSID entry under the HKEY_CLASSES_ROOT registry key which points to that DLL. This object was registered in the system wide classes key, meaning it was accessible to all users on the system, including system services. However the server executable was missing. Specifically it was registered to use the DLL %PROGRAMDATA%\CrossDevice\CrossDevice.Streaming.Source.dll . Not only does this path not exist, it’s also within the C:\ProgramData directory. This is a common location for all users on the system and therefore permits anyone to create directories. Therefore you can create an arbitrary DLL file at that location and the COM object can be instantiated potentially leading to privilege escalation. But how to get the COM object, and thus the DLL, loaded into a privileged process? The fixed bug Calif blogged about, CVE-2026-50343, abused a weak registry key permissions to add the class as a installer plugin and then get the InstallService to load it into memory. The issue with the InstallService was fixed, so we need an alternative way to abuse the unfixed dangling COM reference. Abuse Custom COM Marshaling, Again A technique I’ve used multiple times in the past to load an arbitrary DLL into a privileged process is to abuse custom COM marshaling. When you call an interface method which is implemented out-of-process, the COM runtime will marshal the parameters into an RPC call to send to the server. If a parameter is a COM object then the runtime marshals that object into an OBJREF structure that allows the object to be used in the server. The two main types of OBJREFs are shown in the diagram below, or you can read about them in the official DCOM documentation here : The default COM marshaling strategy is by reference which produces a Standard OBJREF containing all the information needed to connect to the original object. The object might even be on a completely different computer. When the object is unmarshaled this information is used to create an RPC …

Explained

What happened

This is a Windows privilege escalation, CVE-2026-66804, an incomplete fix for an earlier bug called "Dark Elevator". The chain:

  1. A system-wide COM object (CLSID {E9F83CF2-...}) was registered for all users but its backing DLL, %PROGRAMDATA%\CrossDevice\CrossDevice.Streaming.Source.dll, did not exist. This is a dangling COM registration.
  2. C:\ProgramData lets any user create directories, so a normal user can plant that exact DLL path. If the object is ever instantiated in a privileged process, the attacker DLL loads there.
  3. The hard part is getting a SYSTEM process to load it. The earlier bug used an installer plugin, which Microsoft fixed. This researcher used custom COM marshaling instead: when a COM object is passed across a process boundary, an object implementing IMarshal can name an arbitrary CLSID to instantiate on unmarshal, which loads that CLSID in-process DLL.
  4. Microsoft has a mitigation that disables custom marshaling (EOAC_NO_CUSTOM_MARSHAL / COMGLB_UNMARSHALING_POLICY_STRONG), so the attacker needs a SYSTEM COM server that does not enable it. The author found the Shell Create Object Handler, reachable by a normal user through the CreateObjectTask scheduled task plus a named ready-event.
  5. Marshaling the fake object to that SYSTEM process triggers an unconditional unmarshal, loads the planted DLL as SYSTEM, and gives local admin.

Why it works

The root cause is state that points at nothing: a registration that survives its target. Because the DLL path sits in a world-writable directory, the dangling pointer becomes an attacker-controlled load. Everything after is plumbing to reach the trigger from low privilege, exploiting that one SYSTEM service left a dangerous legacy feature (custom marshaling) enabled. The original fix patched one trigger path but left the dangling registration itself, so a new path reopened it.

The bigger pattern

Two general ideas. First, a dangling reference plus a writable location equals attacker control. This is the Windows analogue of a dangling symlink, an unclaimed S3 bucket, or a subdomain takeover: a name that resolves to something an attacker can now supply. Second, fix the root cause, not the symptom. Patching individual trigger paths while the invalid registration remains is whack-a-mole, and attackers simply find the next deputy.

What it means in practice

  • When you remove or relocate a component, remove every registration and reference that points at it. Orphaned config, plugin entries, service definitions and DNS records are all attack surface.
  • Never let a trusted lookup resolve to a path or endpoint that a lower-privileged party can write. Audit world-writable directories (C:\ProgramData, shared temp, shared buckets) for anything a privileged process will load.
  • Keep dangerous legacy deserialization or dynamic-load features disabled by default on privileged services. Custom COM marshaling here is the same risk as unrestricted object deserialization in Java or .NET.
  • When patching, ask whether you fixed the class of bug or one instance. If the invalid state still exists, assume another trigger will be found.
  • The web-backend parallel: dangling references (takeover-able DNS, dead feature flags pointing at removed services, unclaimed OAuth redirect URIs) are the same bug shape. Reap them when you decommission.