Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/windows-hardening/windows-local-privilege-escalation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1863,6 +1863,44 @@ Then **read this to learn about UAC and UAC bypasses:**
../authentication-credentials-uac-and-efs/uac-user-account-control.md
{{#endref}}

## WNF-triggered privileged scheduled-task cleanup

A scheduled task that denies manual execution may still expose an unprivileged trigger. When auditing a privileged task, export its XML and inspect **all** triggers as well as its task DACL, principal and action. In particular, a `WnfStateChangeTrigger` names a Windows Notification Facility (WNF) state; if that securable state permits the current user to publish an update, the user can indirectly start the task under its configured identity despite `<AllowStartOnDemand>false</AllowStartOnDemand>`.<sup>[[38]](#references)</sup>

For example, the Recall `\Microsoft\Windows\WindowsAI\Recall\PolicyConfiguration` task ran as SYSTEM, accepted updates to WNF state `7508BCA32C079E41` from a standard user, and then inspected `%LOCALAPPDATA%\CoreAIPlatform.00\UKP\{GUID}` even when Recall was disabled.<sup>[[38]](#references)</sup>

```powershell
schtasks /query /tn "<TASK>" /xml > task.xml
Select-String -Path .\task.xml -Pattern 'WnfStateChangeTrigger|StateName|UserId|ClassId|AllowStartOnDemand'
```

To test reachability, resolve `ZwUpdateWnfStateData` from `ntdll.dll`, submit an empty update for the eight-byte state name, and correlate the return status with the task's last-run data or Procmon activity. The following reduced probe uses the byte sequence from the Recall task example; substitute the bytes exported for the task being tested.<sup>[[38]](#references)</sup>

```c
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (WINAPI *UpdateWnf)(PVOID,PVOID,ULONG,PVOID,PVOID,PVOID,ULONG);
int main(void) {
unsigned char stateName[8] = {0x75,0x08,0xBC,0xA3,0x2C,0x07,0x9e,0x41};
HMODULE ntdll = LoadLibraryW(L"ntdll.dll");
UpdateWnf update = (UpdateWnf)GetProcAddress(ntdll, "ZwUpdateWnfStateData");
if (!update) return 1;
return update(stateName, NULL, 0, NULL, NULL, NULL, 0) < 0;
}
```

### Turning recursive cleanup into privileged deletion

The useful primitive is not the trigger alone, but a privileged cleanup routine that accepts an attacker-controlled directory. Trace the operation from enumeration to the final mutation: profile/session discovery, wildcard-selected children, `SHCreateItemFromParsingName`, and `IFileOperation::DeleteItem` are strong indicators that a previously checked pathname will later be traversed recursively. If descendants are not opened relative to trusted directory handles and validated before deletion, an attacker can change resolution between enumeration and use.<sup>[[38]](#references)</sup>

Pair the WNF trigger with the [oplock, junction and Object Manager symlink substitution](#from-folder-contents-delete-to-system-eop) described below: the oplock pauses the cleaner after it selects the legitimate child, while the writable ancestor is replaced so the resumed privileged delete resolves through `\RPC Control` to the protected target. In the Recall chain this yielded the SYSTEM deletion of `C:\Config.Msi` required by the documented MSI rollback technique.<sup>[[31]](#references)[[38]](#references)</sup>

### Nested descendants bypass shallow reparse-point fixes

Validating only the attacker-controlled root and its immediate child does not secure a recursive pathname-based delete. Keep those checked objects legitimate and move the junction/oplock substitution into an additional child below them; `IFileOperation::DeleteItem` eventually reaches the unvalidated descendant and follows its new target. CVE-2026-20941 was produced by exactly this class of incomplete fix after the first Recall patch validated `UKP` and the GUID-shaped directory but retained the old recursive deletion operation.<sup>[[38]](#references)</sup>

The safe pattern is to open **every descendant** relative to an already trusted directory handle, obtain and verify the final resolved path/object, and apply the deletion disposition to that verified handle. Do not validate a prefix and then return to recursive path-based deletion. During patch review, also inspect structures at the real call site: in this case runtime inspection of `OBJECT_ATTRIBUTES` at `NtCreateFile` showed a non-null `RootDirectory` and a relative `{GUID}\\secure_file.lock` name, disproving misleading decompiler output that suggested a second `FILE_DELETE_ON_CLOSE` primitive.<sup>[[38]](#references)</sup>

## From Arbitrary Folder Delete/Move/Rename to SYSTEM EoP

The technique described [**in this blog post**](https://www.zerodayinitiative.com/blog/2022/3/16/abusing-arbitrary-file-deletes-to-escalate-privilege-and-other-great-tricks) with a exploit code [**available here**](https://github.com/thezdi/PoC/tree/main/FilesystemEoPs).<sup>[[31]](#references)[[32]](#references)</sup>
Expand Down Expand Up @@ -2197,5 +2235,6 @@ C:\Windows\microsoft.net\framework\v4.0.30319\MSBuild.exe -version #Compile the
- [35] [jas502n - CVE-2019-1388 PoC](https://github.com/jas502n/CVE-2019-1388)
- [36] [research.nccgroup.com - Kerberos Resource Based Constrained Delegation When An Image Change Leads To A Privilege Escalation](https://research.nccgroup.com/2019/08/20/kerberos-resource-based-constrained-delegation-when-an-image-change-leads-to-a-privilege-escalation)
- [37] [blog.ropnop.com - Extracting Ssh Private Keys From Windows 10 Ssh Agent](https://blog.ropnop.com/extracting-ssh-private-keys-from-windows-10-ssh-agent)
- [38] [MDSec - Total Recall: Retracing Your Steps Back to NT AUTHORITY\SYSTEM](https://www.mdsec.co.uk/2026/02/total-recall-retracing-your-steps-back-to-nt-authoritysystem/)

{{#include ../../banners/hacktricks-training.md}}