ez 0dayz?
Introduction
This short post will be dedicated to the discussion of filesystem logic bugs - a special class of bugs.
In particular we’ll look into some common logic bugs on the Windows platform.
These bugs are special because they can be discovered with no RE, fuzzing or even coding skills(literally all you need is ProcMon), but their impact can vary from permanent DOS to LPE. In that matter they sell well, and I know researchers who exclusively hunt for them.
Apart from being an “easy 0day”, I think the process of finding such bugs teach a good initial reconnaisance procedure.
Regardless of the target, I still suggest you attach ProcMon and investigate filesystem/registry operations as the first step, even for a fuzzing/vr campaign.
Ps: The post is really short because I think it’s pointless to find CVEs as case studies for something like an arbitrary file deletion. The reader should be able to imagine it. At the same time I also think this topic is important enough to deserve a post.
Attack Surface
These bugs are usually local, so you should look into applications that cross the privilege boundary.
Examples include drivers, services, installers, startup apps and auto elevate apps.
The gist is to trick the privileged application into performing filesystem operations on unintended locations.
You would normally interact with the application with ProcMon enabled, then play with the filters and investigate the operations it’s performing related to the filesystem and processes.
For installers, you can find them under the C:\Windows\Installer
directory.
You can trigger a repair as an unprivileged user using msiexec
, by pointing to a GUID or filename.
1 | "C:\WINDOWS\system32\msiexec.exe" /fa {9EA198A3-76A5-40B9-8A2A-99796C52BE23} |
For drivers/services, try to log operations while using the client software provided along.
After learning how the client interacts with the service, you can hack up your own client to play with it.
All except for one bug I’m introducing today manifests due to improper permissions on files/folders, so that can be something to filter for in ProcMon.
With that being said, let’s jump into the first bug class, TOCTOU.
TOCTOU
Time of check time of use(TOCTOU) bugs are a type of race condition bugs.
When a check for an operation and the operation itself is not atomic, there is an opportunity for the state to be altered right after the check and before the operation such that the operation produces undefined results.
Here I’ll use TOCTOU to represent race condition bugs in general, although it’s technically a sub-class.
Example 1: Write->Execute
A typical write-execute behaviour looks like this.
A high privileged software extracts another tool from itself, drops it to a directory with improper permissions then execute it(the second CreateFile
call is a built-in precursor to the CreateProcess
API as shown).
We can run arbitrary code by looping to detect the file, setting an oplock on it once found, and replacing its contents with arbitrary executable once the oplock is triggered.(on execution).
Example 2: Write->Read
Similar to above, but this time the application is simply reading the data back after writing.
This is not always exploitable, you’ll have to see how the data influences application behaviour which requires intuition/RE.
If you can write bogus data to trigger an unhandled exception, that’s useful in some cases we’ll see later.
Conclusion
The core of these issues lie in the improper permissions set on the file such that an unprivileged user is capable of opening a write handle to it.
They can be mitigated by either writing to a protected directory or creating the file with an appropriate ACL.
Improper Folder Permissions
In the event where only a single operation occurs and there’s no chance for TOCTOU, you might think the code is now safe.
However, we can still perform exploitation if the operation’s target directory is overly permissible.
Conditions:
- Have write permission (delete is not needed) over the desired directory.
- Have the permission to delete all files inside the directory.
If these conditions are satisfied, we can create a filesystem junction in place of the directory.
A junction points to an existing directory/path and redirects all operations there.
We can use sysinternals tool accesschk.exe
to find writable directories.
1 | accesschk.exe -accepteula -wus "Users" c:\*.* > user.txt |
Example 1: File delete to arbitrary delete
If a driver deletes C:\secure\vuln\log.txt
where vuln is a folder with a permissible ACL as mentioned above, we can create a junction to point C:\secure\vuln
to C:\windows\system32
.
Now it will try to delete C:\windows\system32\log.txt
That’s not really useful because we want an arbitrary delete, not a fixed filename delete.
Here’s where symlinks come in handy.
On Windows, non-high privileged users are unable to create symlinks in filesystem directories.
Only a few object directories, such as \RPC Control\
, allow for symlink creation.
The curious reader can use the sysinternals tool WinObj
to explore.
Now if we junction C:\secure\vuln
to \RPC Control\
and create a symlink \RPC Control\log.txt
to C:\windows\system32\ntdll.dll
, the operation will go from C:\secure\
to \RPC Control\
, to the symbolic log.txt
then to ntdll.dll
and delete ntdll.(In the real world this is not possible because ntdll.dll is only controllable by TrustedInstaller and also because we’ll get a sharing violation trying to delete it)
C Code to create a junction:
1 | BOOL CreateJunctionW(LPCWSTR VictimDirectory, LPCWSTR TargetDirectory, BOOL Delete) |
Junctions are implemented as NTFS Reparse Points, so we issue the FSCTL_SET_REPARSE_POINT
ioctl.
C Code to create symlink:
1 | static HANDLE CreateSymLinkInternalW(LPCWSTR ToCreate, LPCWSTR CreateFrom) |
We can exploit this into an unreliable code execution by following https://www.zerodayinitiative.com/blog/2022/3/16/abusing-arbitrary-file-deletes-to-escalate-privilege-and-other-great-tricks
File Deletion on Windows
Here I’ll digress slightly and talk about the different ways to delete files on Windows, because many people are not filtering properly in ProcMon when looking for file deletes.
There are mainly 3 ways to delete a file on Windows.
DeleteFile
Processes can delete files using Win32 APIs like DeleteFileW
, which call the native API SetDispositionInformationEx
under the hood.
Interestingly this is also what happens during an OpenFile
call like
1 | OpenFile("C:\\Users\\User\\Desktop\\todelete.txt", &buf, OF_DELETE); |
We should filter for:
1 | operation: SetDispositionInformationEx |
SetFileInformationByHandle
This API can also be used to set disposition information on open file handles so the file will be deleted upon handle closure.
Example:
1 | HANDLE x = CreateFileW( |
Internally this calls the slightly different SetDispositionInformationFile
API.
We should filter for:
1 | operation: SetDispositionInformationFile |
CreateFile flag
Finally processes can delete a file by opening it with the flag FILE_FLAG_DELETE_ON_CLOSE
then closing the handle.
1 | HANDLE x = CreateFileW( |
We should filter for:
1 | operation: CreateFile |
These should help you catch all file deletion traces in ProcMon.
Example 2: File move to arbitrary write
An interesting case study I’ve read somewhere was regarding a file move to LPE.
When an operation such as move C:\secure\vuln\a.txt c:\secure\vuln\a.txt.bak
occurs, we can junction vuln
to \RPC Control\
, symlink a.txt
to some payload and a.txt.bak
to a service binary.
When the move is executed our payload will be moved to replace the service binary.
The move
command(and similar API/utils) show up as SetRenameInformationFile
in procmon, so that’s something to look for if you’re hunting.
This case study shows the flexibility and creativeness of a chained setup when exploiting logic bugs.
Have a file move where the target is not controllable but readable? Use the same logic for an arbitrary read.
Conclusion
File write is even easier to exploit, just overwrite an obscure dll/service binary and get system.
File read is target dependent. Think of ways where the target can transmit the data.
Just as an example, if we exploit a driver for arbitrary read and the driver sends it to a usermode client, we can open a handle to the client and read its memory for the data.
Folder creation can be exploited for permanent DOS by creating the folder C:\Windows\System32\cng.sys
as mentioned in the zdi blog above.
Gotcha
Since a certain Windows 10 build in mid 2022(I don’t remember the exact build number), Microsoft shipped a mitigation called RedirectionTrust Policy
, which when enforced prevents a process from following junctions created by non-admin users.
As of mid 2023 now it seems like only msiexec.exe
(windows installer) enforces it. This killed off quite a lot of bugs in installers.
I wrote a small tool to query for this policy.
Unsafe Path
Unknown to some, the system PATH is actually really important for security.
As an exercise, run the following powershell command
1 | @(1,'USR'),@(2,'SYS')|%{("("+$_[1]+" PATH):");[Environment]::GetEnvironmentVariable('PATH',$_[0]).Split(';');""} |
and see if there are any directories on the system PATH that’s world writable.
When Windows boots, the Task Scheduler service is automatically started, which tries to load a non-existent WptsExtensions.dll
.
For some reason this dll does not exist anywhere, so Windows will search all system PATH directories in an attempt to load it, following the LoadLibrary
search path.
If we can drop a malicious dll with the same name in one of the system PATH directories, we can get code execution on the next boot.
It’s a LPE if we find an installer/driver adding an overly permissible directory to the system PATH.
Impersonated Device Map
Quoting James Forshaw:
1 | Each login session has a DosDevices mapping under \Sessions\0\DosDevices\X-Y where X-Y is the login session ID. This object directory is writeable by the user. When a \??\ path is looked up the kernel first checks the per-login session mapping for a symlink to the drive mapping, if not found it will fallback to looking up in \GLOBAL??. This mapping is also done when impersonating another user, which is typical of system services when performing actions on behalf of another user. |
In other words, volumes such as the C
drive are symbolic links in the global object manager directory.
Processes and users(logon sessions) have their own device directories, with the process’s overriding the user’s and the user’s overriding the global directory.
When an operation on a filesystem path such as C:\Windows\test.exe
is performed, the kernel first transforms the path into a NT Path by prefixing \??\
to it.\??\
is yet another symbolic link which resolves to the logon session’s device directory.
Users are allowed to write to their own device directories, but not the global directory.
When the kernel resolves the C
drive symbolic link, it first checks if a process device map exists.
If it does, it tries to look up C:
inside of that.
Other wise, it falls back to the \??\
user device directory(normally does not have any drive mappings as shown above), and that falls back to the global \GLOBAL??
directory where the legitimate mapping exists.
More importantly to us, if a service impersonates our current user while calling CreateProcessW(L"C:\Windows\System32\notepad.exe")
, we can create our own mapping to map the C Drive(C:\
) to point to C:\test\
in our logon session’s device directory and the process created will be C:\test\Windows\System32\notepad.exe
.
This used to be the case for LoadLibrary
calls as well, until Microsoft added the flag OBJ_IGNORE_IMPERSONATED_DEVICEMAP
internally when LoadLibrary
searches for the dll.
Not sure why they didn’t fix it for CreateProcess
and CreateFile
calls, but these are still exploitable for arbitrary r/w/x.
As an exercise, try exploiting CVE-2020-1337 using this technique instead of the publicly available one using junctions.
C Code to spoof device map(combined above):
1 | HANDLE CreateFakeDeviceMapW(LPCWSTR DrivePath, LPCWSTR FakeDirectory) |
In practice, just running the code above will also change the device map for all other processes started with the current user’s logon session and therefore using the current user’s device map.
This often leads to issues such as explorer.exe hanging/crashing after clicking around too much and the GUI just goes nuts.
We can deal with this by setting all current processes to use the \GLOBAL??
directory as their per process device map.
That will override the user device map settings as mentioned above.
Child processes and new processes however will still default back to the user’s corrupted device directory.
This can be solved by creating junctions to folders under the legitimate C drive in our rogue C drive like shown here:
https://offsec.almond.consulting/images/windows-msiexec-eop-cve-2020-0911/redirection_setup.png
The link contains a great case study regarding this scenario on the windows installer.
C Code to set global directory:
1 | BOOL SetAllProcessToGlobalDeviceMap(VOID) |
I also found an eccentric 0day/condition a while ago while playing with this.
Reported and I’ll disclose it in the next post if Windows fixes/rejects it.
Tooling
Wrote a small library to alleviate the trouble testing these bugs!
It implements several functionalities such as oplock, junction management and redirection trust enumeration.
Conclusion
There used to be more of such techniques on Windows, such as ability to create hardlinks and symlinks.
Hardlinks are especially interesting because they represent a true second mapping of the file with the same permissions.
On Linux for example, let’s say a SUID app accesses GetCurrentPath() + "/scripts/run.py"
, where GetCurrentPath()
returns the path of the binary, we can hardlink the app to our home directory.
Now the app still retains its SUID bit but GetCurrentPath()
will return our home directory, running a file we can control.
Although I’ve described some of the more general scenarios, there is no limit to logic bugs.
You can never imagine the weird operations softwares are performing until you see them in ProcMon.
Similar to IDOR bugs in web apps, these bugs feed families.
Try triggering a repair on your favourite software’s installer and finding one for yourself!