diff --git a/src/windows-hardening/windows-local-privilege-escalation/README.md b/src/windows-hardening/windows-local-privilege-escalation/README.md index 85e4b95dc0b..11e663e29a1 100644 --- a/src/windows-hardening/windows-local-privilege-escalation/README.md +++ b/src/windows-hardening/windows-local-privilege-escalation/README.md @@ -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 `false`.[[38]](#references) + +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.[[38]](#references) + +```powershell +schtasks /query /tn "" /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.[[38]](#references) + +```c +#include +#include +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.[[38]](#references) + +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.[[31]](#references)[[38]](#references) + +### 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.[[38]](#references) + +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.[[38]](#references) + ## 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).[[31]](#references)[[32]](#references) @@ -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}}