Post

CVE-2026-20817 - Windows Error Reporting Service EoP

This vulnerability was such a gaping hole in the Windows Error Reporting service that Microsoft completely removed the affected feature. A low privilege user could simply send a specially crafted ALPC message with a reference to a command line that the service executed with SYSTEM privileges. At least that’s what I thought initially.

MSRC Vulnerability Summary

CVE-2026-20817 is local privilege escalation vulnerability caused by an “Improper Handling of Insufficient Permissions or Privileges”, according to its CWE classification, in the Windows Error Reporting service. Its discovery is credited to Denis Faiustov and Ruslan Sayfiev, with GMO Cybersecurity.

High-level description of CVE-2026-20817 High-level description of CVE-2026-20817

According to the FAQ section, “an attacker who successfully exploited this vulnerability could gain SYSTEM privileges”, and its exploitation is deemed “more likely”.

Patch Analysis

The Windows Error Reporting service is a common target, so we already know that we should start our analysis with a “binary diffing” of WerSvc.dll, which is the main DLL implementing the service. For reference, I compared the version 10.0.26100.7309 (before the patch) with the version 10.0.26100.7623 (containing the patch). As usual, you can find their download links easily thanks to Winbindex.

Binary diffing of `WerSvc.dll` Binary diffing of WerSvc.dll

The analysis is straightforward. The only function that appeared to have undergone changes is SvcElevatedLaunch, which is a rather unequivocal name.

Usually, I find it hard to understand a patch right from the graph view, but this time, it offered an immediate overview of the fix. As we can see on the annotated screenshot below, two code blocks (in gray) and a “feature test” were added. It shows that, if __private_IsEnabled() (i.e. the patch) is enabled, the function returns immediately with the error code 0x80004005 (E_FAIL).

Binary diffing graph Binary diffing graph

In other words, the patch completely removes the vulnerable feature. Now, we have two things to figure out. First, what is the vulnerability, and second, how to reach the vulnerable code.

The vulnerability

The prototype of SvcElevatedLaunch generated by Ghidra, based on the DLL’s PDB file published by Microsoft, is as follows. I just edited the parameter names.

1
2
3
4
5
long CWerService::SvcElevatedLaunch(
    CWerService *this,
    _WERSVC_MSG *p_msg_in, // Pointer to input ALPC message
    _WERSVC_MSG *p_msg_out // Pointer to output ALPC message
)

Although the symbol _WERSVC_MSG is public, the structure is not documented publicly. However, a quick analysis shows that it is an extension of the known _PORT_MESSAGE structure, which acts as a sort of common header for ALPC messages.

For a deep dive into ALPC, I’d recommend 0xcsandker’s blog post Offensive Windows IPC Internals 3: ALPC.

The SvcElevatedLaunch method does two things. First, it opens the ALPC client’s process. Second, it invokes the internal function ElevatedProcessStart, passing a reference to a File Mapping object opened by the ALPC client. Again, it should be noted that all the parameter names, and the content of the _WERSVC_MSG structure, were edited by me as a result of a few hours of reverse engineering the internal functions down the call tree.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Open the ALPC client's process
dwProcAccessRights = PROCESS_CREATE_PROCESS | PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION;
OpenSenderProcessThread(
    this, // CWerService class
    p_msg_in, // Pointer to input ALPC message
    dwProcAccessRights, // Client process access rights
    &hClientProcessHandle, // Pointer to output client process handle
    0, // Client thread access rights, not user here
    NULL, // Pointer to output client thread handle, not used here
    0 
);

// Start elevated process
ElevatedProcessStart(
    p_msg_in->Unknown, hClientProcessHandle, p_msg_in->FileMapping,
    pClientHandleListCopy, uVar3, &hNewProcessHandle
);

// Return result
p_msg_out->NewProcessHandle = hNewProcessHandle;
p_msg_out->MessageFlags = 0x50000001;

The internal function ElevatedProcessStart performs the following steps.

  1. It duplicates the client’s File Mapping handle into the current process with the Win32 API DuplicateHandle.
  2. It maps a view of the client’s File Mapping object with the Win32 API MapViewOfFile.
  3. It reads the content of the mapped file into a local (wide) string buffer.
  4. If calls the internal function CreateElevatedProcessAsUser, with this input string as a parameter.
  5. If the previous call succeeds, the output process handle is copied to the client’s process with the Win32 API DuplicateHandle.

As for CreateElevatedProcessAsUser, it does the following.

  1. It creates a SYSTEM Token (or an elevated Token on behalf of the current user, if it’s an admin under UAC).
  2. It constructs a WerFault.exe command line.
  3. It invokes the Win32 API CreateProcessAsUserW, with the command line constructed previously.

The WerFault.exe command is as follows. The absolute path of WerFault.exe is constructed securely using the GetSystemDirectoryW API, and is passed as the lpApplicationName parameter of CreateProcessAsUserW. However, the ALPC client has full control over the command line options, passed to the service through its File Mapping buffer.

1
C:\Windows\System32\WerFault.exe [USER CONTROLLED OPTIONS HERE]

So, by sending a specially crafted ALPC message, a client is able to coerce the Windows Error Reporting service to start WerFault.exe as SYSTEM, with controllable command line options. All of a sudden, this vulnerability sounds a lot less exciting because the client doesn’t control the program that is executed.

Reaching the Vulnerable Code

According to its incoming call tree, SvcElevatedLaunch is reached from an (A)LPC thread which invokes a generic handler named _ProcessRequest. This handler eventually calls DispatchPortRequestWorkItem.

Call tree leading to `SvcElevatedLaunch` Call tree leading to SvcElevatedLaunch

As its name suggests, DispatchPortRequestWorkItem is a dispatcher which contains a huge switch statement. In our case, 0x50000000 is the code that triggers the call to SvcElevatedLaunch.

Extract of `DispatchPortRequestWorkItem` Extract of DispatchPortRequestWorkItem

By going even further up in the call tree, we can see that WerSvc creates an (A)LPC server when it starts. The name of the port is \WindowsErrorReportingServicePort.

The call to NtAlpcCreatePort appears further down, and is not represented on the screenshot below.

Extract of `_StartLpcServer` Extract of _StartLpcServer

Interestingly enough, this ALPC server has already been the object of offensive security research in the past. Most notably, I found an analysis of the SilentProcessExit feature, by Hexacorn. Searching a bit further for online resources also lead me to the (re)discovery of the “LSASS Shtinkering” technique (notably this implementation), which contains some sample code to send and receive ALPC messages.

Sending an ALPC message is rather simple. You first need to invoke NtAlpcConnectPort to connect to an ALPC port, and then call NtAlpcSendWaitReceivePort to send your message and receive the response from the server. The real challenge is to figure out the format of the message, and make sure you set all the important values inside that structure are set appropriately. I already alluded to it in the previous part. I’ll spare you the tedious reverse engineering part and give you what I found directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct _WERSVC_MSG_ELEVATED_LAUNCH
{
    // Offest: 0x00
    PORT_MESSAGE PortMessage;
    // Offest: 0x28 (40)
    DWORD MessageFlags; // p_msg_out + 0x28 (40) -> Message type + error code
                        // on return. (DWORD)
    // Offset: 0x2c (44)
    DWORD LastError;    // p_msg_out + 0x2c (44) -> Last error code returned by
                        // "ElevatedProcessStart". (DWORD)
    // Offset: 0x30 (48)
    BOOL Unknown;       // p_msg_in + 0x30 (48) -> unknown value, but must be "1"
                        // according to if condition in "ElevatedProcessStart". (DWORD)
    // Offset: 0x38 (56)
    HANDLE FileMapping; // p_msg_in + 0x38 (56) -> A File Mapping Object, containing
                        // options to pass to "WerMgr.exe". (HANDLE)
    // Offset: 0x40 (64)
    HANDLE SourceHandles[16]; // p_msg_in + 0x40 (64) -> An array of client handles
                              // to be duplicated into the new process. (HANDLE[16])
    // Offset: 0xc0 (192)
    BOOL CopySourceHandles; // p_msg_in + 0xc0 (192) -> A boolean determining access
                            // rights for opening client process?
    // Offset: 0xc8 (200)
    HANDLE NewProcessHandle; // p_msg_out + 0xc8 (200) -> Receives the handle of the
                             // created process. (HANDLE)
    // Offset: 0xd0 (208)
    BYTE Padding[1192]; // Total structure size must be 0x578, this is checked in
                        // "CWerService::CheckIfValidPortMessage".
} WERSVC_MSG_ELEVATED_LAUNCH, * PWERSVC_MSG_ELEVATED_LAUNCH;

As mentioned previously, the structure starts with a PORT_MESSAGE header. Then, the MessageFlags attribute must contain the value 0x50000000 if we want to reach SvcElevatedLaunch. The second most important attribute is FileMapping. It contains a handle to a File Mapping object, which acts as shared memory to transfer the arguments used in the WerFault.exe command line to the WER service. Optionally, the client can also pass up to 16 handles that will be duplicated into the created process.

Proof-of-Concept

Following this analysis, I wrote a proof-of-concept which you can find on my GitHub. It only triggers the WerFault.exe command line execution as SYSTEM with a user-controlled buffer containing the command line arguments. It does not try to actually gain arbitrary code execution. Further work is required to achieve this result.

While working on this research, I came across a fake PoC on GitHub here: https://github.com/oxfemale/CVE-2026-20817. Although the Visual Studio solution doesn’t seem malicious at first glance, the code doesn’t make much sense. Nonetheless, this is a good reminder we should all be extra cautious with what we download from online repositories.

Proof-of-Concept for CVE-2026-20817 Proof-of-Concept for CVE-2026-20817

A couple of things are important to note about this PoC:

  • the WER service must be running;
  • it is assumed that the ALPC port name is \WindowsErrorReportingServicePort;
  • Defender detects a malicious behavior.

Obviously, the WER service must be running before attempting to run the PoC, otherwise the ALPC server won’t be listening. However, this service is usually not running, and is triggered only when required. Although, low privilege users cannot start it manually, it has a start trigger that can most likely be used for that purpose. I haven’t looked into this particular aspect yet.

By default, the port name \WindowsErrorReportingServicePort is used to start the ALPC server, but a custom one can be defined in the registry key HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting (ErrorPort value of type REG_SZ). This PoC assumes that the default port name is used.

The last thing to note is that the WerFault.exe process is created by the WER service as a child process of the client’s process. To do so, it implements a succession of API calls to spoof the client’s PID and set it as a parent of the new process. Because this technique is widely abused by malware, Defender flags it as a suspicious behavior and raises an alert.

Suspicious behavior detected Suspicious behavior detected

Conclusion

Although Microsoft do not require a fully functional proof-of-concept to take action, meaning that they don’t ask you to absolutely pop a SYSTEM shell, it is likely that this primitive was abused by the original researchers to do so.

More concretely, I suspect that a clever combination of WerFault.exe command line options can be used in conjunction with some Windows internals trickery, such as shown in the last part of the blog post A Deep Dive into the GetProcessHandleFromHwnd API recently published by James Forshaw.

To be continued?…

This post is licensed under CC BY 4.0 by the author.