skip to content
David Carliez
Table of Contents

How a writable BRLTTY parameter in Windows Narrator’s optional Braille stack becomes service-account code execution and an interactive SYSTEM shell.

TL;DR

CVE-2026-58635 is a local privilege-escalation vulnerability in the optional Windows Narrator Braille stack. On a vulnerable system, an ordinary authenticated user can connect to that service over its local endpoint without authentication and modify global BrlAPI parameter 29, BRLAPI_PARAM_LITERARY_BRAILLE_TABLE.

That parameter is supposed to select a literary Braille contraction table. BRLTTY 6.4.1 also accepts an executable path in this code path: when the supplied path has an extension recognized through Windows PATHEXT, BRLTTY treats it as an external contraction program and starts it under the service account.

The resulting primitive is:

standard user
→ local BrlAPI connection (localhost:0, auth=none)
→ global write to parameter 29
→ external contraction-program handling
→ arbitrary process as the BRLTTY service account

The service runs as NT AUTHORITY\LOCAL SERVICE (S-1-5-19) and the resulting token has SeImpersonatePrivilege. For the end-to-end demonstration, I used SigmaPotato v1.2.6 to obtain a SYSTEM token, then moved a command shell into the active desktop session.

Microsoft fixed the issue in the July 2026 security updates. The vulnerability is rated Important, has a CVSS 3.1 score of 7.8, and is mapped to CWE-77: Improper Neutralization of Special Elements used in a Command.

Original vulnerability credit: Microsoft’s advisory identifies yhw & txz as the researchers who reported CVE-2026-58635. This post covers my independent exploit development and validation of the published issue, not the original discovery.

An unusual Windows attack surface

The first important detail is that the vulnerable component is not present on every default Windows installation. It arrives with the optional Narrator Braille capability:

Accessibility.Braille~~~~0.0.1.0

Once installed, it provides, among other components:

%SystemRoot%\System32\brlapi.dll
%SystemRoot%\brltty\bin\brltty.exe
BrlAPI service

BRLTTY is an accessibility project that provides access to refreshable Braille displays. BrlAPI is its client interface. Applications can use it to exchange data with the Braille service without each application needing to implement device-specific protocols.

That architecture creates a trust boundary worth auditing: unprivileged clients send requests to a long-lived service, and some requests affect global runtime configuration. The dangerous question is not merely whether a parameter is writable, but what the privileged process eventually does with the supplied value.

Finding the primitive: parameter 29

BrlAPI exposes a parameter interface for reading and updating service state. Parameter 29 is the literary Braille table:

constexpr int kLiteraryBrailleTableParameter = 29;
constexpr unsigned int kGlobalParameterFlag = 1;

On the vulnerable BRLTTY 6.4.1 build, the parameter is both global and writable. The proof of concept dynamically resolves the required client exports from the system copy of brlapi.dll:

brlapi_openConnection
brlapi_setParameter
brlapi_closeConnection

It then asks for the local endpoint with no authentication:

ConnectionSettings requested = {"none", "localhost:0"};
const int descriptor = open_connection(&requested, &actual);

The final write is almost anticlimactic:

set_parameter(
BRLAPI_PARAM_LITERARY_BRAILLE_TABLE,
0,
BRLAPI_PARAMF_GLOBAL,
payload_path,
payload_path_length);

The important behavior happens after this call returns. BRLTTY passes the supplied string into the contraction-table loader. A normal value names a Braille table. On Windows, however, a path with an executable extension present in PATHEXT is interpreted as an external contraction program and passed to the host-command launcher.

A configuration string has crossed the boundary into process creation.

The Windows path-handling trap

My first trigger was logically correct and still failed:

setParameter(29, GLOBAL, C:\Users\Public\localservice_proof.exe) = -1
BrlAPI error: Invalid parameter

That failure was useful. It showed that the server was receiving and rejecting the value rather than the client failing to connect. The next step was to follow the exact Windows/MinGW path-processing branch used by BRLTTY instead of assuming that a conventional Win32 path would work everywhere.

The successful trigger resolves the payload to an absolute Windows path and converts backslashes to forward slashes:

std::replace(path.begin(), path.end(), L'\\', L'/');

Thus:

C:\lab\bin\localservice_stage.exe

becomes:

C:/lab/bin/localservice_stage.exe

This is what allows the value to reach the external-program branch in the tested BRLTTY/MinGW build. With the normalized path, the global parameter write returns success and BRLTTY starts the supplied executable.

Proving the service-account boundary

Before attempting any further escalation, the payload does only two things:

  1. records its username, SID, and PID; and
  2. captures whoami /all to show token groups and privileges.

On the tested Windows 11 25H2 build, the result was:

USER=LOCAL SERVICE
SID=S-1-5-19

This established the vulnerability’s concrete primitive without relying on parent-process assumptions or service configuration alone. The service launched attacker-selected code, and the code independently inspected its own token.

There is a noteworthy discrepancy to preserve. Microsoft’s FAQ says successful exploitation can execute code as NT AUTHORITY\NETWORK SERVICE. My tested service registration and runtime token showed NT AUTHORITY\LOCAL SERVICE. Service identity may differ by branch, packaging state, or configuration.

Either way, the important boundary is the same: a standard user can cause code to execute under a more privileged Windows service identity.

From LocalService to SYSTEM

The CVE-specific exploit is complete once the attacker controls code under the BRLTTY service account. A second question is whether that account can be converted into a full SYSTEM token on the target configuration.

In my lab, the LocalService token included:

SeImpersonatePrivilege

That privilege makes the process eligible for well-known token-impersonation techniques. Rather than hide that conversion inside the CVE trigger, the proof of concept keeps it as an explicit optional stage:

localservice_stage.exe
→ records the CVE-launched token
→ starts pinned SigmaPotato v1.2.6
→ SigmaPotato obtains a SYSTEM token
→ system_shell_launcher.exe starts an interactive shell

SigmaPotato is a third-party exploitation component. It is not part of CVE-2026-58635 and is not required to prove the command-injection vulnerability. Its purpose here is to demonstrate the shortest practical path from the observed LocalService token to SYSTEM on the tested machine.

Making the shell visible

A SYSTEM process created by a service normally remains in Session 0. That proves code execution but does not produce a useful interactive desktop shell.

The final launcher therefore:

  1. obtains the active console session with WTSGetActiveConsoleSessionId;
  2. duplicates its current SYSTEM token as a primary token;
  3. changes the duplicate’s TokenSessionId to the active session;
  4. selects winsta0\default; and
  5. calls CreateProcessAsUserW for cmd.exe.

On success, Windows opens a visible cmd prompt:

and:

Terminal window
whoami

returns:

nt authority\system

This final stage is operational plumbing, not a new vulnerability. Separating it from the BrlAPI trigger keeps the claims auditable:

StageSecurity claim
BrlAPI parameter 29 writeUnprivileged control of a global service parameter
External contraction programArbitrary service-account process execution
Token inspectionObserved LocalService identity and privileges
SigmaPotatoEnvironment-dependent conversion to SYSTEM
Session launcherPlaces the already-obtained SYSTEM token on the active desktop

The complete exploit chain

Putting the pieces together:

┌──────────────────────────────────────────────────────────────┐
│ Standard user │
│ │
│ Load %SystemRoot%\System32\brlapi.dll │
│ Connect to localhost:0 with auth=none │
└──────────────────────────────┬───────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ BrlAPI global parameter write │
│ │
│ Parameter 29 = C:/.../localservice_stage.exe │
└──────────────────────────────┬───────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Vulnerable BRLTTY contraction-table handling │
│ │
│ Executable extension found through PATHEXT │
│ Value treated as external contraction program │
└──────────────────────────────┬───────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Service-account code execution │
│ │
│ Tested result: NT AUTHORITY\LOCAL SERVICE, S-1-5-19 │
│ Tested token: SeImpersonatePrivilege enabled │
└──────────────────────────────┬───────────────────────────────┘
│ optional conversion
┌──────────────────────────────────────────────────────────────┐
│ SYSTEM token → active-session command shell │
└──────────────────────────────────────────────────────────────┘

The exploit does not require an administrator account, a crafted Braille device, a malicious driver, or interaction with Narrator’s UI. It needs the vulnerable optional capability to be installed and the local user to be able to reach the BrlAPI service surface.

Detection opportunities

The exploit crosses several boundaries that defenders can monitor even if no single event is definitive by itself:

  • installation or unexpected presence of the optional Accessibility.Braille capability;
  • startup of the BrlAPI/BRLTTY service on systems where Braille support is not normally used;
  • local BrlAPI clients connecting with auth=none;
  • writes to global literary-table parameter 29;
  • brltty.exe or the BrlAPI service launching an executable from a user-controlled directory;
  • a LocalService child process starting token-impersonation tooling; and
  • a service-context process creating an interactive console in a user session.

Process ancestry is particularly useful. A contraction-table selection should not normally result in BRLTTY launching an arbitrary executable from a source tree, download directory, or temporary workspace.

The public PoC uses obvious filenames and produces explicit result files. Real exploitation would not need to be so cooperative, so detections should focus on the trust transition rather than repository-specific names alone.

References