Files
An0nUD4Y-Offensive-COM/03-com-privilege-escalation.md
2026-07-18 21:50:11 +05:30

91 KiB
Raw Permalink Blame History

03 · COM for Local Privilege Escalation

Audience / level: Red team operators and Windows internals students; no COM background assumed, ramping to original-research methodology. Prerequisites: basic Windows administration (services, registry, tokens), TCP/IP fundamentals, and the ability to read short C++ snippets. Estimated reading time: 120150 minutes. After this chapter you will be able to explain the COM impersonation contract and every way it breaks, reproduce the DiagHub and USO weaponization chains, pick the correct potato for any build from Windows 7 to Server 2022, recognize trapped-COM-object and IDispatch primitives, and run a repeatable workflow for finding new COM LPE bugs yourself.

Contents


1. Why out-of-process COM servers are LPE targets

COM objects are activated through the Service Control Manager / RPCSS service. An out-of-process (OOP) server is registered under HKLM\SOFTWARE\Classes\CLSID\{CLSID} either with a LocalServer32 value naming an executable, or with an AppID that names a Windows service hosting the class. That server process frequently runs as NT AUTHORITY\SYSTEM, LOCAL SERVICE, or NETWORK SERVICE — which is exactly why it is interesting: if an unprivileged user can activate the object and drive one of its methods, the user is steering code that executes inside a privileged process.

Beginner note: A CLSID (class identifier) is the GUID naming a COM class, e.g. {42CBFAA7-A4A7-47BB-B422-BD10E9D02700}. An AppID is a second GUID, under HKLM\SOFTWARE\Classes\AppID\{AppID}, that groups classes under one activation policy: which identity the server runs as (RunAs), which service hosts it, and who may launch or access it. When you audit "COM permissions" for LPE, you audit AppIDs, not CLSIDs.

Whether a low-privileged user may even talk to such an object is governed by two SDDL strings stored as values under the AppID key:

  • LaunchPermission — who may activate the class. The rights mask includes ExecuteLocal 0x2 and ActivateLocal 0x8.
  • AccessPermission — who may call methods on an already-running object. Machine-wide fallbacks for both live at HKLM\SOFTWARE\Microsoft\Ole\DefaultLaunchPermission and DefaultAccessPermission (editable through DCOMCNFG).

Beginner note: An SDDL string is the text form of an ACL; each parenthesized ACE is (type;;rights;;;SID). Reading O:SYG:SYD:(A;;CCDCSW;;;AU): the ACE grants rights CCDCSW to AU — Authenticated Users (S-1-5-11). Other principals you will see here: INTERACTIVE (S-1-5-4), ALL APPLICATION PACKAGES (AppContainer apps), and LPAC capability SIDs.

Many inbox services grant launch and access to Authenticated Users, INTERACTIVE, or even AppContainer/LPAC principals. The Diagnostics Hub service (section 4) is the canonical example: its launch SDDL contains (A;;CCDCSW;;;AU) — Authenticated Users — plus ALL APPLICATION PACKAGES and an LPAC capability SID, meaning both normal users and sandboxed apps may activate a SYSTEM object. Any SYSTEM object with a dangerous method and a permissive AppID is a candidate LPE primitive.

The call path from your code to the privileged method is:

Client → Proxy → COM library / RPC runtime → ALPC or TCP/NP transport → Stub → Server object

Marshalling across that boundary is NDR ("stubless" proxies generated from type information) or type-library driven (oleaut32 automation marshalling). Everything about who the server acts as during the call relies on one mechanism — the impersonation contract in section 2 — and most of this chapter is a catalogue of how that contract fails.

Key idea: The LPE question for any COM class is always the same three-tuple: (1) does the AppID let me activate and call it, (2) does the server run as a higher identity, (3) does any method touch a resource I control — a file path, a registry key, a DLL name, a process, or an interface pointer. If all three are yes, you have a lead.

Legal: Every technique in this chapter escalates privileges on a Windows host. Use only inside an authorized engagement or your own lab. The PoCs below reproduce the original researchers' public work; several are intentionally destructive to the target build (they overwrite System32 files).


2. The impersonation contract

Every COM LPE in this chapter is, at root, a broken promise about whose token the server uses. Learn the contract precisely; the bug taxonomy in section 3 is just its failure modes.

Beginner note: An access token is the kernel object representing a logon session's identity: user SID, group SIDs, privileges. Threads normally run with the process primary token. Impersonation lets a server thread temporarily wear a copy of its client's token (an impersonation token) so access checks during a call evaluate against the caller, not the privileged server; RevertToSelf() drops it. An integrity level (IL) — Untrusted, Low, Medium, High, System — is a mandatory-integrity label inside the token; it gates writes, but as section 6 shows, IL alone is not a security boundary.

The client controls what it offers the server at two levels:

  1. Process-wide, once per process: CoInitializeSecurity(pSecDesc, cAuthSvc, asAuthSvc, pReserved1, dwAuthnLevel, dwImpLevel, pAuthList, dwCapabilities, pReserved3) sets the default authentication and impersonation levels for all proxies ([74]).
  2. Per-proxy — the call that matters for exploitation: CoSetProxyBlanket(pProxy, dwAuthnSvc, dwAuthzSvc, pServerPrincName, dwAuthnLevel, dwImpLevel, pAuthInfo, dwCapabilities), a wrapper for IClientSecurity::SetBlanket ([74]). A client can raise (or lower) the impersonation level on any individual interface pointer it holds.

The impersonation-level constants the client passes ([77]):

Constant Value Meaning
RPC_C_IMP_LEVEL_DEFAULT 0 Inherit process default
RPC_C_IMP_LEVEL_ANONYMOUS 1 Token unusable by the server
RPC_C_IMP_LEVEL_IDENTIFY 2 Server can identify the client only — the token cannot be used to access objects
RPC_C_IMP_LEVEL_IMPERSONATE 3 Server may act as the client on the local machine
RPC_C_IMP_LEVEL_DELEGATE 4 Server may act as the client off-machine (Kerberos only)

Two capability flags decide which client token the proxy presents: EOAC_STATIC_CLOAKING (0x20) presents the token captured when the blanket was set; EOAC_DYNAMIC_CLOAKING (0x40) presents the current thread token on each call.

Authentication levels matter too: RPC_C_AUTHN_LEVEL_CONNECT = 2 up through PKT_INTEGRITY = 5 and PKT_PRIVACY = 6 — the KB5004442 / CVE-2021-26414 DCOM hardening (section 6) is defined in terms of forcing a minimum of PKT_INTEGRITY for remote activation ([78]).

Server side — the correct pattern ([75]). A security-aware OOP server must verify what it was offered, impersonate, do the dangerous work as the client, and revert:

DWORD impLevel; DWORD authnSvc, authzSvc, authnLevel, caps; RPC_AUTHZ_HANDLE privs;
CoQueryClientBlanket(&authnSvc, &authzSvc, NULL, &authnLevel, &impLevel, &privs, &caps);
if (impLevel < RPC_C_IMP_LEVEL_IMPERSONATE) return E_ACCESSDENIED;   // often missing!
HRESULT hr = CoImpersonateClient();   // thread token := client's token
/* ... privileged operation performed AS THE CLIENT ... */
CoRevertToSelf();

The check on the second line is the whole game: without it, a server may happily operate on an identification-level token (and fail mysteriously), or — worse for security — skip impersonation entirely and operate as SYSTEM. Transport equivalents exist for non-COM servers, and the potato family abuses all of them: raw RPC servers use RpcImpersonateClient()/RpcRevertToSelf(), named-pipe servers use ImpersonateNamedPipeClient()/RevertToSelf(), and the kernel-side equivalent is PsImpersonateClient gated by SeTokenCanImpersonate.

When a server receives only an IDENTIFY-level token and tries to use it (duplicate it to a primary, open objects), the call fails with error 1346 ERROR_BAD_IMPERSONATION_LEVEL. That error is the signature of the identification-token wall the whole potato family ran into after Microsoft's 2018 hardening (section 7).

Figure 1 — The COM impersonation contract: client blanket, server-side check, impersonate, revert How to read this figure: the left lane is the client offering an impersonation level via CoSetProxyBlanket; the right lane is the server. The guarded box around CoImpersonateClient is the access check most LPE bugs are missing — follow the dashed arrows for the failure modes (no impersonation, late impersonation, identify-level token).

Key idea: Impersonation is opt-in by the client. A malicious client that wants a privileged server to act as SYSTEM must either find a server that never impersonates (bug class 1), or find a path where the server impersonates, checks, and then reverts before doing the dangerous operation (bug class 2). Conversely, a client that wants to capture a SYSTEM token must offer RPC_C_IMP_LEVEL_IMPERSONATE itself — exactly what the USO client in section 5 does.


3. How the contract fails: the LPE bug taxonomy

Six failure modes cover nearly every COM LPE published to date. Memorize this list — it is also the triage checklist for original research in section 10.

  1. No impersonation at all. The server performs file, registry, or process operations as SYSTEM on attacker-controlled paths without ever calling CoImpersonateClient(). This is the classic privileged-file-operation bug class ([68]): the client passes a path, the server opens/moves/writes it with its own token. DiagHub's AddAgent (section 4) is the archetype — the client names a DLL inside C:\Windows\System32 and the SYSTEM service loads it.
  2. Late or short-lived impersonation. The server impersonates, performs an access check, then calls RevertToSelf() and performs the write as SYSTEM. The check is honest and the operation is still exploitable, because the resource being written can be swapped or linked after the check. The Storage Service's SvcMoveFileInheritSecurity — which applies inherited ACEs with SetNamedSecurityInfo() after reverting — is the documented instance, and it is stage one of the DiagHub chain (section 4).
  3. Identification-token confusion. The server (or attacker tooling) ends up holding only an IDENTIFY-level token. Using it fails with error 1346 ERROR_BAD_IMPERSONATION_LEVEL. This failure mode is why early RoguePotato experiments captured "useless" tokens, and why Microsoft's post-2018 CLSID hardening deliberately downgrades captured tokens to Identification — a dead token is the mitigation (sections 6 and 7).
  4. Cloaking confusion. Static vs dynamic cloaking picks the wrong token when a server itself makes outbound calls. Under EOAC_STATIC_CLOAKING the proxy presents the token captured at blanket-set time even if the thread has since impersonated someone else; under EOAC_DYNAMIC_CLOAKING it presents the current thread token. In relay and reflection chains, the wrong choice is the difference between reflecting a SYSTEM authentication and reflecting an anonymous one ([76]).
  5. Cross-session / "interactive user" activation. An AppID with RunAs = Interactive User causes the object to be instantiated in someone else's logon session when activated from session 0 or another user's session. Combined with missing client verification in the server, activation from a low session lands code execution in a privileged user's session — the Session Moniker chain behind CVE-2017-0100 (section 6).
  6. Marshalled-by-reference leaks. Object-oriented remoting marshals returned objects by reference by default: the object stays inside the server and the client receives a remote pointer. If the server hands out an object that was never meant to cross the trust boundary, the client now drives privileged code living inside the server process — the "trapped COM object" bug class of section 8, with a 2025-vintage PPL-injection PoC built entirely on IDispatch/ITypeInfo.

Figure 2 — Bug class 1: the missing impersonation call, the most common COM LPE root cause How to read this figure: the client's attacker-controlled path enters the server method on the left. The correct flow (top) impersonates before touching the resource; the vulnerable flow (bottom) skips straight to the privileged operation, so the file/registry/process access is made with the server's SYSTEM token.


4. Case study: DiagHub (Forshaw 2018)

Since: Windows 10 (the Windows 7/8.1 analog is the "IE ETW Collector") · Affects: Windows 10 builds before 19H1/1903 · Researcher: James Forshaw, Project Zero, April 2018, with companion bugs P0 issues 1427 and 1428 ([1], [2]).

The Microsoft (R) Diagnostics Hub Standard Collector Service ("DiagHub") collects ETW diagnostics on behalf of sandboxed apps (Edge/IE). It runs as SYSTEM, shipping as DiagnosticsHub.StandardCollector.Service.exe plus DiagnosticsHub.StandardCollector.Runtime.dll. It had already been abused by Lokihardt (sandbox escape) and by Forshaw (user to SYSTEM), and CVEs kept landing afterwards: CVE-2018-0952 (Atredis Partners PoC, [66]) and a later case-sensitivity directory-traversal EoP (irsl PoC, [67]).

4.1 The DCOM object

  • CLSID {42CBFAA7-A4A7-47BB-B422-BD10E9D02700} ("Diagnostics Hub Standard Collector Service"), DCOM-activatable by normal users and by AppContainer/LPAC sandboxes (the (A;;CCDCSW;;;AU) AppID of section 1).
  • Interfaces: IMarshal, IUnknown, IStandardCollectorAuthorizationService (IID 2D2AC45D-03BB-4B8A-8EFE-93EF98217054), and IStandardCollectorService (IID 0D8AF6B7-EFD5-4F6D-A834-314740AB8CAA); proxy CLSID 4323664B-B884-4929-8377-D2FD097F7BD3.
  • IStandardCollectorService exposes 8 methods (3 IUnknown + 5): CreateSession, GetSession, DestroySession, DestroySessionAsync, AddLifetimeMonitorProcessIdForSession. Forshaw recovered the names with the OleViewDotNet IPID→VTable workflow plus WinDbg dqs DiagnosticsHub_StandardCollector_Runtime+0x36C78 L8, and the signatures via OleViewDotNet's "View Proxy Definition" NDR decompilation — the exact recipe generalized in section 10.

4.2 The load-anything primitive

The session object's AddAgent(dllName, guid) loads a collector "agent" DLL. The simplified server code ([1]):

void EtwCollectionSession::AddAgent(LPWCSTR dll_path, REFGUID guid) {
    WCHAR valid_path[MAX_PATH];
    if (!GetValidAgentPath(dll_path, valid_path)) return E_INVALID_AGENT_PATH;
    HMODULE mod = LoadLibraryExW(valid_path, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
    dll_get_class_obj = GetProcAddress(hModule, "DllGetClassObject");
    return dll_get_class_obj(guid);
}

GetValidAgentPath constrains the load to C:\Windows\System32but not to any file extension. So: place any file (for example license.rtf) whose contents are a valid DLL exporting DllGetClassObject into System32, call CreateSession then AddAgent(L"license.rtf", guid), and your code runs as SYSTEM, outside DllMain and outside loader lock (the service calls DllGetClassObject explicitly after load). Client skeleton from the blog:

CoCreateInstance(CLSID_CollectorService, nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&service));
SessionConfiguration config = {}; config.version = 1;
config.monitor_pid = GetCurrentProcessId(); CoCreateGuid(&config.guid);
config.path = SysAllocString(L"C:\\Dummy");
service->CreateSession(&config, nullptr, &session);
session->AddAgent(L"dummy.dll", agent_guid);

4.3 The full exploit chain (issue 1428)

DiagHub supplies the loader; the Storage Service supplies the write. The chain ([1], [2]):

  1. StorSvc SvcMoveFileInheritSecurity impersonates the caller, MoveFileEx()es a file, then reverts impersonation and calls SetNamedSecurityInfo() as SYSTEM to apply inherited ACEs — bug class 2 from section 3. Two bugs fall out: the failed-ACL revert path allows arbitrary file creation (issue 1427), and via hard links SetNamedSecurityInfo applies attacker-chosen ACEs to any file SYSTEM can WRITE_DAC (issue 1428).
  2. Pick a non-TrustedInstaller-owned, non-critical System32 file — the blog uses C:\Windows\System32\license.rtf. Candidates are enumerable with NtObjectManager's Get-AccessibleFile -AccessRights WriteDac run against the StorSvc process token.
  3. Overwrite license.rtf with a DLL implementing DllGetClassObject.
  4. DiagHub CreateSessionAddAgent("license.rtf", …)DLL loaded as SYSTEM.
  5. The same loader primitive was later reused by the Metasploit module for the CVE-2019-0841 AppXSvc hardlink EoP, whose credits read "James Forshaw — Code creating hard links and communicating with DiagHub service".

Beginner note: A hard link is a second directory entry pointing at the same underlying file data. Created through the right system calls, it needs no ownership of the target, so an attacker makes a file they do control alias a file SYSTEM owns — then any "set ACL on the controlled path" operation retargets the privileged file. WRITE_DAC is the access right to change a file's permissions.

Figure 3 — The DiagHub chain: StorSvc write primitive into the AddAgent loader How to read this figure: read left to right. SvcMoveFileInheritSecurity plus a hard link converts a weak ACL operation into WRITE_DAC on license.rtf; the file is overwritten with a DLL; CoCreateInstance({42CBFAA7-…})CreateSessionAddAgentLoadLibraryExW executes it as SYSTEM. The mitigation ribbon at the right marks the 19H1 ProcessImageLoadPolicy fix.

4.4 Microsoft's mitigation

Windows 10 19H1 / 1903 hardened DiagHub with a ProcessImageLoadPolicy restricting the collector to loading Microsoft-signed binaries only (first flagged publicly by @x9090 and @decoder_it on Twitter in Jan/Feb 2019). That killed the "load an arbitrary-extension file from System32" trick outright. The USO trick of section 5 was its replacement — and that was hardened in turn in 2020-era Insider builds.

Key idea: Microsoft increasingly kills the weaponization primitive — loader policy, signing-level checks, KnownDlls — rather than each individual file-write bug. The same pattern reappears as the oleaut32 VerifyTrust signing-level check in section 8. When you plan an engagement, assume arbitrary-file-write bugs outlive their public weaponization paths.


5. Case study: USO and UsoDllLoader (itm4n 2019)

Since: August 2019, itm4n — "Weaponizing Privileged File Writes with the USO Service", parts 1 and 2 ([9], [10]); tool UsoDllLoader ([11]). · Affects: mainstream Windows 10 of the era; broken on 2020-era Insider builds and later patched.

5.1 The primitive

The Update Session Orchestrator service (UsoSvc, implemented in C:\Windows\System32\usosvc.dll, running as SYSTEM) attempts to load a non-existent DLL, windowscoredeviceinfo.dll, from System32 every time an Update Session is created ([9]). Any privileged-file-write primitive therefore becomes code execution: drop a payload as C:\Windows\System32\windowscoredeviceinfo.dll, then trigger a scan — via the built-in undocumented client usoclient.exe StartScan, or directly over COM — and the service loads the DLL and calls a specific export, so payload code runs as SYSTEM outside DllMain, avoiding loader-lock constraints.

Warning: This is not a vulnerability by itself. It is a weaponization bridge — the successor to DiagHub after 1903 — that converts any arbitrary-write-to-System32 primitive (a potato, a StorSvc bug, a spooler bug) into SYSTEM code execution. itm4n's repo notes (June 2020) that the trick was already broken on then-current Insider builds while still working on mainstream Windows 10; treat it as historical-but-instructive and verify per target build.

5.2 The COM interface map

Part 2 of the series reconstructs the client side of the orchestrator from scratch ([10]):

  • CLSID UpdateSessionOrchestrator = {B91D5831-B1BD-4608-8198-D72E155020F7}; AppID {E7299E79-75E5-47BB-A03D-6D319FB7F886}; server usosvc.dll.
  • CoCreateInstance requests IID IUpdateSessionOrchestrator = {07F3AFAC-7C8A-4CE7-A5E0-3D24EE8A77E0}, whose 5 methods past IUnknown are CreateUpdateSession, GetCurrentActiveUpdateSessions, LogTaskRunning, CreateUxUpdateManager, CreateUniversalOrchestrator (recovered from the usosvc.dll VTable).
  • IUsoSessionCommon IID = {FCCC288D-B47E-41FA-970C-935EC952F4A4} — 68 methods; OleViewDotNet auto-generated the interface code once the IID was known. Another GUID seen in a QueryInterface during the work: C57692F8-8F5F-47CB-9381-34329B40285A.

The reconstructed "StartScan" client flow, verbatim from part 2:

CoInitializeEx(0, COINIT_MULTITHREADED);
CoCreateInstance(CLSID_UpdateSessionOrchestrator, nullptr, CLSCTX_LOCAL_SERVER,
                 IID_PPV_ARGS(&updateSessionOrchestrator));
updateSessionOrchestrator->LogTaskRunning(L"StartScan");
updateSessionOrchestrator->CreateUpdateSession(1, &IID_IUsoSessionCommon, &usoSessionCommon);
CoSetProxyBlanket(usoSessionCommon, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
                  COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_DEFAULT,
                  RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, NULL);
usoSessionCommon->Proc21(0, 0, L"ScanTriggerUsoClient");   // VTable[21] = StartScan trigger
CoUninitialize();

Note the CoSetProxyBlanket(... RPC_C_IMP_LEVEL_IMPERSONATE ...) line: the client must offer impersonation for the privileged server to act on its behalf. That is the section 2 contract observed in the wild, in Microsoft's own client code.

5.3 The RE methodology (why this case study matters)

The interface above was undocumented. itm4n's workflow, step by step:

  1. Static: symchk to pull PDB symbols; string cross-reference on "StartScan" leads to PerformOperationOnSession() / PerformOperationOnManager() in usosvc.dll.
  2. Dynamic: breakpoint on _guard_dispatch_icall_fptr (the CFG indirect-call thunk) immediately after CoCreateInstance to catch every proxy dispatch; WinDbg dqs on the proxy VTable (usoapi!IUpdateSessionOrchestratorProxyVtbl) to dump slot addresses.
  3. Slot matching: map each client-side ObjectStublessClientN slot to its server-side symbol in usosvc.dll — that is how Proc21 was identified as the StartScan trigger.
  4. Tool failure and pivot: OleViewDotNet failed to expand the object (ClassFactory cannot supply requested class), so the interface was recovered by client-side dynamic analysis instead of OVDN introspection. A realistic "the tool fails, you pivot" moment — expect it (section 10).

6. DCOM NTLM reflection and the hardening timeline

This section separates two bugs that are constantly conflated, then lays out the eight-year Microsoft hardening campaign that the entire potato family is a reaction to.

Key idea: CVE-2015-2370 (MS15-076) is the DCOM/DCE-RPC local NTLM reflection bug — Project Zero issue 325. CVE-2017-0100 (MS17-012) is a different DCOM LPE: the HelpPane.exe interactive-user client-verification bug from Forshaw's COM Session Moniker research. Same subsystem, different mechanism, different year. Do not conflate them; the timeline below keeps them on separate rows.

Beginner note: NTLM reflection means convincing a privileged process to start an NTLM authentication to you, then feeding that authentication back into a local server context you control, so the local SSPI handshake completes and yields an impersonation token for the privileged identity. No password is cracked; attacker and target share one machine.

6.1 The root bug: CVE-2015-2370 (MS15-076, July 14 2015)

Reported by James Forshaw on 2015-04-09 as "Windows: DCOM DCE/RPC Local NTLM Reflection Elevation of Privilege" (Project Zero issue 325, [3]) — a follow-on to the unfixed WebDAV NTLM reflection issue 128.

Mechanism. When a DCOM object reference is marshalled by reference (OBJREF_STANDARD), the OBJREF carries RPC string bindings: a TowerID plus an address string. Specifying TowerID NCACN_IP_TCP and a string of the form host[port] makes the object resolver (rpcss, acting for the privileged server) open a TCP connection to an arbitrary local port and perform NTLM authentication per the security bindings. A local attacker relays that SYSTEM NTLM exchange back into a local AcceptSecurityContext() loop and negotiates a SYSTEM impersonation token locally. In Forshaw's original PoC the reflected authentication was aimed at the local DCOM activation service to activate CLSID_Package {F20DA720-C02F-11CE-927B-0800095AE340} as SYSTEM; a variant wrote a file to C:\Windows (2).

Fix. MS15-076 — "the authentication implementation in the RPC subsystem … does not prevent DCE/RPC connection reflection" — added reflection protections to the RPC runtime (rpcrt4.dll). Weaponization history. NetSPI's "Trebuchet" ("Exploiting MS15-076 (CVE-2015-2370)", [35]) turned the PoC into arbitrary file write using Forshaw's SyScan'15 symlink/junction tricks — junctions plus \RPC Control object-manager symlinks via CreateSymlink. This bug is the direct ancestor of every potato in section 7.

6.2 CVE-2017-0100 (MS17-012, March 2017) — what it actually is

Official title: "Windows HelpPane Elevation of Privilege Vulnerability" ([36]) — "a DCOM object in Helppane.exe, configured to run as the interactive user, fails to properly authenticate the client." Local users gain privileges via a crafted application (Windows 7 SP1 through 10 1607, Server 2008 R2 through 2016). It is bug class 5 from section 3: RunAs = Interactive User plus cross-session activation (the COM Session Moniker) plus missing client verification — Helppane executed attacker content without checking who was asking.

Microsoft's fix then checked the wrong thing: Forshaw's follow-up report "Windows: Bad Fix for COM Session Moniker EoP" became CVE-2017-0298 ([37]). The HelpPane check compared integrity levels rather than verifying membership of the administrators group; IL is not a security boundary, and UIAccess IL-ratcheting could defeat it. Public text: exploit-db / 0day.today ID 28134.

Key idea: Verify who the caller is (client blanket, identity, group membership), not how elevated their token looks. An integrity level is a label, not an identity.

6.3 Microsoft's DCOM hardening timeline

Date Change Impact
Jul 2015 MS15-076 / CVE-2015-2370 — RPC runtime blocks DCE/RPC connection reflection Kills raw reflection; potato family moves to local token negotiation
Mar 2017 MS17-012 / CVE-2017-0100 (then CVE-2017-0298 bad-fix fix) Session Moniker chain closed
May 2017 CVE-2017-0213 (COM Aggregate Marshaler, CStdMarshal::Finish_RemQIAndUnmarshal2 RQI2 unpack bug) & CVE-2017-0214 (type-library load validation) patched — both Forshaw; PoC: BITS SetNotifyInterface → load attacker type lib under impersonation → scriptlet moniker → code exec in BITS as SYSTEM. 0213 later entered CISA KEV Type-library attack surface shrinks
Oct 2018 (RS5) Silent rpcss fix (Win10 1809 / Server 2019): OXID binding no longer built via string concat host[port]; new CreateRemoteBindingToOR() builds bindings via RpcStringBindingCompose()host[port][135] fails parsing; also remote OXID queries forced to port 135 only and processed as ANONYMOUS LOGON; abusable-CLSID set hardened so captured tokens become Identification tokens; PrintNotify-style CLSIDs gated to INTERACTIVE group Kills Rotten/JuicyPotato (decoder's post "No more rotten/juicy potato?", 2018-10-29, [15])
2019 (19H1/1903) DiagHub ProcessImageLoadPolicy (MS-signed loads only) Kills DiagHub weaponization
Jun 2021+ KB5004442 / CVE-2021-26414 — DCOM server hardening: minimum RPC_C_AUTHN_LEVEL_PKT_INTEGRITY enforced for remote DCOM activation (phased) Mostly a remote DCOM mitigation ([78])
Jan 2023 CVE-2023-21746 — NTLM local-auth context-swap fix in msv1_0.dll (SsprHandleChallengeMessage: if ISC_REQ_UNVERIFIED_TARGET_NAME set and SPN present → SPN forced NULL → SMB anti-reflection on cifs/127.0.0.1 fires) Kills LocalPotato SMB scenario

Every row of this table creates the conditions for the next potato: the 1809 OXID changes force RoguePotato's remote-resolver architecture, the INTERACTIVE gating forces JuicyPotatoNG's type-9 logon trick, and the NTLM context-swap fix pushes LocalPotato into its HTTP variant. Section 7 is the offensive mirror image of this table.


7. The Potato family

7.1 Common prerequisites and the three COM roles

Beginner note: SeImpersonatePrivilege ("Impersonate a client after authentication") lets a process legitimately use a captured impersonation token — duplicate it and hand it to CreateProcessWithTokenW. SeAssignPrimaryTokenPrivilege ("Replace a process level token") lets a process swap in a new primary token via CreateProcessAsUser. Both are held by default by service accounts: IIS AppPool identities, MSSQL service accounts, LOCAL SERVICE, NETWORK SERVICE, scheduled-task service accounts. That is why every potato is a service-account to SYSTEM escalation, not user to SYSTEM.

Token-type math (all potatoes, [30]): the captured SYSTEM token is an impersonation token. To run a process you either duplicate it to a primary token and call CreateProcessWithTokenW (needs SeImpersonatePrivilege), or call CreateProcessAsUser (needs SeAssignPrimaryTokenPrivilege). If the captured token is only Identification-level, both paths fail with error 1346 — the taxonomy's failure mode 3.

Key idea: COM appears in this family in exactly three roles. (a) The trigger — a DCOM activation (CoGetInstanceFromIStorage with a crafted IStorage) that forces a privileged COM server to authenticate to the attacker. (b) The OXID resolver — rpcss's IObjectExporter::ResolveOxid2 interface (UUID 99FCFEC4-5260-101B-BBCB-00AA0021347A) that locates objects, and that attackers fake or redirect. (c) The callback surface — attacker-implemented COM objects (fake IUnknown/IRemUnknown2 servers) into which the privileged side calls back, enabling CoImpersonateClient()/RpcImpersonateClient(). Everything else in these chains — NTLM relay, named-pipe impersonation, MS-RPRN/MS-EFSR coercion, SMB context swapping — is RPC/SMB/SSPI, not COM. Precise attribution matters when you hunt for variants and when you write detections.

7.2 Pre-history: Hot Potato and RottenPotatoNG

  • Hot Potato (FoxGlove, January 2016, [29]): NBNS spoofing + WPAD + Windows Update HTTP NTLM to SYSTEM; patched as MS16-075 (Metasploit windows/local/ms16_075_reflection). No COM anywhere — included only because the name causes confusion. RottenPotatoNG (FoxGlove, [31]): the official C++ port of RottenPotato without the Meterpreter/Incognito dependency; the codebase SweetPotato and CandyPotato later wrapped.

7.3 RottenPotato: local NTLM reflection over DCOM (2016)

Since: FoxGlove Security — @breenmachine & @vvalien1, September 2016 ([28], [31]) · Affects: ≤ Windows 10 1803 / Server ≤ 2016 era; killed by the 1809 OXID changes · COM role: trigger + OXID redirect.

RottenPotato is CVE-2015-2370's mechanism repackaged for post-MS15-076 builds: instead of reflecting raw RPC, it MITMs the local OXID resolution of a DCOM activation and completes the NTLM handshake against a local SSPI server context.

Mechanics

  1. COM trigger: CoGetInstanceFromIStorage(NULL, CLSID_BITS, …) with the hardcoded BITS CLSID {4991D34B-80A1-4291-83B6-3328366B9097} and a hand-crafted IStorage whose string bindings point at 127.0.0.1:6666. This forces the BITS COM server (SYSTEM) to speak DCOM/RPC to the attacker's TCP listener.
  2. Byte-level relay (RPC, not COM): the RPC conversation is relayed to the real OXID resolver on TCP 135 so the activation proceeds normally.
  3. The critical trick (SSPI): when the NTLMSSP exchange begins, the MITM swaps the Type 2 message's challenge plus "Reserved" field — the Reserved field is a reference to the local SecHandle — with output of a local AcquireCredentialsHandle + AcceptSecurityContext() loop.
  4. SYSTEM's Type 3 completes the local authentication → ImpersonateSecurityContext() yields a SYSTEM impersonation token → the original tool hands it to Meterpreter's Incognito to use.
  5. Result: 100% reliable, instant, cross-version SYSTEM from any account holding SeImpersonatePrivilege — on builds up to 1803.

Figure 4 — RottenPotato: BITS DCOM trigger into a local NTLM reflection loop How to read this figure: the BITS server (SYSTEM) is tricked into authenticating to the attacker's local listener via the crafted IStorage. The MITM box in the middle splices the NTLM Type 2 Reserved field into a local AcceptSecurityContext loop; the output on the right is a SYSTEM impersonation token.

Prerequisites

Requirement Detail
Privileges SeImpersonatePrivilege (or SeAssignPrimaryTokenPrivilege) — service accounts
OS/build ≤ Windows 10 1803 / Server 2016 era; BITS abusable as trigger CLSID, local TCP listener allowed

Proof of concept

# Original workflow: Meterpreter session running as a service account *(illustrative)*
load incognito
execute -Hc -f RottenPotato.exe
list_tokens -u                          # SYSTEM impersonation token now available
impersonate_token "NT AUTHORITY\\SYSTEM"

OpSec & detection

  • Local listener on the hardcoded port 6666 and a byte-relay to TCP 135 on the same host — a service account opening loopback RPC relays is anomalous. Process lineage: a service account (IIS AppPool, MSSQL) spawning cmd.exe/payload as SYSTEM is the loudest indicator, shared by every potato.

Mitigations

  • The 1809 / Server 2019 rpcss changes (section 6.3) kill the technique outright; minimize service-account privileges so the prerequisite is absent.

7.4 JuicyPotato: arbitrary-CLSID reflection (2018)

Since: ohpe/@Giutro & @decoder_it, August 2018 ([32], [33], [34]) · Affects: up to Windows 10 1803 / Server 2016; broken by the 1809/2019 silent fix · COM role: trigger + OXID redirect.

"RottenPotatoNG on steroids": the same reflection engine, generalized to arbitrary CLSID (-c), arbitrary local COM-listen port (-l), arbitrary payload program (-p), with -t * trying both CreateProcessWithTokenW and CreateProcessAsUser. No Meterpreter dependency.

Mechanics

  1. The authors brute-forced and enumerated abusable CLSIDs. A usable CLSID must satisfy three criteria: (1) instantiable by the current (service) user, (2) implements IMarshal, (3) runs as an elevated account.
  2. Per-OS CLSID lists were published at ohpe.it/juicy-potato/CLSID/ — BITS {4991D34B-80A1-4291-83B6-3328366B9097}, wuauserv/aucore {e60687f7-01a1-40aa-86ac-db1cbf673334}, and others; DiagHub's own CLSID {42CBFAA7-A4A7-47BB-B422-BD10E9D02700} appears in the Server 2016 list.
  3. The chosen CLSID replaces BITS in the RottenPotato trigger; the rest of the chain (Type 2 Reserved-field swap, local SSPI completion, token duplication) is identical.

Prerequisites

Requirement Detail
Privileges SeImpersonatePrivilege and/or SeAssignPrimaryTokenPrivilege
OS/build ≤ Windows 10 1803 / Server 2016, with a valid CLSID for that exact OS from the published lists

Proof of concept

# *(illustrative — typical published invocation)*
JuicyPotato.exe -l 1337 -p C:\Windows\System32\cmd.exe -t * -c "{4991D34B-80A1-4291-83B6-3328366B9097}"
# -l local COM listen port, -p program to run, -t * = try both token APIs, -c CLSID

OpSec & detection

  • Same lineage indicator as RottenPotato; additionally, activation attempts against unusual service CLSIDs (wuauserv, DiagHub, PrintNotify) from a service-account process are high-fidelity, and repeated DCOM activation failures from one account (wrong-OS CLSID picks) are alertable noise.

Mitigations

  • 1809/2019 rpcss hardening (section 6.3). The CLSID-list approach also died because the surviving abusable-CLSID set was hardened so captured tokens become Identification-level, and PrintNotify-style CLSIDs were gated to the INTERACTIVE group.

7.5 RogueWinRM: BITS over HTTP (side quest)

Since: decoder's "From NETWORK SERVICE to SYSTEM" ([19]) + antonioCoco's RogueWinRM ([26]) · Affects: client SKUs where WinRM is not listening (the default) · COM role: none directly — HTTP — but the BITS trigger is the same service family.

When BITS starts, it attempts HTTP NTLM authentication to 127.0.0.1:5985 (WinRM). If WinRM is off — the default on client SKUs — an unprivileged user can bind port 5985, trigger BITS, and capture the SYSTEM HTTP NTLM authentication into a local SSPI context, yielding a SYSTEM token. RogueWinRM ships as one of SweetPotato's methods (7.8), and is the minimal demonstration that the trigger and the capture are independent problems: swap DCOM for HTTP and the same token math applies.

7.6 RoguePotato: fake OXID resolver and token kidnapping (2020)

Since: @decoder_it & @splinter_code, May 2020 ([14], [24]) · Affects: Windows 10 1809+ / Server 2019+ — exactly where JuicyPotato died · COM role: trigger + fake OXID resolver (callback surface); the impersonation and token steps are pipes/SSPI.

The comeback after the 1809 kills. The problem: post-1809, remote OXID queries are forced to port 135 and processed as ANONYMOUS LOGON, and the old local-resolver trick is dead. RoguePotato's answer: make the privileged client talk to an attacker-controlled machine's port 135, redirect that to a fake OXID resolver, and steer the client onto a named pipe where impersonation actually works.

Mechanics

  1. COM trigger (COM): CoGetInstanceFromIStorage with the BITS CLSID {4991d34b-80a1-4291-83b6-3328366b9097} (default; -c to override) and an IStorage whose string bindings point at a remote attacker IP.
  2. Redirect: the attacker box runs socat tcp-listen:135,reuseaddr,fork tcp:<victim>:9999, forwarding to the fake RogueOxidResolver RPC server — which can run on the victim on port 9999, or standalone on an attacker Windows box.
  3. Fake OXID resolver (COM): implements IObjectExporter — answers ServerAlive2 with RPC_S_OK, and forges ResolveOxid2 to return a DUALSTRINGARRAY with TowerID ncacn_np and endpoint string localhost/pipe/roguepotato[\pipe\epmapper], plus security bindings naming NT AUTHORITY\NETWORK SERVICE. (The first attempt, ncacn_ip_tcp:localhost[9998] with an IRemUnknown2 callback and RpcImpersonateClient(), yielded only an Identification token — error-1346 dead end. Moving to the named-pipe transport was the fix.)
  4. Path-validation bug (borrowed from PrintSpoofer, credit @itm4n & @jonasLyk): the / in the hostname is normalized into the pipe path, so rpcss connects to the attacker pipe \\.\pipe\roguepotato\pipe\epmapper — the protocol forces the \pipe\epmapper suffix, but the /pipe/roguepotato prefix redirects it.
  5. Impersonation (not COM): ImpersonateNamedPipeClient() yields a NETWORK SERVICE impersonation token carrying the rpcss logon-session LUID.
  6. Token kidnapping: rpcss/RpcEptMapper runs as NETWORK SERVICE but shares a logon session with SYSTEM-token-holding threads (Forshaw's "Sharing a Logon Session a Little Too Much", [6]). Enumerate handles in the rpcss process, duplicate a SYSTEM token, then CreateProcessAsUser()/CreateProcessWithToken() (with a Session-0 window-station permission fix) → SYSTEM.

Figure 5 — RoguePotato: remote-triggered fake OXID resolver and token kidnapping How to read this figure: swimlanes separate what is COM (trigger, ResolveOxid2 forgery) from what is pipe/SSPI (ImpersonateNamedPipeClient, token duplication). Follow the binding string localhost/pipe/roguepotato[\pipe\epmapper] — the slash normalization is what lands the SYSTEM-side rpcss on the attacker's pipe.

Prerequisites

Requirement Detail
Privileges SeImpersonatePrivilege
OS/build Windows 10 1809+ / Server 2019+
Config Attacker-controlled machine reachable from the victim on TCP 135 (or a local resolver plus port proxy)

Proof of concept

# Attacker box (redirect 135 back to the victim's fake resolver):
socat tcp-listen:135,reuseaddr,fork tcp:<victim>:9999
# Victim, as the service account *(illustrative — typical published invocation)*:
RoguePotato.exe -r <attacker-ip> -e "C:\Windows\System32\cmd.exe" -l 9999

OpSec & detection

  • Outbound/inbound TCP 135 to a non-domain-controller from a server running a service-account process is the loudest network indicator; socat on the attacker side is your own infrastructure.
  • Local artifacts: the named pipe \\.\pipe\roguepotato\pipe\epmapper, handle enumeration inside the rpcss process, and a payload process spawned from a duplicated rpcss token (NETWORK SERVICE context flipping to SYSTEM).

Mitigations

  • Block workstation/server egress to untrusted hosts on TCP 135; monitor for unexpected OXID resolution. The token-kidnapping half depends on the shared-logon-session design, not a CVE; least-privilege service accounts reduce impact.

7.7 PrintSpoofer: pipe-name normalization (2020)

Since: itm4n, March 2020 ([12], [13]); coercion primitive from Lee Christensen's SpoolSample ([40]) · Affects: Windows 10, Server 2016/2019 (and older with the spooler running) · COM role: none — pure RPC coercion plus pipe impersonation; included because it supplies the path-normalization bug RoguePotato reuses.

Mechanics

  1. Coercion (pure RPC): MS-RPRN RpcRemoteFindFirstPrinterChangeNotificationEx forces the Print Spooler (SYSTEM) to connect back to an attacker-named notification pipe.
  2. The bug: printer-name validation accepts \\HOSTNAME/pipe/foo, which is later normalized to \\HOSTNAME\pipe\foo\pipe\spoolss — redirecting the SYSTEM spooler into the attacker's pipe server \\.\pipe\foo\pipe\spoolss even though the real \pipe\spoolss endpoint is occupied.
  3. Token half: CreateNamedPipe → client connects → ImpersonateNamedPipeClient()OpenThreadTokenDuplicateTokenEx (to primary) → CreateProcessWithTokenW → SYSTEM.

Prerequisites

Requirement Detail
Privileges SeImpersonatePrivilege
OS/build Windows 10, Server 2016/2019 (older with spooler); Print Spooler service running

Proof of concept

# From a service-account shell: interactive SYSTEM cmd on the console, or any -c command
PrintSpoofer.exe -i -c cmd
PrintSpoofer.exe -c "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -enc <b64>"

OpSec & detection

  • The SYSTEM spooler process connecting to a non-standard named pipe (\\.\pipe\*\pipe\spoolss) is the signature; the pipe name is attacker-chosen, so match the pattern, not the string. Also: service account spawning a child as SYSTEM via CreateProcessWithTokenW.

Mitigations

  • Disable the Print Spooler where printing is not required.

Warning (patch status, MEDIUM confidence): PrintSpoofer was unpatched at publication; later Windows builds hardened spooler behaviors, and PrintNotify-based alternatives exist for hardened or no-spooler hosts. Treat current effectiveness as build-dependent — verify on the target build before relying on it.

7.8 SweetPotato: the C# bundle (2020)

Since: @EthicalChaos / CCob, 2020 ([38], [39]); decoder's companion analysis "We thought they were potatoes but they were beans" ([20]) · Affects: "Local Service to SYSTEM … from Windows 7 to Windows 10 / Server 2019" · COM role: inherited from Rotten/Juicy (DCOM trigger) plus the named-pipe half from PrintSpoofer.

A single C# toolkit bundling the RottenPotato code base, the weaponized JuicyPotato BITS+WinRM approach (credit @decoder_it/@Guitro), RogueWinRM, and PrintSpoofer; it auto-attempts the correct technique for the target (-c CLSID, default BITS {4991D34B-80A1-4291-83B6-3328366B9097}; -m method). Its operational value is one binary for the whole pre-2020 family, executed in memory through your .NET loader of choice.

SweetPotato.exe -p C:\Windows\System32\cmd.exe -a "whoami > C:\Windows\Temp\sp.txt"   # *(illustrative)*

Note (MEDIUM confidence): the exact internal method list varies by release version; confirm -m options against the version you actually deploy.

7.9 JuicyPotatoNG: type-9 logon and an SSPI hook (2022)

Since: @decoder_it & @splinter_code, September 2022 ([16], [25]); companion piece "The impersonation game" ([17]) · Affects: Windows 10 1809+ / 11, Server 2019+ / 2022 · COM role: trigger + local callback surface with an SSPI hook.

The problem solved: post-1809, the surviving abusable CLSIDs require the caller to be INTERACTIVE, which service accounts are not. JuicyPotatoNG restores the Juicy workflow on modern builds with two tricks.

Mechanics

  1. Token enrichment: LogonUser() with logon type 9 (NewCredentials) — LSASS clones the caller's token and adds the INTERACTIVE SID (no impersonation privilege is needed for the clone). The enriched token can activate the INTERACTIVE-gated CLSIDs.
  2. Trigger: DCOM activation against the default CLSID {854A20FB-2D44-457D-992F-EF13785D2B51} — PrintNotify, the "Printer Extensions and Notifications" service, SYSTEM, present on Windows 10/11 and Server 2016/2019/2022 — with the local COM server on port 10247.
  3. Token capture: an SSPI hook on AcceptSecurityContext() intercepts the privileged DCOM client's NTLM exchange as it is processed in-process. This avoids RpcServerUseProtseqEp (which would hold the port) and, per the authors, relaxes requirements versus RpcImpersonateClient(). The approach is conceptually rooted in Forshaw's "Windows Exploitation Tricks: Relaying DCOM Authentication" (October 2021, [5]).
  4. Verified PoC output: [+] authresult success {854A20FB-…};NT AUTHORITY\SYSTEM;Impersonation — an impersonation-level SYSTEM token, duplicated and passed to the usual CreateProcessWithTokenW/CreateProcessAsUser pair.

Prerequisites

Requirement Detail
Privileges SeImpersonatePrivilege; local credentials for the type-9 LogonUser clone
OS/build Windows 10 1809+ / 11, Server 2019+ / 2022; PrintNotify CLSID present, port 10247 free

Proof of concept

# *(illustrative — typical published invocation; watch for the authresult success line)*
JuicyPotatoNG.exe -t * -p C:\Windows\System32\cmd.exe -a "whoami > C:\Windows\Temp\ng.txt"

OpSec & detection

  • LogonUser type-9 (NewCredentials) events from a service account are unusual and alertable. In-process SSPI hooking means no extra listener service; detection leans on activation of the PrintNotify CLSID by a service account and the standard potato lineage indicator.

Mitigations

  • The INTERACTIVE-gating mitigation is what this tool circumvents via LSASS behavior, not a patched bug; Microsoft fixes have targeted individual pieces (CLSID gating, resolver behavior) rather than the pattern — verify per build.

7.10 LocalPotato: NTLM context swapping (2023)

Since: @decoder_it & @splinter_code, February 2023 ([21], [23]); HTTP edition November 2023 ([22]) · Affects: builds before the January 2023 patch (CVE-2023-21746) for the SMB scenario; HTTP/WebDAV scenario unpatched · COM role: trigger only.

Key idea: LocalPotato is not a DCOM bug. COM/DCOM is used only as the trigger; the vulnerability lives in NTLM local authentication inside LSASS (msv1_0.dll), and the write primitive is SMB. Mislabeling it as a COM vulnerability will send your variant hunting to the wrong DLL.

Mechanics

  1. Trigger (COM): CoGetInstanceFromIStorage with a SYSTEM-authenticating CLSID — default {854A20FB-2D44-457D-992F-EF13785D2B51} (PrintNotify); alternatives {90F18417-F0F1-484E-9D3C-59DCEEE5DBD8} (ActiveX Installer Service "AxInstSv", Windows 10/11 only) and {A9819296-E5B3-4E67-8226-5E72CE9E1FB7} (Universal Print Management Service "McpManagementService", Windows 11 + Server 2022). A fake local OXID resolver (JuicyPotatoNG-style SSPI hooks, default port 10271) returns security bindings containing a crafted SPN cifs/127.0.0.1, so the privileged DCOM client initiates NTLM with ISC_REQ_UNVERIFIED_TARGET_NAME and that SPN.
  2. The bug (NTLM/LSASS): in NTLM local authentication, Type 2 messages carry a security-context handle referencing an in-memory server context. The attacker runs a rogue client against the local SMB server (127.0.0.1:445) in parallel and swaps the context handles between the two flows — LSASS binds the privileged (SYSTEM) identity to the attacker's SMB session ("Context Swapping").
  3. Result: the attacker's SMB client is authenticated to \\127.0.0.1\C$ as SYSTEM → arbitrary file read/write (the PoC writes raw SMB2 packets per MS-SMB2).
  4. Weaponization paths: write SprintCSP.dll to System32 and trigger SvcRebootToFlashingMode (StorSvc RPC, via BlackArrowSec's RpcClient, released 2023-02-13); or overwrite printconfig.dll and instantiate PrintNotify (the authors' Server 2022 demo); or an XPS Print Job / NetMan DLL hijack.
  5. Patch: January 2023 (CVE-2023-21746) changed SsprHandleChallengeMessage: if ISC_REQ_UNVERIFIED_TARGET_NAME is set and an SPN is present, the SPN is forced to NULL, so SMB anti-reflection denies cifs/127.0.0.1. The HTTP/WebDAV scenario remained unpatched by Microsoft decision — "LocalPotato HTTP edition" ([22]) — and 0patch covers EOL versions.

Prerequisites

Requirement Detail
Privileges SeImpersonatePrivilege (service-account context)
OS/build SMB scenario: builds before January 2023 patches; HTTP edition: unpatched at publication. Local SMB server reachable on 127.0.0.1:445; a SYSTEM-authenticating CLSID for the trigger

Proof of concept

# *(illustrative — typical published invocation; writes attacker file to a privileged path)*
LocalPotato.exe -i C:\users\public\payload.dll -o C:\Windows\System32\SprintCSP.dll

OpSec & detection

  • Loopback SMB sessions to \\127.0.0.1\C$ authenticating as SYSTEM from a service-account context are the signature; the trigger is a DCOM activation of PrintNotify/AxInstSv/McpManagementService by a service account — same indicator family as JuicyPotatoNG.
  • Weaponization touches well-known System32 binaries (SprintCSP.dll, printconfig.dll); file-integrity monitoring catches the write.

Mitigations

  • January 2023+ patches close the SMB scenario (CVE-2023-21746).

Warning (patch status): the HTTP/WebDAV edition was left unpatched by Microsoft decision as of the November 2023 publication; 0patch covers EOL versions. Verify per target build and month before assuming coverage.

7.11 The rest of the family: classification table

The family is much larger than the eight tools above. Verified existence and classification (per-author claims; mechanism column is the part that matters for variant hunting):

Tool Author/Year Mechanism COM in chain? Works on
GodPotato BeichenDream, Dec 2022 Novel DCOM trigger; author: "some defects in rpcss when dealing with OXID"; needs only SeImpersonatePrivilege; .NET 2/3.5/4 builds Yes (trigger) — service-DCOM-object activation + impersonation of the DCOM callback Win811, Server 20122022 ([44])
DCOMPotato zcgonvh "Some Service DCOM Object and SeImpersonatePrivilege abuse" — variants PrinterNotifyPotato.exe, McpManagementPotato.exe; attacker-supplied IUnknown callback → CoImpersonateClient() in its QueryInterface impersonates the SYSTEM caller (these objects default to RPC_C_IMP_LEVEL_IMPERSONATE) Yes (trigger + callback impersonation) Win10/11, Server 20122022 ([43])
PrintNotifyPotato BeichenDream, late 2022 PrintNotify CLSID + fake IUnknown/pointer-moniker callback; works even with legacy Spooler disabled Yes Win10/11, Server 20122022 ([46])
EfsPotato zcgonvh, 2021 MS-EFSR (EfsRpcOpenFileRaw/EfsRpcEncryptFileSrv PetitPotam-style) coercion of SYSTEM to attacker pipe \\.\pipe\lsarpc (alt: efsrpc/samr/lsass/netlogon) + named-pipe impersonation; CVE-2021-36942 patch-bypass notes by @xassiz No — pure RPC coercion + pipe impersonation Servers 20122022 era (pre-EFSR hardening); C# ([41])
SharpEfsPotato bugch3ck EfsRpc + pipe impersonation built from SweetPotato + SharpEfsTrigger No similar ([42])
CoercedPotato @Hack0ura & @Prepouce (Hackvens), 2023 Automated multi-coercion: tries MS-RPRN (RpcRemoteFindFirstPrinterChangeNotification[Ex]) then MS-EFSR functions (14 variants) against local pipe servers \\.\pipe\coerced\pipe\spoolss / …\srvsvc + impersonation No — pure RPC coercion Win10/11, Server 2022 (verified screenshot build 22621) ([47])
PetitPotato wh0amitz Local PetitPotam (EFSRPC) → pipe No — ([48])
RasmanPotato crisprss Coerces RasMan service (SYSTEM) via its RPC interface → pipe impersonation No Win10, Server 20122019 ([49])
BadPotato BeichenDream itm4n's PrintSpoofer ported to C# No Win810, 20122019 ([45])
CandyPotato klezVirus Pure C++ weaponized RottenPotatoNG Yes (Rotten lineage) Win10/2016 era ([50])
MultiPotato S3cur3Th1sSh1t Multi-trigger combo mixed — ([51])
SigmaPotato tylerdotrar, 202324 Modernized GodPotato-fork style; --revshell, in-memory [SigmaPotato]::Main() via .NET reflection Yes (DCOM lineage) Win811, 20122022 ([52])
GenericPotato Micah van Deusen, 2021 Generic SeImpersonation → SYSTEM via modified SSPI/HTTP named-pipe-less approach partial — ([54])
GhostPotato Shenanigans Labs, Nov 2019 MS08-068-style SMB relay bypass via NTLM challenge-cache (300 s) with modified impacket No unpatched-at-the-time SMB relay ([53])
RemotePotato0 antonioCoco/SentinelOne, 2021 Cross-protocol relay RPC→HTTP, "Won't Fix" — remote user→DA, not local LPE; boundary case partial — ([55])
CertPotato SensePost, 2022 ADCS cert path from virtual/network service accounts No — ([56])
LonelyPotato decoder, Dec 2017 RottenPotato standalone (no Meterpreter) Yes ≤1803 ([18])

Warning: No tool called "SharpPotato" exists. If a runbook, wiki, or teammate references it, the intended tool is almost certainly SweetPotato (the C# bundle), BadPotato (PrintSpoofer's C# port), or SharpEfsPotato. Do not waste engagement time hunting for a phantom repository.

Boundary cases: RemotePotato0 ([55]) is a remote cross-protocol relay (RPC→HTTP, rated "Won't Fix", remote user→DA), not a local LPE — file it, and IBM X-Force's DCOM NTLM coercion work RemoteMonologue ([62]), under lateral movement rather than this chapter. For a narrated history of the whole family, see Antonio Cocomazzi's "10 Years of Windows Privilege Escalation with Potatoes" slides (POC2023/Troopers24, [27]).

7.12 Potato decision tree

Figure 6 — Potato lineage 2016 to 2023+, aligned against the Microsoft hardening timeline How to read this figure: time runs left to right; each potato is color-coded by its mechanism (COM trigger, RPC coercion, NTLM/LSASS). The vertical markers are the hardening events of section 6.3 — notice every new tool appears immediately after a mitigation row.

Text form of the decision tree — run this on every foothold:

  1. whoami /priv → is SeImpersonatePrivilege or SeAssignPrimaryTokenPrivilege present? (No → not a potato target; look at sections 4, 8, 9 instead.)
  2. OS ≤ Win10 1803 / Server 2016 → RottenPotatoNG / JuicyPotato (BITS or a list CLSID for the exact OS).
  3. OS ≥ Win10 1809 / Server 2019 → can the victim reach your box on TCP 135 (or can you run a local resolver plus port-proxy)? → RoguePotato. Else → interactive-capable or service account? → JuicyPotatoNG (LogonUser type-9 trick) or GodPotato / DCOMPotato / PrintNotifyPotato (service DCOM callbacks).
  4. Spooler up, Win10/20162019 → PrintSpoofer (C#: BadPotato). Spooler down or hardened → EfsPotato / CoercedPotato coercion alternatives.
  5. Need a file-write primitive on builds ≤ January 2023 → LocalPotato (+ StorSvc/PrintNotify weaponization); patched → LocalPotato HTTP/WebDAV edition.
  6. .NET 4 present and OpSec matters → SweetPotato / GodPotato (.NET 4 build) / SigmaPotato for in-memory execution.

8. Trapped COM objects (Forshaw, January 2025)

Since: James Forshaw, Project Zero, January 30 2025 — "Windows Bug Class: Accessing Trapped COM Objects with IDispatch" ([4]) · Affects: current Windows 10/11 including 24H2 (with caveats below); Microsoft does not treat PPL as a security boundary, so fixes ship late and in new Windows versions.

8.1 The bug class

Beginner note: Object-oriented remoting (DCOM, .NET Remoting) marshals returned objects by reference by default: the object itself stays — is "trapped" — inside the server process, and the client receives a remote pointer. Every method call the client makes then executes in the server's security context. If the object was never designed to be exposed across the trust boundary, the client is driving privileged code.

Beginner note: PPL (Protected Process Light) is Windows' process-protection model: a PPL process can only be opened and injected into by processes signed at an equal or higher protection level (e.g. PsProtectedSignerWindows-Light). LSASS and modern AV/EDR processes run protected, so unsigned code inside a PPL process is a significant offensive prize.

Three introduction scenarios produce trapped objects:

  1. Unsafe object shared inadvertently — CVE-2019-0555. WinRT needed an XML document; developers bolted runtime interfaces onto the XML DOM Document v6 COM object assuming no XSLT. A malicious client QueryInterfaces the legacy IXMLDOMDocument, regains XSLT script execution → sandbox escape.
  2. Asynchronous marshaling primitive. .NET classes that are both serializable and MarshalByRefObject (e.g. FileInfo/DirectoryInfo): send a serialized copy to the server, then read it back — the read is marshaled by reference, yielding a trapped live object with which to create files as the server. Implemented in Forshaw's ExploitRemotingService tool.
  3. Abusing the platform's object-instantiation plumbing — CVE-2017-0211. Coerce a path to CoCreateInstance with an attacker CLSID and get the object back: Structured Storage exposed across the boundary; IPropertyBag → create an arbitrary COM object in the server → XML DOM → XSLT EoP.

8.2 The IDispatch / ITypeInfo vector (the new part)

Any OOP server exposing IDispatch must vend type information: the client calls IDispatch::GetTypeInfoITypeInfo, which is itself marshaled by reference — trapped in the server. ITypeInfo::Invoke is not remotable, but ITypeInfo::CreateInstance IS — and it calls CoCreateInstance inside the server process. The type info must describe a CoClass (it carries a CLSID); reach candidate classes via ITypeInfo::GetContainingTypeLibITypeLib → enumerate classes, including referenced type libraries (stdole is virtually always referenced).

Worked example from the post, using the OleViewDotNet PowerShell module ([4], [8]):

Get-ComClass -Service                                  # service-hosted COM classes
# → enumerate interfaces, filter IsDispatch → 5 candidates on a default box:
#   WaaSRemediation            {72566E27-1ABB-4EB3-B4F0-EB431CB1CB32}
#   SearchGatheringManager     {9E175B68-F52A-11D8-B9A5-505054503030}
#   SearchGathererNotification {9E175B6D-…}
#   AutomaticUpdates           {BFE18E9C-6D87-4450-B37C-E02F0B373803}
#   Microsoft.SyncShare.SyncShareFactoryClass {DA1C0281-456B-4F14-A46D-8ED2E21A866F}
$o = New-ComObject -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32
$lib = Import-ComTypeLib -Object $o
Get-ComObjRef $o                        # proves OOP hosting: shows svchost.exe
$lib.Parse(); $lib.ReferencedTypeLibs   # → stdole 2.0 {00020430-0000-0000-C000-000000000046}
# stdole classes: StdFont {0BE35203-8F91-11CE-9DE3-00AA004BB851}, StdPicture {0BE35204-…}

Both stdole classes are registered InProcServer32 only (oleaut32.dll), so instantiating one through the remote type library plants it inside the service. Navigation: GetRefTypeOfImplType(0)GetRefTypeInfoGetContainingTypeLibGetTypeInfoOfGuid(<StdFont CLSID>)CreateInstance()Get-ComObjRef proves the new object lives in the service process. (StdPicture has an OOP check; StdFont does not. Forshaw found no direct EoP in the font object itself — it is a stepping stone.)

Type-library/proxy substitution → type confusion. The stub for a remoted interface is generated from the type library; control the TLB (registration swap, or referencing an OOP TLB) and you control the server-side stub's view of the types — classic type confusion (Forshaw's earlier NGEN-COM attack overwrote the KnownDlls handle this way). Microsoft's countermeasure now lives in oleaut32.dll: VerifyTrust — when loading a TLB inside a protected process it requires a cached signing level ≥ 12 (Windows) via NtGetCachedSigningLevel/NtSetCachedSigningLevel; raw (non-PE) TLBs fail with STATUS_INVALID_IMAGE_FORMAT. The same hardening blocks ROT-based OOP type-library references.

8.3 The PPL injection chain (flagship PoC)

WaaSRemediationAgent runs inside WaaSMedicSvc as PPL PsProtectedSignerWindows-Light ($cls.AppIDEntry.ServiceProtectionLevel = WindowsLight) — the service family that also gates access to LSASS-level protected processes. The chain ([4]):

  1. Set HKLM\SOFTWARE\Microsoft\.NETFramework\AllowDCOMReflection = 1 (DWORD) — re-enables .NET DCOM reflection disabled since MS14-009 — and OnlyUseLatestCLR = 1 to force CLR v4 (otherwise v2 loads). (Admin is needed for HKLM; per-user HKCU COM/.NET registration can substitute — Forshaw: "the general technique does not [require admin]" given any suitable IDispatch-exposing server.)
  2. TreatAs hijack: point HKLM\SOFTWARE\Classes\CLSID\{0BE35203-8F91-11CE-9DE3-00AA004BB851}\TreatAs (StdFont) at the .NET System.Object COM class.
  3. Create WaaSRemediationAgent OOP → walk IDispatch → stdole → StdFont CreateInstance → the CLR loads inside the PPL and returns System.Object.
  4. GetType → .NET reflection → System.Reflection.Assembly::Load(byte[]) → unsigned code executes inside a PPL-Windows process → instantiate an object in the loaded assembly → run code (LSASS-with-PPL and protected AV/EDR processes are now reachable). Clean up the registry afterwards.
  5. Windows 11 24H2 wrinkle: the 64-bit mscorlib.tlb is a raw TLB and fails the VerifyTrust signing level (STATUS_INVALID_IMAGE_FORMAT); the 32-bit Framework\v4.0.30319\mscorlib.tlb is PE-packed (MZ), so setting the cached signing level (or swapping the TLB registration path to the 32-bit file) makes the PoC work again. Forshaw did not release his PoC (C++; "very similar to my CVE-2014-0257 exploit", [7]).

Figure 7 — Trapped COM objects: IDispatch to StdFont CreateInstance to CLR inside PPL How to read this figure: each arrow crosses a trust boundary into the service process. The left chain (IDispatchITypeInfo → stdole → StdFont) is pure COM plumbing; the right chain (TreatAsSystem.ObjectAssembly.Load) is the .NET payload delivery. The side panel marks where oleaut32 VerifyTrust hardening can break the chain on 24H2.

8.4 Third-party PoCs and detection

  • ComDotNetExploit — Mohamed Fakroud's writeup plus PoC for the PPL injection ([59], [60]).
  • ForsHops — IBM X-Force Red's "Fileless lateral movement with trapped COM objects" and tool: forshops.exe [target] [c:\path\to\assembly] ([57], [58]). Detection guidance (Samir Bousseaden): watch for a TreatAs key appearing on the StdFont CLSID, the AllowDCOMReflection/OnlyUseLatestCLR values, and a CLR load inside the WaaSMedicSvc svchost; YARA strings include the WaaSRemediation CLSID {72566E27-…} and {34050212-8AEB-416D-AB76-1E45521DB615}.
  • Outflank (Kyle Avery, July 2025) used LLM-assisted enumeration to find alternative IDispatch-exposing classes, avoiding the PPL CLR-load limitation entirely ([61]) — the bug class is a class, not one service. Framing for operators: the LPE angle is sandbox/AppContainer escapes and PPL injection rather than plain user→SYSTEM; because Microsoft does not treat PPL as a security boundary, expect these primitives to live long.

9. Other documented COM LPE vectors

  1. UPnP Device Host + Update Orchestrator chain (NCC Group / Fox-IT, November 2019, [63]). CVE-2019-1405 — logical flaw in the UPnP Device Host service's COM objects UPnPContainerManager/UPnPContainerManager64, implementing the undocumented IUPnPContainerManager (IID 6D8FF8D4-730D-11D4-BF42-00B0D0118B56; methods ReferenceContainer, UnReferenceContainer, CreateInstance, CreateInstanceWithProgID, Shutdown) → any local user executes commands as LOCAL SERVICE. Fixed November 2019 by adding Administrators-only access checks to the dangerous methods. CVE-2019-1322 — service misconfiguration: members of the SERVICE group could reconfigure UsoSvc (sc stop UsoSvcsc config UsoSvc binpath= …sc start), which runs as SYSTEM; fixed October 2019 by stripping Full Control from the SERVICE group. Chained: any local user → SYSTEM on Windows 10 18031903. PoC: COMahawk (COMahawk64.exe). The canonical "service exposes a dangerous COM API to everyone" teaching example, and the writeup's methodology section is a ready-made OleViewDotNet walkthrough.
  2. COM Aggregate Marshaler and type-library bugs (Forshaw, May 2017). CVE-2017-0213 (Aggregate Marshaler CStdMarshal::Finish_RemQIAndUnmarshal2 RQI2 unpack bug; the PoC uses BITS SetNotifyInterface → type-library load under impersonation → scriptlet moniker → code execution in BITS as SYSTEM; exploit-db 42020; later entered CISA KEV, [65]) and CVE-2017-0214 (type-library input validation). Pattern: callback interface plus type-library processing equals code execution inside the privileged server.
  3. Session Moniker / interactive-user DCOM (2017). CVE-2017-0100 plus bad-fix CVE-2017-0298 — full treatment in section 6.2; pattern: RunAs=Interactive User + cross-session activation + missing client verification.
  4. WalletService memory corruption over COM — CVE-2020-1362 (Q4n/Haoran Qin + Zhiniang Peng, July 2020, [64]). The WalletService COM server: Wallet::WalletCustomProperty::SetGroup OOB write → SetLabel BSTR pointer write-what-where → fake vtable → redirect a virtual call to dxgi!ATL::CComObject<CDXGIAdapter>::vector deleting destructor, whose LoadLibraryExW uses a writable global path → load an attacker DLL as SYSTEM; chained with CVE-2020-1361 for the heap leak. Medium IL → SYSTEM, verified on build 18363. Pattern: DCOM method parameters → memory corruption in the privileged server — the marshaling layer is the attack surface, not the business logic.
  5. Per-user COM hijack as an LPE vector. User-writable per-user registrations (HKCU\Software\Classes\CLSID\{…}\InprocServer32, TreatAs, AppID paths) shadow machine registrations for classes activated in the user's logon session. Classic uses are UAC bypass and persistence (bohops, [69], [70]; MITRE ATT&CK T1122 Component Object Model Hijacking), but it becomes LPE/PPL-relevant when a higher-privileged in-session process — or an IDispatch trapped-object server — activates the class: Forshaw explicitly notes the per-user variant of the StdFont TreatAs hijack performs the PPL injection without admin (section 8.3). Triage question: "what identity activates user-scope classes on this box?"
  6. .NET COM interop into privileged processes. Any registered .NET COM class loads the CLR (mscoree) into the activating process — including SYSTEM services and PPL processes. Leveraged by CVE-2014-0257 (Forshaw's IE sandbox escape, public PoC, [7]), by the trapped-object chain, and historically by .NET DCOM reflection (System.Type over DCOM → arbitrary reflection; fixed by MS14-009, re-enablable via AllowDCOMReflection; OnlyUseLatestCLR selects CLR v4). Pattern: registry values + TreatAs + a .NET class = unsigned code inside signed processes.
  7. WinRT brokers. WinRT is COM underneath: runtime classes, activation via combase, broker processes crossing IL/AppContainer boundaries. Verified in-scope example: CVE-2019-0555 (XML DOM through WinRT, section 8.1). Brokers are OOP COM servers with sandbox-facing activation permissions — triage them with exactly the section 10 checklist.
  8. DiagHub sibling CVEs. CVE-2018-0952 ([66]) and the case-sensitivity directory-traversal EoP ([67]) — covered with the case study in section 4; "DCOM service with file-manipulation methods = recurring CVE farm".
  9. Adjacent but NOT COM — do not mislabel. Perfusion (itm4n) is the RpcEptMapper registry-key ACL bug (Win7/2008R2/8/2012) — RPC, not COM. FullPowers recovers the LOCAL/NETWORK SERVICE default privileges through the task scheduler — not COM. CVE-2020-1337 is a Print Spooler file-write — RPC. Precision here is not pedantry: it decides which hardening rows of section 6.3 apply, and which fuzzer you point at the target in section 10.

10. Research Lab: Finding New COM LPE Bugs

Everything above was found with the workflow below. The category is not mined out: DiagHub kept producing CVEs for years, the potato family respawns after every mitigation, and the trapped-object class produced a flagship result in January 2025.

10.1 Hunting hypothesis

The classes of new bugs this category still hides:

  • OOP servers whose methods touch attacker-controlled paths without impersonation, or with check-then-revert gaps (bug classes 12) — especially newly shipped inbox services.
  • Service-hosted classes whose AppID grants AppContainer/LPAC activation — a sandbox-escape pipeline identical to DiagHub's original audience.
  • Callback-accepting methods (interface pointers as parameters) where the server calls back with RPC_C_IMP_LEVEL_IMPERSONATE — the DCOMPotato pattern, almost certainly present in unaudited services.
  • IDispatch-exposing service classes beyond the five default-box candidates (alternative trapped-object entries, as Outflank's LLM-assisted enumeration demonstrated, [61]) and type-library trust / TreatAs paths that survive the VerifyTrust signing-level hardening.

10.2 Enumeration

OleViewDotNet (Forshaw, [8]; .NET 4; PowerShell module Install-Module OleViewDotNetPowerShell 5 only, not 7+):

  • GUI: Registry → Local Services (COM objects hosted in services, filterable), Registry → CLSIDs by Server, and per-object View Launch Permissions / View Access Permissions (decoded AppID SDDLs).
  • PowerShell:
Get-ComClass -Service                          # service-hosted classes
Get-ComClass -Clsid $clsid | % Servers         # InProc vs Local servers
$cls = Get-ComClass -Clsid $clsid
$cls.AppIDEntry                                # RunAs, service name, SDDLs
$cls.AppIDEntry.ServiceProtectionLevel         # PPL check (e.g. WindowsLight)
# object walk (New-ComObject, Get-ComInterface, Import-ComTypeLib, Get-ComObjRef): section 8.2

Raw registry: HKLM\SOFTWARE\Classes\CLSID, …\AppID\{guid} values LaunchPermission, AccessPermission, RunAs, Service, ServiceProtectionLevel; machine defaults under HKLM\SOFTWARE\Microsoft\Ole; per-user shadow at HKCU\Software\Classes\CLSID.

Existing abusable-CLSID corpora: the per-OS lists at ohpe.it/juicy-potato/CLSID/ ([33]), the strontic xcyclopedia CLSID library, and the juicy-potato repo README tables ([34]). RPC/RPCSS surface: RpcView (endpoint/interface dump), impacket rpcdump.py, and the Wireshark DCERPC + DCOM (IObjectExporter/IRemUnknown2) dissectors for watching activations on the wire.

10.3 Triage checklist

A target is promising if:

  1. OOP server (LocalServer32 or service-hosted AppID).
  2. Runs as SYSTEM (or LOCAL/NETWORK SERVICE for a hop).
  3. Launch and access ACLs include your principal — Authenticated Users, INTERACTIVE, or AppContainer/LPAC for sandbox escapes.
  4. Methods touch filesystem paths, the registry, process creation, LoadLibrary*, SetNamedSecurityInfo, or token APIs; or accept interface pointers (callback impersonation — the DCOMPotato pattern); or return objects (the trapped-object pattern).
  5. Bonus: IDispatch present (type-library vector), .NET interop, or RunAs=Interactive User.

10.4 Static RE checklist (Forshaw's DiagHub recipe, generalized)

  1. OleViewDotNet: create an instance → Marshal ⇒ View Properties → capture PID + IPID + OXID + string bindings + authentication services.
  2. OVDN process view (needs admin and configured symbols: dbghelp from WinDbg plus srv*https://msdl.microsoft.com/download/symbols) → IPID entry → interface pointer + server-side VTable address (e.g. DiagnosticsHub_StandardCollector_Runtime+0x36C78).
  3. WinDbg attach: dqs <module>+<offset> L<nMethods> → symbolized method names (public symbols exist on Microsoft components).
  4. OVDN View Proxy Definition → decompiles the NDR stubless bytecode → method signatures; the Structures tab → marshaled struct layouts → rebuild the interface in C++/C#.
  5. In IDA/Ghidra: xref command strings (itm4n's "StartScan" technique, section 5.3), map _guard_dispatch_icall_fptr indirect calls to VTable slots via dynamic breakpoints, and match client proxy slots (ObjectStublessClientN) to server symbols.
  6. Hunt the failure modes of section 3: missing or late CoImpersonateClient/RpcImpersonateClient, early RevertToSelf, identify-level acceptance, device-map/symlink handling under impersonation, RunAs mismatches, type-library/TLB trust, TreatAs, unmarshaled-object safety.

10.5 Dynamic harness

Minimal C++ client — the same shape as the USO reconstruction (section 5.2), generalized (illustrative skeleton):

CoInitializeEx(0, COINIT_MULTITHREADED);
IUnknown* pUnk = nullptr;
CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pUnk));
// Offer impersonation — the contract from section 2:
CoSetProxyBlanket(pUnk, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
                  COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_DEFAULT,
                  RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE);
// QueryInterface to the target IID, then invoke methods with controlled inputs:
// path arguments (junctions, symlinks, device-map paths, odd extensions) and
// interface-pointer arguments (your own fake IUnknown/IMarshal implementations)
CoUninitialize();

Observe the server side with Process Monitor (filter: server PID; watch file/registry/LoadImage operations executing as SYSTEM), API Monitor, and WinDbg breakpoints on CoImpersonateClient, RpcImpersonateClient, CreateFileW, RegCreateKeyExW, LoadLibraryExW, SetNamedSecurityInfo. ETW providers Microsoft-Windows-RPC and COM give call tracing. Use TokenViewer (Forshaw) to inspect any captured token — impersonation vs identification is the number-one source of "the exploit didn't work" confusion (error 1346, section 2).

10.6 Fuzzing marshal paths

  • Mutate marshaled NDR buffers against the NDR interpreter stubs — the exact layer OVDN decompiles in step 10.4.4. Watch for OOB behavior in server-side unmarshalling (the WalletService class, CVE-2020-1362) and for marshal-by-reference object leaks (section 8).
  • Prior art: COMRace (USENIX Security '22 — data-race detection in COM objects, [71]); Black Hat EU 2024 "Enhancing Automatic Vulnerability Discovery for Windows RPC/COM in New Ways" (harness generation, [72]). Poor-man's fuzzer: interface brute-calling via the OVDN PowerShell module (New-ComObject + method invocation with mutated arguments), and RPC-aware fuzzing driven from IDA-generated clients.

10.7 Patch diffing

BinDiff/Ghidra the usual binaries: rpcss.dll, combase.dll, ole32.dll, oleaut32.dll, usosvc.dll, usocoreworker, and the DiagHub runtime. Proven patterns to look for:

  • New ACL checks inside methods (the CVE-2019-1405 fix).
  • Service security-descriptor changes (the CVE-2019-1322 fix).
  • Silent binding-builder changes (the 1809 MakeBindingCreateRemoteBindingToOR rewrite).
  • Image-load and signing trust: ProcessImageLoadPolicy (19H1); signing-level enforcement (VerifyTrust in oleaut32).
  • NTLM package changes (SsprHandleChallengeMessage, CVE-2023-21746 — diff msv1_0.dll).

A silent diff in any of these is a map of what the previous technique did — and usually of what a variant must now avoid.

10.8 From crash or lead to primitive

  • A missing-impersonation lead becomes a primitive when you can (a) control the resource path and (b) name a target file the server will touch — prove it with ProcMon showing a SYSTEM-integrity CreateFile on your path.
  • A captured token becomes a primitive only at RPC_C_IMP_LEVEL_IMPERSONATE; check it in TokenViewer before writing any process-spawning code, and expect error 1346 otherwise.
  • A marshaling crash becomes a primitive when you control the overwritten offset — the WalletService writeup ([64]) is the model for turning DCOM-parameter corruption into a vtable redirect.
  • A trapped object becomes a primitive when Get-ComObjRef proves it lives in the privileged process and you can reach a CreateInstance-capable type info (section 8.2).

10.9 Further reading

  • Windows Bug Class: Accessing Trapped COM Objects with IDispatch ([4])
  • Weaponizing Privileged File Writes with the USO Service, parts 12 ([9], [10])
  • CVE-2019-1405 and CVE-2019-1322 — Elevation to SYSTEM via UPnP Device Host and Update Orchestrator ([63])
  • DCOM Turns 30: Revisiting a Legacy Interface in the Modern Threatscape, hack.lu 2025 slides ([73])

11. References

Project Zero / Forshaw (primary sources)

itm4n

decoder.cloud / splinter_code / antonioCoco

FoxGlove Security / ohpe

CVE-2015-2370 ecosystem and CVE-2017-0100

Other potatoes and coercion tooling

Trapped COM object ecosystem

Other COM LPE CVEs, privileged-file-operation abuse, and fuzzing

Microsoft documentation (impersonation contract and hardening)