A Windows privilege fix that held 28 days: Project Zero on dangling COM registrations
Project Zero bypassed Microsoft's July fix for the Dark Elevator bug in 28 days by abusing a COM registration that still pointed at a missing DLL. The technique turns any writable dangling registration into SYSTEM.
By Parminder Kumar Sharma · · 10 min read

A fix that lasted 28 days
Microsoft shipped a fix for the privilege escalation bug nicknamed Dark Elevator on 14 July 2026, tracked as CVE-2026-50343. On 11 August 2026 it shipped a second fix, CVE-2026-66804, because the first one had not closed the door. That is 28 days between the patch and the patch for the patch. Project Zero researcher James Forshaw, who reported the follow-up with 14 other people, has now published how he walked through the gap.
The 28 day figure does not establish that anyone was exploited. It is not an in the wild campaign, there is no known victim, and both bugs need an attacker to already have a local account on the machine. What it does establish is more useful for a defender: the root cause was never the specific service Microsoft patched in July. It was a dangling COM registration, a pattern that recurs across Windows and across third party software, and the technique Forshaw describes turns that pattern into SYSTEM code execution almost mechanically. If you only track the two CVE numbers, you will miss the class of problem sitting in your own estate.
What a dangling COM registration actually is
The Component Object Model, COM, is the plumbing that lets one Windows component ask the operating system for another by a fixed identity rather than by file path. Each component has a class identifier, a long hexadecimal value written as a CLSID. In the registry, that CLSID maps to the file that implements it. For an in process component the file is a DLL, and the mapping lives under the HKEY_CLASSES_ROOT key. When any process asks to create that class, the COM runtime reads the registry, finds the DLL and loads it.
A dangling registration is a mapping that survives after the file it names has gone. The CLSID entry is still present and still readable, but the DLL it points at is missing: uninstalled, never installed, renamed, or shipped by mistake. In the Dark Elevator case the dangling class was the CrossDevice object, CLSID beginning E9F83CF2. It was registered in the system wide classes key, so every user and every SYSTEM service could see it, and it pointed at a DLL that did not exist.
That is where the second half of the problem lives. The missing DLL was expected under C:\ProgramData, the shared data directory that, by design, lets any user create folders and files. So an unprivileged user can create the exact folder and file the registration is waiting for, and plant a DLL of their choosing. The registration is no longer dangling in a harmless sense. It is a loaded slot pointing at attacker controlled content. The danger is not the missing file on its own, nor the writable directory on its own. It is the two together: a shared, world readable class identity whose backing file can be supplied by anyone.
How the empty slot becomes SYSTEM
Planting the DLL is not enough on its own. The attacker still needs a privileged process to load it. The original Dark Elevator bug reached that goal one way, through a weakly permissioned registry key that let the class be added as an installer plugin so a privileged install service would load it. Microsoft closed that specific route in July. The registration, though, was still dangling, so Forshaw needed a different way to make a privileged process instantiate the class.
The route he used is custom COM marshaling. When one COM component passes an object to another that runs in a separate process, the runtime has to serialise, or marshal, that object so it can travel. The default is marshal by reference, which sends a pointer back to the original object. But a component can opt in to marshal by value by implementing the IMarshal interface, and in doing so it gets to name the class that should be used to rebuild the object on the far side. Crucially, that class does not have to be the class of the object being sent.
So a crafted object can declare that its unmarshaling class is the dangling CrossDevice class. When it arrives at a privileged process, the runtime unmarshals it automatically, before any method the attacker called even runs, looks up that CLSID, finds the planted DLL and loads it. The object never had to be genuine. It only had to lie about which class should rebuild it. This is why the pattern generalises: the same trick works against any dangling registration whose backing path an attacker can write to, and, as Forshaw notes, against some buggy custom unmarshalers where merely loading a DLL is enough to cause harm.
Windows does have a mitigation. Since Windows 8 a process can refuse custom marshaling from untrusted classes, either by setting a no custom marshal capability when it initialises COM security, or by selecting the strong unmarshaling policy through the global options interface. When either is on, an unknown class like the dangling one is rejected and the DLL is never loaded. The catch is that the mitigation is opt in, per process, and the dangling class is not on the small trusted list. So the exploit reduces to a search: find a service that runs as SYSTEM and did not turn the mitigation on.
Forshaw found one, the Shell Create Object Handler, a SYSTEM hosted object that a normal user can bring to life indirectly by starting a shell scheduled task and signalling a named readiness event. Once it is running as SYSTEM with custom marshaling allowed, its one exposed interface takes an object pointer as a parameter, which is all that is needed to deliver the crafted object and trigger the load. The point for a defender is not this one service. It is that at least one SYSTEM service shipped without the mitigation, and Forshaw is explicit that there are very likely others.
The chain in one picture
What the record states, and what it leaves open
Drawn from the Project Zero write-up and the NVD records for both CVEs, accessed 21 September 2026.
| Question | Stated on the record | Not stated |
|---|---|---|
| Impact | Local elevation of privilege to SYSTEM, technical impact total | Any remote or unauthenticated path |
| Preconditions | An existing local account and a writable backing path | That default installs are always vulnerable |
| Exploitation | CISA coordinator marks exploitation none, automatable no | That it will never be weaponised later |
| Scope of fix | The July and August fixes address these two services | That all SYSTEM services now enable the marshaling mitigation |
CVE details from the NVD records, with scores assigned by Microsoft as the CNA. Accessed 21 September 2026.
| CVE | Fixed on the record | Score and weakness |
|---|---|---|
| CVE-2026-50343 | 14 July 2026, Microsoft Install Service, the original Dark Elevator bug | CVSS 7.8 high, CWE-269, by Microsoft |
| CVE-2026-66804 | 11 August 2026, Windows Cross Device Service, the incomplete fix bypass | CVSS 7.8 high, CWE-284, by Microsoft |
Finding dangling registrations in an estate
The useful move is to stop chasing the two CVE numbers and start hunting the pattern. There are two questions worth answering across a fleet. First, which in process COM classes point at a DLL that cannot be found. Second, of those, which point at a path that a non administrator can write to, because only that subset is exploitable by planting. Forshaw published an enumeration approach for the first question using his own tooling, and it is fair to reproduce here because it is detection, not weaponisation.
# Enumeration idea, adapted from Project Zero. Requires OleViewDotNet
# and NtObjectManager. List in-process COM classes whose DLL cannot be
# resolved on the system path, then review the paths by hand.
function Test-ComServer($Server) {
try {
Use-NtObject($lib = Import-Win32Module -Path $Server -Flags AsDataFile) { $true }
} catch { $false }
}
$db = Get-ComDatabase -LoadMode MachineOnly
$cs = Get-ComClass -Database $db -ServerType InProcServer32
$cs | Where-Object { -not (Test-ComServer $_.DefaultServer) } |
Sort-Object DefaultServer |
Select-Object Name, DefaultServer
That list is the candidate set, not the verdict. A registration that points at a missing DLL under a system only directory such as C:\Windows\System32 is far less interesting than one under C:\ProgramData, a per user profile, or any directory whose permissions let a standard user write. The manual step is to check the resolved directory of each missing path against who can write to it. Anything writable by non administrators and referenced by a system wide class is the priority.
For continuous detection rather than a point in time audit, telemetry helps, though this next part is defensive guidance from us rather than a claim in the Project Zero post, which does not discuss Sysmon. Three signals are worth building rules around. A file creation event for a DLL landing in a shared writable directory such as C:\ProgramData is unusual and cheap to alert on. An image load event where a privileged host process, for example a dllhost running as SYSTEM, loads a DLL from a user writable path is a strong signal, because legitimate system DLLs load from protected directories. And registry events that add or change a class InProcServer32 mapping deserve review, since that is how a dangling slot gets created or repointed.
What to do, in order
Take this with you
A practical order of work
- Confirm the July and August 2026 Windows cumulative updates are deployed, since they carry the fixes for CVE-2026-50343 and CVE-2026-66804.
- Enumerate in process COM classes whose backing DLL cannot be resolved, across a representative sample of your build images and a spread of live hosts.
- For each missing path, resolve the directory and check whether a standard user can write to it. Keep only the writable, system wide cases.
- Treat any missing DLL under a shared or user writable directory that is referenced by a system wide class as a finding to remediate, by removing the stale registration or restricting the directory.
- Turn on or tune telemetry for DLLs loaded by SYSTEM processes from user writable directories, and for new files landing in shared writable directories.
- Feed the pattern, not just the CVE numbers, into your software vetting so third party installers that leave dangling registrations are caught before rollout.
Microsoft fixed the two bugs Forshaw and his co reporters found. The technique that reached SYSTEM is the durable part, and it depends only on a class identity that everyone can see, a backing file that anyone can supply, and one privileged process that forgot to say no. The question to carry into your own estate is not whether these two CVEs are patched. It is this: across your fleet, how many system wide COM classes point at a file that is not there, and of those, how many sit in a directory where any user can put one?
Sources
- PrimaryJames Forshaw's write-up of the dangling COM registration technique and CVE-2026-66804, the primary source for the mechanismGoogle Project Zeroaccessed 2026-09-21
- PrimaryNVD record for CVE-2026-66804, used for the description, CVSS 7.8 score, CWE-284 and the CISA coordinator SSVC valuesNVDaccessed 2026-09-21
- PrimaryNVD record for CVE-2026-50343, the original Dark Elevator bug, used for the description, CVSS 7.8 score, CWE-269 and publish dateNVDaccessed 2026-09-21
- PrimaryMSRC update guide entry referenced by NVD for the Cross Device Service elevation of privilege fixMicrosoft Security Response Centeraccessed 2026-09-21
- PrimaryCISA Known Exploited Vulnerabilities feed, checked to confirm neither CVE is listedCISAaccessed 2026-09-21


