Author: Michael Published: May 2026 Severity: CVSS 8.8 (High), AV:N / AC:L / PR:L / UI:N Affected: Windows 10, Windows 11, Windows Server 2012 through 2025 Patched: March 11, 2026, KB5079473


Abstract

This paper presents an analysis of CVE-2026-23669, a heap buffer overflow in Microsoft’s RPC runtime library (rpcrt4.dll). Although the vulnerability is publicly categorized under the Print Spooler umbrella, our research shows the underlying bug sits in the endpoint mapper (epmap) client code within the RPC runtime itself. The Print Spooler serves as an attack vector, but the flaw is not exclusive to it. Any Windows service that performs remote endpoint resolution through rpcrt4.dll could, in principle, be affected.

Through binary diffing, Ghidra decompilation, and controlled testing on a pre-patch Windows 11 system, we confirmed a reliable remote denial-of-service condition and identified a privilege escalation path from any domain user to domain administrator via NTLM coercion. We also examine why direct remote code execution from this primitive alone is effectively blocked by several overlapping mitigations in modern Windows, offering a practical look at how Microsoft’s defense-in-depth strategy holds up in the real world.


1. Introduction

Microsoft’s March 2026 Patch Tuesday addressed 67 vulnerabilities, among them CVE-2026-23669, a heap overflow in the Windows Print Spooler rated at CVSS 8.8. The vulnerability was originally identified by security researcher Andrea Pierini, who noted publicly that the bug is not confined to the print spooler but resides in the endpoint mapper protocol within rpcrt4.dll.

That observation turned out to be the key to understanding this vulnerability. Unlike PrintNightmare and its successors, CVE-2026-23669 does not exploit a flaw in spooler-specific logic. Instead, it targets a fundamental weakness in how the RPC runtime resolves remote endpoints, a mechanism that underpins dozens of Windows services beyond printing.

Our research spanned 18 days and involved binary diffing of six patched DLLs, full Ghidra decompilation of rpcrt4.dll, ntdll.dll, and spoolsv.exe, combined with controlled exploitation in an isolated lab environment. What follows is a detailed account of our findings, including what works, what doesn’t, and why.


2. Vulnerability Mechanics

2.1 The Endpoint Mapper Protocol

When a Windows service needs to reach a remote RPC endpoint, it queries the target machine’s Endpoint Mapper on port 135 using the ept_map RPC call (opnum 3). The response carries an array of “towers”, binary structures that describe available network endpoints in terms of protocol, address, and port.

Inside rpcrt4.dll, the relevant data structure is what we call the EPM Registration Context. It is a 0x388-byte heap object. At offset +0x2D8, it holds a fixed 4-slot pointer array (32 bytes) meant for up to 4 tower pointers. The count of active entries lives at offset +0x2F8.

2.2 The Missing Correlation Check

The ept_map response is a conformant varying array, defined in the IDL with [size_is(max_towers)] constraining the maximum number of entries returned. On the wire, this translates to a MaxCount field in the NDR encoding, which should be validated against the caller’s max_towers parameter, hardcoded to 4.

Our analysis of the client-side NDR unmarshaling code reveals that this correlation validation is skipped because the correlation descriptor pointer is NULL. This is a compile-time artifact. The MIDL-generated stub for ept_map does not emit correlation validation metadata on the client-side path.

The consequence is straightforward. A malicious EPM server can return any number of towers. NDR writes all of the corresponding tower pointers into the 4-slot array without complaint, overflowing into adjacent fields of the EPM context structure.

2.3 Triggering the Overflow

The attack hinges on getting the target’s spooler to issue an EPM query against a machine the attacker controls. This is accomplished through the notification callback mechanism.

First, the attacker authenticates to the target’s \pipe\spoolss using RPC_C_AUTHN_LEVEL_CONNECT, the lowest authentication level, available to any domain user. Then, the attacker invokes RpcRemoteFindFirstPrinterChangeNotificationEx (opnum 65) with a callback address pointing to the attacker’s host. The target’s win32spl.dll then attempts to resolve the remote spoolss endpoint by querying the attacker’s IP on port 135. At that point, the attacker’s rogue EPM responds with more than 4 towers.

Because the encoding is valid NDR, with MaxCount and ActualCount simply reflecting the actual number of towers, all client-side validation passes. The overflow is immediate and deterministic.

2.4 Overflow Geometry

With 11 towers in the response, the overflow map looks like this:

Slot Offset Field Overwritten
0–3 +0x2D8 In-bounds (valid tower pointers)
4 +0x2F8 Entry count (DWORD)
5–9 +0x300 Internal flags and async state
10 +0x328 RPC_ASYNC_STATE.RuntimeInfo

Each overflow slot receives a heap pointer to the corresponding tower data allocation, a valid, attacker-influenced heap block.


3. Self-Terminating Cleanup

One of the more interesting aspects of this vulnerability is the behavior of the cleanup loop that runs after the overflow.

Once NdrClientCall3 returns, the tower processing function iterates over the pointer array, freeing each tower and zeroing the slot. But here’s the catch. At iteration 4, the loop reaches the count field itself (offset +0x2F8). The 8-byte zero-write at that location sets the count to 0, and the loop terminates on the next check.

This self-termination is deterministic. Slots 5 and beyond are never touched by the cleanup. They persist as valid heap pointers to attacker-controlled tower data until the object is eventually destroyed.

In practice, the cleanup still crashes roughly 80% of the time. The count field temporarily holds the low 32 bits of tower[4]’s heap pointer, which is a large value. The loop tries to free non-heap addresses at the overflow offsets before it gets a chance to zero the count, and the result is a STATUS_HEAP_CORRUPTION (0xc0000374) crash.


4. Patch Analysis

4.1 Binary Diff Results

We diffed all six DLLs modified by KB5079473:

DLL Size Change Functional Changes
rpcrt4.dll +8 bytes +80 bytes new code (ShouldCapEndpointArray)
spoolsv.exe +4,096 bytes +VerifyRpcValidSecurityLevelForWinspool
localspl.dll -20,480 bytes +EscapePrinterName, +IncrementPrintJobCount
win32spl.dll -16,384 bytes Removed Point-and-Print functions
inetpp.dll -4,096 bytes Minor changes
spoolss.dll 0 bytes No changes

The 728KB byte diff in rpcrt4.dll initially had us wondering whether a second vulnerability was hiding in the update. After exhaustive analysis, comparing API call counts, searching for new validation functions, and examining every change that wasn’t clearly compiler noise, we’re confident it’s all recompilation artifacts. Only the 80-byte ShouldCapEndpointArray function constitutes a security fix.

4.2 Defense in Depth

Microsoft’s patch is notably thorough, employing three independent layers.

Layer 1, Authentication Gate (spoolsv.exe): A new function, VerifyRpcValidSecurityLevelForWinspool, is added to the notification handler opnums (58, 59, 60, 65, 66). It rejects connections made at RPC_C_AUTHN_LEVEL_CONNECT, now requiring at least RPC_C_AUTHN_LEVEL_PKT_PRIVACY. This cuts off low-privilege users from the vulnerable code path.

Layer 2, Array Bounds Cap (rpcrt4.dll): ShouldCapEndpointArray is called before the cleanup loop, capping the iteration count to 4 regardless of whatever value sits at +0x2F8. This defuses the cleanup overflow even if the authentication gate is somehow bypassed.

Layer 3, Attack Surface Reduction (win32spl.dll): Point-and-Print functions are removed entirely, closing off the remote printer driver installation vector.


5. Exploitation Assessment

5.1 Denial of Service

The DoS condition is straightforward. With an honest NDR encoding containing more than 4 towers, the spooler crashes shortly after receiving the rogue EPM response. The crash is logged as Event 7031 (Service Control Manager) and generates WER crash reports. The spooler auto-restarts within about 15 seconds, meaning the attack can be repeated indefinitely.

5.2 Remote Code Execution, Why It Doesn’t Work

We put considerable effort into trying to convert the heap overflow into remote code execution. Over 16 iterations of binary analysis and live testing, we systematically ruled out every direct path. The short version is that modern Windows mitigations work.

Control Flow Guard (CFG) is enabled on spoolsv.exe. Any attempt to call a heap address as a function pointer is caught by __guard_dispatch_icall. We scanned thousands of function entry points across loaded DLLs looking for CFG-valid stack pivot gadgets and came up empty.

LFH Bucket Separation puts the EPM context (0x388 bytes) and Connection objects (0x350 bytes) into different Low Fragmentation Heap buckets. You can’t corrupt into a neighboring object’s vtable through adjacent heap writes.

Object Lifecycle is another blocker. The corrupted EPM context is a transient client-side object. It’s allocated for the ept_map call and destroyed immediately after. There’s no window to reference the corrupted state from a separate code path.

Synchronous Execution seals it off further. Network EPM calls go through NdrClientCall3, which is synchronous. The async notification callback at +0x338, which would have been the most promising dispatch target for the corrupted fields, never fires for non-ncalrpc transports.

We did identify one interesting CFG bypass candidate in ntdll.dll that performs an unguarded indirect call. But it’s only reachable during exception dispatch, not from an arbitrary vtable call, creating a dependency loop that doesn’t resolve.

The takeaway is that no single one of these mitigations is bulletproof on its own. Together, they close off every practical RCE path we could identify from this primitive.

5.3 Privilege Escalation via NTLM Coercion

While direct RCE is off the table, the vulnerability does enable a meaningful escalation path.

The pre-patch authentication gap on the notification opnums allows any domain user to force the Print Spooler, running as SYSTEM, to initiate an outbound SMB connection to an attacker-controlled host. That connection authenticates as the machine account, yielding an NTLM coercion primitive.

This is the same class of technique as the well-known PrinterBug (MS-RPRN abuse). When combined with standard NTLM relay methods against Active Directory services, it can lead to full domain compromise in environments that haven’t hardened their relay defenses. The specific fix in KB5079473 closes this gate by enforcing PKT_PRIVACY on the affected opnums.

We are intentionally omitting the specific relay chains here, as they are well-documented elsewhere and adding them to a CVE-specific writeup would lower the barrier without adding meaningful defensive insight.


6. Comparison with PrintNightmare

CVE-2026-23669 is often mentioned alongside PrintNightmare (CVE-2021-1675 / CVE-2021-34527), but the two are fundamentally different vulnerabilities.

Aspect PrintNightmare CVE-2026-23669
Bug location spoolsv.exe (driver install) rpcrt4.dll (endpoint mapper)
Bug type Improper access control Heap buffer overflow
Attack vector RpcAddPrinterDriverEx (opnum 89) Rogue EPM response via notification callback
Authentication Any user (pre-patch) Any user at CONNECT level (pre-patch)
Direct RCE Yes (DLL load as SYSTEM) No (blocked by CFG + LFH + object lifecycle)
Escalation path Direct DLL execution NTLM coercion + relay
Patch mechanism Restrict driver install to admins Auth level check + array bounds cap

Our testing on Windows 11 24H2 (Build 26200) also confirmed that traditional PrintNightmare exploitation is independently blocked on recent builds through UNC path validation in RpcAddPrinterDriverEx. Several established implementations all returned ERROR_INVALID_PARAMETER on this build, regardless of CVE-2026-23669’s patch status.


7. Detection and Mitigation

7.1 Detection Indicators

Windows Event Logs:

Event 7031 (System) with the message “The Print Spooler service terminated unexpectedly” is the primary indicator. Repeated spooler crashes with snmpapi.dll or ntdll.dll referenced in WER reports should also raise flags.

Network Indicators:

Look for outbound connections from spoolsv.exe to unexpected external hosts on port 135. Also watch for ept_map responses where MaxCount exceeds 4 in the conformant array header.

WER Crash Signatures:

The most common crash signature is exception code 0xc0000374 (STATUS_HEAP_CORRUPTION) with faulting modules snmpapi.dll or ntdll.dll in the crash reports.

7.2 Mitigation

The primary recommendation is to apply KB5079473, the March 2026 cumulative update.

If patching is not immediately possible, the following registry change disables remote RPC endpoint registration for the spooler, preventing the notification callback from triggering the vulnerable EPM query:

reg add "HKLM\SYSTEM\CurrentControlSet\Control\Print" ^
    /v RegisterSpoolerRemoteRpcEndPoint /t REG_DWORD /d 2 /f

8. Responsible Disclosure Timeline

Date Event
Pre-March 2026 Vulnerability discovered (credited researcher: Andrea Pierini)
March 11, 2026 Microsoft releases KB5079473
March 11, 2026 CVE-2026-23669 published (NVD, MSRC)
April 14, 2026 Our independent research begins
April 30, 2026 Deterministic crash confirmed on pre-patch Windows 11
May 1, 2026 Full analysis complete, paper published

9. Conclusion

CVE-2026-23669 is a significant vulnerability in the Windows RPC runtime that extends beyond the Print Spooler. The heap overflow in the endpoint mapper client code is deterministic and remotely triggerable by any authenticated domain user, providing both a denial-of-service condition and a privilege escalation primitive through NTLM coercion.

Modern Windows mitigations, including CFG, LFH bucket isolation, and synchronous object lifecycles, collectively prevent direct remote code execution from this primitive alone. But the NTLM coercion path demonstrates that real-world impact can still reach domain compromise when combined with standard Active Directory attack techniques.

Microsoft’s patch is well-engineered, with three independent layers of defense that each address a different aspect of the attack chain. It’s a good example of how patching done right doesn’t just fix a bug but closes off entire categories of follow-on abuse.

More broadly, this vulnerability reinforces a pattern we see again and again in modern Windows exploitation. A single primitive rarely gets you code execution on a fully-mitigated system. The interesting work, and the real defensive value, is in understanding how primitives chain together and where the mitigation layers intersect.


Acknowledgments

Thanks to Andrea Pierini for the original vulnerability discovery and the critical observation about the epmap component. Thanks to the Zero Day Initiative for their March 2026 patch analysis. Thanks to the impacket project for the RPC protocol implementation used in our tooling. And thanks to the Microsoft Security Response Center for the comprehensive, multi-layered patch.


This research was conducted in an isolated lab environment with explicit authorization. All testing was performed on systems owned and operated by the researcher.