
VFS for Git ab95cf51 - High - Local - Privilege leftover - Users mount pipe dehydrate/unmount
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · vfs-mount-dehydrate
Class: Privilege leftover Reach: Local
VFS for Git master ab95cf51 (tag v2.0.26259.1) - Microsoft
CreatePipeByName on the per-enlistment mount pipe grants BUILTIN\Users ReadWrite | CreateNewInstance. There is no owner SID check and no ImpersonateNamedPipeClient. HandleDehydrateFolders Path.Combines attacker folder names and feeds git via ProcessStartInfo.Arguments. The CLI dehydrate path runs IsFolderValid. The mount handler does not.
Anyone in Users who can guess \\.\pipe\GVFS_<ENLISTMENT> can unmount ProjFS, move directories as the mount user, and force git reset HEAD --hard.
| ID | no CVE yet |
| Class | Privilege leftover (named-pipe ACL) / path traversal / argument injection |
| Reach | Local |
| CWE | CWE-284, CWE-22, CWE-88 |
| CVSS | High: 7.8 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Product | VFS for Git |
| Affected | master ab95cf51d6ffd0ee0e2feae7180078fe9ecbbb3a; installer tags v2.0.26229.1 / v2.0.26259.1. Windows only. |
| Auth | Local. Any BUILTIN\Users principal while the victim has a mount |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only. how=gadget (no Windows VM / live ProjFS). |
Pipe protocol: UTF-8 Header or Header|body, terminated by byte 0x03. Pipe name is GVFS_ + uppercased enlistment root with : replaced by _ (example C:\Repos\Foo -> \\.\pipe\GVFS_C_\REPOS\FOO).
Victim has gvfs mount at a known root. Attacker is another Users principal on the same machine. Integrity and availability as the victim. Not SYSTEM. Not a shell by itself.
| Privilege | Method / sink | What it grants | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|---|
| Connect to the victim mount pipe | CreatePipeByName ACL BuiltinUsers ReadWrite | CreateNewInstance; no owner check | Talk the mount IPC | Recon of the pipe | Lab-proven (gadget ACL). Attacker SID accepted; owner-only mode returns connect-denied |
| Read enlistment path, cache path, RepoUrl | GetStatus | Leak the root used for leftover #1 / #4 | Recon | Lab-proven. Status JSON included RepoUrl and GVFS_C_\REPOS\FOO |
Unmount ProjFS and exit gvfs.mount | message Unmount | Take the enlistment down | Availability | Lab-proven. Replies Ack then Complete; mount state Unmounted |
| Unmount on a parse failure | ReleaseLock with empty body -> RequestData == null -> UnmountAndStopWorkingDirectoryCallbacks + Environment.Exit | Same unmount plus process exit | Availability | Lab-proven. Empty ReleaseLock| unmounted |
| Discard the victim working tree | Dehydrate|{"Folders":"--hard",...}. movedFolders.Add even if the source dir is missing. Reset("HEAD", "\"--hard\" ") becomes git reset HEAD --hard after quote strip | Destroy dirty tracked files | Integrity / availability | Lab-proven. Dirty tracked file lost the witness string |
| Move a folder out of the worktree as the victim | Path.Combine(WorkingDirectoryBackingRoot, folder) + MoveDirectory to BackupFolderPath | Steal / relocate a worktree folder | Integrity | Lab-proven. keepme left src and appeared under the attacker backup path |
Traverse .. (CLI IsFolderValid is not used) | same Combine; ..\secret | Move a sibling the mount user can write | Integrity | Lab-proven. Sibling secret landed under the attacker backup path. Applying IsFolderValid blocked it |
| Steal or destroy other victim-writable directories | same move with a ..\ chain that resolves outside the enlistment | Broader move as the mount user | Integrity | Inferred from Combine. This lab uses a throwaway tree only |
| Code exec as the victim | this pipe | Process create of attacker image | Integrity | Closed. No process create of attacker image on this leftover (see vfs-service-sid and vfs-hook-impersonate) |
| SYSTEM | mount IPC | LocalSystem | Integrity | Closed. Mount runs as the enlistment user |
--hard is not a .. path. CLI IsFolderValid still allows it. A fix must also reject option-like folder names.
Same pin leftovers: vfs-quote-breakout, vfs-clone-acl, vfs-service-sid, vfs-hook-impersonate, vfs-install-cwd.
I pinned VFSForGit master ab95cf51 after the 2026-09-28 merge. The service pipe and the per-enlistment mount pipe share CreatePipeByName. Dehydrate on the CLI validates folders. The mount handler does not.
Wrong turns already recorded: treating --hard as a path traversal (it is argument injection after quote strip); claiming SYSTEM; a reverse shell. Theatre. Native Windows was missing, so the lab is a gadget of those functions plus a throwaway git tree.
python3 vfs-mount-dehydrate-Abraxas-Labs.py
or
cd lab
./run.sh
Gadget HTTP on 127.0.0.1:19640 speaking the pipe message shape. Throwaway git tree inside the container. No reverse shell. No real profile path. Last line uses how=gadget.
SUCCESS vfs-mount-dehydrate how=gadget unmount=yes reset-hard=yes dirty-gone=yes move-inside=yes move-dotdot=yes pipe-acl=users-no-owner is-folder-valid-not-on-pipe=yes VFS-MOUNT-DEHYDRATE-WITNESS
Restrict CreatePipeByName to the enlistment owner SID plus LocalSystem. Drop CreateNewInstance for Users. Impersonate the client and require owner. Replicate IsFolderValid in HandleDehydrateFolders and reject names that look like git options. Do not treat ReleaseLock parse failure as process exit.