
Executive Summary
CVE-2026-49176 represents a critical local privilege-escalation vulnerability affecting Windows WalletService, a LocalSystem-hosted service that manages wallet operations through the public Windows.ApplicationModel.Wallet WinRT API. The vulnerability emerges from a trust boundary violation during service initialization: WalletService obtains the caller’s token to resolve their Documents known folder, then reverts to LocalSystem before opening the resulting path. An attacker can exploit this behavior by pre-seeding a malicious Extensible Storage Engine (ESE) database with persisted callbacks, redirecting their Documents folder to contain this payload, and triggering WalletService initialization through the public API.
The resulting code execution occurs within the SYSTEM service context and leverages ESE’s callback mechanism to load attacker-controlled code. By combining this with a temporary service broker, an attacker can execute arbitrary code as NT AUTHORITY\SYSTEM in the interactive session with no residual artifacts. Microsoft addressed this vulnerability in build 26200.8875 by disabling ESE operations and restricting folder creation during initialization. Credits are due to Chen Le Qi (@cplearns2h4ck) and Mochi Nishimiya (@MochiNishimiya) from STAR Labs SG. The vulnerability carries a CVSS 3.1 score of 7.8 (Important) and maps to CWE-269 (Improper Privilege Management) and CWE-59 (Improper Link Resolution Before File Access).
TL;DR
The exploitation sequence unfolds as follows:
standard user -> create an ESE wallet.db containing a persisted callback
-> redirect the user's Documents known folder to the seeded parent
-> call WalletManager.RequestStoreAsync and GetItemsAsync
-> WalletService opens \Wallet\wallet.db as LocalSystem
-> ESE opens the mandatory Cards table
-> ESE resolves the persisted callback as DLL!Export
-> attacker DLL loads inside the SYSTEM service host
-> SYSTEM token is moved into the active session
-> interactive cmd.exe as NT AUTHORITY\SYSTEM
The WalletService Attack Surface
WalletService operates as the backing service for the Windows.ApplicationModel.Wallet WinRT API, making it accessible to any standard user. The service activation chain proceeds through several distinct stages: WalletManager.RequestStoreAsync() leads to WalletItemStore.GetItemsAsync(), which triggers WalletService activation, wallet item manager initialization, and ultimately calls WalletDatabaseESE::Open. This call path is entirely public and requires no elevated privileges to invoke.
During initialization, WalletService performs a critical sequence of operations. The service first obtains the caller’s token via CoImpersonateClient, uses this token to retrieve the caller’s FOLDERID_Documents path through SHGetKnownFolderPath, appends a fixed \Wallet\ component, then reverts to its own LocalSystem identity before opening the resulting path. This creates a trust boundary crossing: the service uses caller-controlled identity state to select a pathname, then crosses back into LocalSystem before determining what object exists at that pathname.
What the July Patch Changed
Microsoft’s patch, identified as Feature_Servicing_WalletServiceRedirectionGuardinternal (ID: 3305877819), made targeted changes to five functions within WalletService:
| Function | Fixed behavior |
|---|---|
WalletDatabaseESE::Open | Returns before opening the ESE database |
ServerUtils::HideAndRestrictFolder | Skips the legacy folder restriction |
RestrictFolder | Skips replacement of the directory DACL |
FileUtils::DeleteFiles | Skips legacy cleanup deletion |
WalletDatabaseESE::HandleDatabaseCorruption | Skips corruption cleanup |
The behavioral differential between vulnerable and patched builds is pronounced. Vulnerable build 26200.8737 creates a complete directory structure under containing ESE files (edb, log files, journal files, wallet.db, wallet.jfm). Fixed build 26200.8875 creates only an empty Wallet directory with inherited access permissions and performs no ESE operations.
Proving the Filesystem Primitive First
Research methodology began with a simple test: redirecting a standard user’s Documents folder to a disposable parent directory with limited write access, then triggering WalletService initialization. On vulnerable build 26200.8737, the following structure was created:
\Wallet\
edb.chkedb.logedbres00001.jrsedbres00002.jrsedbtmp.logwallet.dbwallet.jfm
On fixed build 26200.8875, only an empty Wallet directory appeared with inherited access. This single test established three critical facts: first, the public API is reachable from a standard-user token; second, the caller controls the parent directory used by privileged initialization; and third, vulnerable and patched builds diverged precisely at the legacy ESE and restriction code paths.
Dead Ends That Narrowed the Search
Several exploitation approaches were explored and eliminated during research. The service always appends a fixed “Wallet” directory component, defeating arbitrary file-write primitives. WalletDatabaseESE validates the resolved path against the supplied path, blocking whole-directory junction attacks. Wallet image properties use random GUID basenames with forced .png suffixes and offer no automatic execution primitive.
ESE Persisted Callbacks
Extensible Storage Engine supports a sophisticated mechanism called user-defined-default callbacks. These callbacks can be stored in the database schema as column specifications: a column stores a callback reference in the format C:\path\callback.dll!ExportName. When persisted callbacks are enabled and the relevant table is opened, ESE resolves and loads the callback module. This feature was designed for trusted databases; it becomes an execution primitive when a privileged process accepts a database created by a lower-privileged user.
The pre-seeding code pattern uses the following approach:
column.coltyp = JET_coltypLong;
column.cbMax = sizeof(long);
column.grbit = JET_bitColumnTagged | JET_bitColumnUserDefinedDefault;
user_default.szCallback = callback_spec;
JetAddColumnA(
session,
table_id,
"Computed",
&column,
&user_default,
sizeof(user_default),
&column_id);
On the service side, the critical condition that enables execution is:
JET_paramEnablePersistedCallbacks = 1
Building the Attacker-Seeded Wallet Database
Creating a seeded wallet.db requires five sequential operations. First, establish a private ESE instance under the standard user’s writable directory. Second, create the wallet.db file. Third, create a table named Cards (mandatory for WalletService). Fourth, add a user-defined-default column containing the callback path and export name. Fifth, shut down ESE cleanly so WalletService can consume the resulting database without corruption detection.
The resulting directory structure appears as:
%LOCALAPPDATA%\CVE-2026-49176-SHELL\\
Wallet\
wallet.db
payload\
wallet_callback_shell.dll
shell_broker.exe
result.txt
The callback string reference takes the form:
\wallet_callback_shell.dll!WalletCallback
Triggering WalletService from a Standard User
Activation is straightforward via WinRT from PowerShell:
$store = Await-WinRtOperation `
([Windows.ApplicationModel.Wallet.WalletManager]::RequestStoreAsync()) `
([Windows.ApplicationModel.Wallet.WalletItemStore])
$operation = $store.GetItemsAsync()
GetItemsAsync() suffices because WalletService initialization opens the database and the mandatory Cards table before normal item enumeration completes. The original Documents location is restored in a finally block, independent of exploitation success.
Proving the SYSTEM Boundary
Initial callback proof-of-concept output confirmed:
event=DLL_PROCESS_ATTACH
pid=6232
user=SYSTEM
image=C:\Windows\System32\svchost.exe
The proof file was owned by NT AUTHORITY\SYSTEM; direct write attempts from the low account to the same directory failed with access denied. Testing confirmed vulnerable component identity on Windows 11 Pro 25H2 build 26200.8737 (WalletService.dll SHA-256: 51A15901B445F0F58D0930535B2136BB567E56709291CA758F5A8265D66F4D12). The fixed comparison (build 26200.8875, SHA-256: B9B1B38E9036F9B55634BCD8181E779EBFC599587DB4B374BF33FBE124D04880) created only an empty Wallet directory with no callback execution.
Making the Shell Visible
Service-host context requires additional delivery mechanics. The callback payload performs the following actions:
1. Verify token is WinLocalSystemSid
2. Create temporary service pointing to shell_broker.exe
3. Start service
4. Delete service registration
The shell broker then executes:
1. Find active interactive session via WTSGetActiveConsoleSessionId and WTSEnumerateSessionsW
2. Duplicate SYSTEM token as primary token
3. Change TokenSessionId to active session
4. Select winsta0\default
5. Call CreateProcessAsUserW for cmd.exe
The result: cmd.exe in session 1 as NT AUTHORITY\SYSTEM with zero residual service registration. The following table outlines the security boundary crossings at each stage:
| Stage | Security claim |
|---|---|
| Caller-controlled Documents mapping | User controls parent used by WalletService |
| Pre-seeded wallet.db | User controls ESE schema consumed by service |
| Persisted callback resolution | ESE loads attacker-selected DLL in SYSTEM host |
| Callback identity check | Confirms actual LocalSystem code execution |
| Temporary broker service | Obtains clean service token for desktop delivery |
| Session-aware CreateProcessAsUserW | Produces visible SYSTEM command shell |
The Complete Exploit Chain
The full exploitation chain flows as follows:
┌──────────────────────────────────────────────────────────────┐
│ Standard user │
│ Create Wallet\wallet.db with a Cards table │
│ Persist callback = C:\...\callback.dll!WalletCallback │
│ Redirect FOLDERID_Documents to the seeded parent │
└──────────────────────────────┬───────────────────────────────┘
│ public Wallet WinRT API
▼
┌──────────────────────────────────────────────────────────────┐
│ WalletService, LocalSystem │
│ Resolve caller Documents path │
│ Revert impersonation │
│ Enable ESE persisted callbacks │
│ Open attacker-created wallet.db and Cards │
└──────────────────────────────┬───────────────────────────────┘
│ ESE resolves DLL!Export
▼
┌──────────────────────────────────────────────────────────────┐
│ Attacker callback DLL inside SYSTEM svchost │
│ Verify WinLocalSystemSid │
│ Start temporary shell broker service │
└──────────────────────────────┬───────────────────────────────┘
│ duplicate token and set session
▼
┌──────────────────────────────────────────────────────────────┐
│ Interactive cmd.exe in the active session │
│ Owner: NT AUTHORITY\SYSTEM │
└──────────────────────────────────────────────────────────────┘
Detection Opportunities
Observable boundaries exist throughout the exploit chain. Detection can focus on: non-administrator processes changing the per-user Documents known-folder mapping before WalletService activation; WalletService opening Wallet\wallet.db beneath unusual user-selected parents; WalletService-hosting svchost.exe loading DLLs from user-writable profile or application-data paths; SYSTEM processes creating and rapidly deleting short-lived services with binaries in user profiles; SYSTEM processes changing primary token session IDs and starting cmd.exe on winsta0\default; and wallet databases that predate service initialization rather than being created by WalletService itself.
The highest-signal detection event is the module load. WalletService has no normal operational reason to load a DLL from a random user’s LocalAppData tree while processing Wallet data. Defensive inventory techniques should compare the user’s current FOLDERID_Documents against the normal profile location, then check whether Wallet\wallet.db predates the service’s first use.
Key Takeaways
- CVE-2026-49176 exploits a trust boundary crossing in WalletService’s known-folder resolution logic, where caller identity is used to select a pathname before privilege boundary restoration.
- ESE persisted callbacks provide a direct code-execution primitive when untrusted databases are opened with
JET_paramEnablePersistedCallbacks = 1. - Temporary service creation and token session manipulation enable interactive SYSTEM shell delivery with minimal residual artifacts.
- Known-folder redirection is a powerful primitive for path injection attacks against services that resolve user identity before crossing privilege boundaries.
- The exploit is entirely user-mode and requires no kernel vulnerability, driver, or exploit mitigation bypass.
- CVSS 7.8 (Important) reflects the privileged-to-system escalation path; detection is enabled through DLL load monitoring and known-folder state tracking.
- Microsoft’s patch completely disables ESE operations during initialization, eliminating the callback execution primitive entirely.
Defensive Recommendations
- Immediate patching: Deploy build 26200.8875 or later to all Windows 11 25H2 systems to close the ESE persisted callback execution path.
- Known-folder monitoring: Alert on non-administrator processes that modify FOLDERID_Documents or other per-user registry hives before privileged service activation.
- Module-load telemetry: Enable Process Monitoring ETW and log all module loads within svchost.exe; flag loads from user %LOCALAPPDATA% and %APPDATA% paths.
- Service creation auditing: Enable Windows Security Event ID 4697 (A service was installed in the system) and correlate rapid creation/deletion patterns with SYSTEM token holders.
- Database integrity checks: For services consuming untrusted databases, verify creation timestamps against service initialization logs; alert on pre-existing wallet.db files in unexpected locations.
- Privilege boundary enforcement: Code review services that cross privilege boundaries; ensure caller identity is used only for authorization decisions, never for pathname selection ahead of privilege restoration.
- ESE callback hardening: Disable
JET_paramEnablePersistedCallbacksfor all services consuming user-provided data; ensure callback modules are loaded only from signed system locations.
Conclusion
CVE-2026-49176 exemplifies the risks inherent in services that resolve caller identity before crossing privilege boundaries. The vulnerability combines several well-known primitives—known-folder redirection, ESE callback execution, and token session manipulation—into a complete privilege-escalation attack with minimal complexity. The July patch represents a clean solution: by disabling ESE database operations entirely, the service eliminates the callback execution surface while maintaining functional availability. Organizations should prioritize patching and implement the detection strategies outlined above to identify exploitation attempts in-flight.
References
- Microsoft Security Update Guide: CVE-2026-49176
- Microsoft ESE documentation: Callback Parameters
- Microsoft ESE documentation: JET_USERDEFINEDDEFAULT Structure
- STAR Labs SG
- CVE-2026-49176 PoC repository: github.com/DavidCarliez/CVE-2026-49176_LPE_POC
Original text: “CVE-2026-49176 Exploit Development: WalletService to SYSTEM” by David Carliez.


