Security News

Cybersecurity news aggregator

🔓
CRITICAL Vulnerabilities Reddit r/netsec

CVE-2026-40369: Twelve Bytes to Escape the Browser Sandbox

CVE-2026-40369 (CVSS 7.8 HIGH) is an unprivileged arbitrary 12-byte kernel write vulnerability in the Windows `nt!ExpGetProcessInformation` function, reachable via the `NtQuerySystemInformation` system call from contexts like browser renderer sandboxes. Affected versions include Windows 11 24H2 prior to 10.0.26100.8390, Windows 11 25H2 prior to 10.0.26200.8390, and Windows 11 26H1 prior to 10.0.28000.2113. The flaw is fixed in the respective listed versions.
Read Full Article →

Share this post Facebook Twitter LinkedIn Email VK Reddit WhatsApp CVE-2026-40369: Twelve Bytes to Escape the Browser Sandbox Posted by: voidsec Post Date: May 20, 2026 voidsec 2026-05-20T12:41:12+02:00 Reading Time: 13 minutes TL;DR: CVE-2026-40369 is an unprivileged arbitrary 12-byte kernel write primitive in nt!ExpGetProcessInformation , reachable from any context that can call NtQuerySystemInformation , including Chrome, Edge and Firefox renderer sandboxes . In this post, I dissect the root cause and how to chain the primitive into a full LPE to lift a Medium-IL non-administrator process up to NT AUTHORITY\SYSTEM via NtCreateToken . I had originally prepared this bug for Pwn2Own Berlin . A couple of days before the contest, Ori Nimron independently dropped a public PoC for the same primitive on GitHub. Since the cat is out of the bag, I’m releasing the technical write-up and the exploitation strategy of my own chain , which takes a different route to SYSTEM than Ori’s: rather than classical token theft, it forges a SYSTEM primary token from scratch via NtCreateToken , and I think it’s worth documenting. Table of Contents Pre-Requisites To follow this end-to-end, you’ll want to be comfortable with: The NtQuerySystemInformation syscall, its information classes, and the way the user-mode SystemInformation pointer is validated (or not) on the way down. Hex-Rays / IDA Pro and basic Windows kernel reverse engineering. The decompile excerpts in this post come from ntoskrnl.exe on Windows 11 25H2 build 26100.8246, with ImageBase = 0x140000000 . The _TOKEN object layout. The field offsets I’ll touch ( ModifiedId at +0x38 , Privileges.Present at +0x40 , Privileges.Enabled at +0x48 , SessionId at +0x78 ) are the canonical ones for the 25H2 servicing branch; cross-reference with Vergilius when porting. The WIL feature-state cache mechanism, and in particular how Feature_RestrictKernelAddressLeaks gates the kernel-pointer-leaking information classes of NtQuerySystemInformation (classes 11 , 64 , 66 , etc.). NtCreateToken and the SeCreateTokenPrivilege / SeTcbPrivilege / SeImpersonatePrivilege trio used to materialise a forged SYSTEM token without going through the classical SeDebug + OpenProcess + DuplicateTokenEx dance. The vulnerability in a nutshell NtQuerySystemInformation(class=0xFD) forwards a caller-controlled pointer into nt!ExpGetProcessInformation where three kernel-mode DWORD writes are performed without validating the destination when Length == 0 . Because ProbeForWrite() becomes a no-op on zero-length buffers, any writable kernel virtual address can be targeted from user mode, including browser renderer sandboxes. Call graph and code path NtQuerySystemInformation(SystemInformationClass, SystemInformation, Length, ReturnLength) -> nt!NtQuerySystemInformation -> nt!ExpQuerySystemInformation (probes SystemInformation, dispatches by class) -> nt!ExpGetProcessInformation (process-walk worker, contains the unchecked write) Symbol mapping for build 26100.8246 ( ImageBase = 0x140000000 ): nt!NtQuerySystemInformation : 0x140AE08A0 nt!ExpQuerySystemInformation : 0x140ADBB10 nt!ExpGetProcessInformation : 0x140ADA6D0 Crash site ( inc dword ptr [rbx] ): 0x140ADAAFE nt!ProbeForWrite : 0x14017C9F0 nt!IoConfigurationInformation : 0x140FD7838 The unchecked write Hex-Rays output for nt!ExpGetProcessInformation (truncated for clarity): NTSTATUS __fastcall ExpGetProcessInformation( __int64 a1, // SystemInformation pointer (caller-controlled) unsigned int a2, // Length _DWORD *a3, // ReturnLength out _DWORD *a4, // optional session-id filter int a5) // information class (5 / 57 / 148 / 252 / 253) { unsigned int *v85, *v95, *v99; ... v95 = (unsigned int *)a1; ... if ( a5 == 252 ) { ...; v90 = v95; v85 = NULL; } else { v90 = NULL; if ( a5 == 253 ) { v77 = 0; v86 = 12; v71 = 12; v87 = 0; v99 = v95; // <-- v99 is set to the caller's pointer v85 = NULL; goto LABEL_11; } ... } v99 = NULL; // <-- only reached for non-253 paths LABEL_11: ... /* size check: sets a status but DOES NOT return early */ v97 = v86; v11 = a2 < v86; if ( a2 < v86 ) { if ( !a3 ) return STATUS_INFO_LENGTH_MISMATCH; /* only triggers if return-length out is NULL */ v11 = a2 < v86; } v13 = v11 ? STATUS_INFO_LENGTH_MISMATCH : 0; /* status latched, execution continues */ /* access-check section: SeAccessCheck does NOT gate the write below */ PreviousMode = KeGetCurrentThread()->PreviousMode; if ( a5 != 148 || (result = ExCheckFullProcessInformationAccess(PreviousMode), result >= 0) ) { ... SeAccessCheck(SeMediumDaclSd, ...); ... /* main process-walk loop */ NextProcess = (__int64 *)PsIdleProcess; while ( 1 ) { if ( !NextProcess ) { ...; return v70; } if ( !ExpSysInfoShouldSkipProcess((__int64)NextProcess) && (!a4 || NextProcess != PsIdleProcess) ) { SessionId = PsGetSessionId((__int64)NextProcess); if ( (!a4 || SessionId == *a4) && PsIsProcessInSilo((struct _KPROCESS *)NextProcess, CurrentServerSilo) ) break; /* fall through to the per-process body below */ } Nex...

Share this article