Symlink attacks without code execution

Background

Let’s take a look at the following piece of code:

if (is_infected_file(path)) {
  remove_file(path);
}

This is a oversimplified routine from a typical antivirus scanner — it takes a file path, checks data of that file using malware signatures, and removes the file if it’s “infected”.

However, if the remove_file() routine follows symlinks (i.e., it deletes a symlink target, not a symlink itself), security problems arise…

A malicious program can create a file, fill it with bytes that are detected by a specific antivirus engine (e.g., write the EICAR string), trigger the antivirus scan somehow (e.g., by trying to read that file, thus triggering on-access scans), and then quickly replace that file with a symlink to another file.

This leads to a well-known race condition: the is_infected_file() routine deals with one file, but, occasionally, the remove_file() routine deals with another file! The path is the same, but in these two routines it points to different files: one is a regular file that is detected as “infected”, another one is a symlink pointing somewhere.

If the attacker is lucky enough, the remove_file() routine deletes the symlink target (which is attacker-chosen).

This leads to many possible issues, including denial-of-service (when an important system file is deleted), privilege escalation (when a configuration file with security-related settings is deleted, forcing some software to use “less secure” defaults), or even information disclosure (when software creating a backup is forced to copy sensitive files to a world-readable location).

In general, symlink attacks allow low-privileged programs to do some unintended actions against files they can’t access directly.

More examples can be found in this Wikipedia article and in this research paper.

There is one limitation: symlink attacks require code execution. In particular, attackers must create a file, trigger a vulnerable application, and then quickly replace that file with a symlink.

Challenge

What if no code execution is needed to mount such an attack?

Of course, a program that follows symlinks in a way that enables attacks mentioned above should be fixed. (And the number of vulnerable programs is shocking…)

As an additional measure, there are two mitigations in the Windows world:

  • by default, only a privileged user can create symlinks to files (this reduces the attack surface slightly, because a low-privileged user can create symlinks to directories — strictly speaking, these are junction points);
  • known vulnerable programs running with high privileges don’t follow symlinks created by low-privileged users (the NTFS driver and the NT kernel work together to block that kind of access)*.

* — symlinks created by unprivileged users are marked as such in the file system metadata, any attempt to follow such a symlink from a privileged program running with the RedirectionTrust mitigation enabled is blocked. See this post for additional details.

Both mitigations work against “dynamic” symlinks. Here, I mean symlinks that are created at the runtime.

Unprivileged users can attach external drives to the computer, which may contain file systems with symlinks marked as created by privileged processes, but unprivileged users can’t create such symlinks in the mounted file system.

Two mitigations mentioned above don’t provide any kind of protection against symlinks already existing on external drives. That sounds reasonable, because symlink attacks require the creation of a symlink instead of a regular file or a directory (e.g., if the is_infected_file() routine gets an existing symlink as input, the remove_file() routine won’t be called, because the target file isn’t “infected”). So, blocking the creation of dangerous symlinks seems to be enough…

What if attackers don’t need to create symlinks at the runtime to mount these attacks?

If we can’t create symlinks at the runtime, we can force the NTFS driver to do so on our disk image!

By filling a directory with files sharing the same name (this is an obvious file system format violation, but it can be easily created in a HEX editor), we can trigger healing routines implemented in the NTFS driver, which “throw away” a file being accessed, one at a time.

(Of course, we need to mount this crafted file system image, which requires elevated privileges, but… Stay tuned!)

It looks like this (when mounted using the ntfs-3g driver):

$ ls -l /mnt/tmp/1
total 2
-rwxrwxrwx 1 root root 68 Jan  3 18:29 test.txt
-rwxrwxrwx 1 root root 68 Jan  3 18:29 test.txt
-rwxrwxrwx 1 root root 68 Jan  3 18:29 test.txt
-rwxrwxrwx 1 root root 68 Jan  3 18:29 test.txt

Actually, the first three files in this index are regular ones, containing the EICAR string. The fourth file in that index is a symlink (pointing somewhere into the C: drive). Edit (2025-02-24): and it’s marked as “trusted” (created by a privileged user), so the RedirectionTrust mitigation is bypassed…

During my experiments, I also found that the order of file records describing those “duplicate” files is important for the exploitation: (1) the symlink must have a file record number lower than three other files in that directory have, and (2) file record numbers for three regular files in that directory must grow according to the positions of those files in the directory index, and (3) the symlink must be the last entry in the directory index.

Here is the relevant output of the fls tool (The Sleuth Kit) to demonstrate these conditions, the comments (marked with “#“) are mine:

r/r 57-128-1:	test.txt # Regular file (EICAR), file record: #57, index position: 0
r/r 58-128-1:	test.txt # Regular file (EICAR), file record: #58, index position: 1
r/r 59-128-1:	test.txt # Regular file (EICAR), file record: #59, index position: 2
r/r 48-128-1:	test.txt # Symlink, file record: #48, index position: 3

When such a file system is mounted, an attempt to open (e.g., via the CreateFile() call) or list the “test.txt” file in the affected directory will result in:

  1. When this file is opened (by its path) or listed for the first time — it’s a regular file (containing the EICAR string).
  2. When this file is opened (listed) for the second time — it’s a regular file (containing the same string).
  3. When this file is opened (listed) for the third time — it’s a regular file (containing the same string).
  4. When this file is opened (listed) for the fourth time — it’s a symlink (reparse point). And, at this point, the file will be “stable” (always the symlink).

The root cause is that the NTFS driver tries to fix the format violation, removing one file with a duplicate name from the directory index when such a file is opened (or listed), but keeping all remaining files (having duplicate names) in that index intact (and this is repeated each time the file is opened or listed, until only one file is left: the symlink).

The actual number of “file open” events before the file is “replaced” with a symlink depends on the number of files with duplicate names in the directory. It can be any number. In the proof-of-concept image and in this post, it’s three.

So, it’s possible to “replace” the file each time the CreateFile() routine is called for that file!

Here is my video demonstrating this:

And here is that proof-of-concept image (compressed): https://github.com/msuhanov/articles/raw/refs/heads/master/misc/PoC.vhdx.gz

So, it’s possible to replace a regular file with a symlink to another file without executing code, just by mounting and accessing a crafted file system image…

There is one more advantage: the NTFS driver provides a “more atomic” replace primitive than usermode applications can achieve. In fact, the attacker always wins the race condition, if the exact number of “file opens” for a particular vulnerable application is known.

Can unprivileged users mount such an attack?

In general, unprivileged users can’t mount disk images, but they can mount external drives (e.g., a USB Flash stick containing such a crafted file system image).

In this scenario, the attack requires physical access, but this is a common case: an employee having an unprivileged account on a corporate computer and physical access to that computer (and, again, TPM-based full-disk encryption can be used to block obvious attacks involving moving the internal drive to another computer to bypass NTFS access restrictions).

Additionally, some disk encryption software allows unprivileged users to freely mount their own file-based containers (so, even remotely connected unprivileged users can mount custom file system images). For example, VeraCrypt:

Under Windows, a user without administrator privileges can (assuming the default VeraCrypt and operating system configurations):

Mount any file-hosted VeraCrypt volume provided that the file permissions of the container allow it.

(Source.)

So, there are at least two ways to mount the symlink attack described without elevated privileges. However, both of them require prior access (physical or local).

Network attack with unintended user interaction (AV:N/UI:R)

Attacks involving malware placed into disk images (VHD, VHDX) are well-known. Unsuspecting users are double-clicking on such files, mounting (and opening) them without any prompt — as you can see on the video linked above, there is no UAC prompt in the default Windows configuration “for personal use”. (Attackers exploit such disk images to bypass Mark-of-the-Web on endpoints and antivirus checks on e-mail gateways.)

With this vulnerability, it’s possible to deliver arbitrary file deletion payloads via crafted disk images: a user double-clicks on the disk image received via e-mail (or another network source), an antivirus engine starts scanning the mount point of that disk image, triggering the symlink attack. No code execution from the attacker’s side is required!

Of course, the attacker must chain two vulnerabilities: this one and another one in antivirus software (it must follow symlinks when deleting “infected” files). It’s 2025 and such vulnerabilities still exist in antivirus software! (More about this next time…)

Update (2025-10-10): Cisco fixed the “follow symlinks to files” vulnerability in ClamAV some months ago, but no CVE number was assigned (probably, because they state that “[o]nly an admin should be able to create a reparse point for a file”, but they were aware of the issue described in this post).

Timeline

  • 2025-02-03: I sent the vulnerability report to Microsoft.
  • 2025-02-19: Microsoft replied that this is not a vulnerability.

After careful investigation, this case does not meet MSRC’s current bar for immediate servicing as symlinks from removable media are intended to be followed in Windows. Additionally, Windows doesn’t differentiate between symlinks on removable drives and the C: drive.

If you can find a specific service that demonstrates Remote Code Execution/Elevation of Privilege please submit another report and we are happy to investigate it.

One thought on “Symlink attacks without code execution

Leave a comment