Several months ago I found an issue (now CVE-2020-27348) with Ubuntu’s new package management system, Snapcraft. This bug introduced a classic pattern of insecurity to these ‘Snap’ managed applications which is analogous to DLL sideloading issues on Windows (a form of dynamic library injection).
In this post, I’ll discuss how this issue was discovered while playing in a CTF and how it can be leveraged to get code execution via these packages. Some of the affected packages included Chromium, VLC, Docker, Audacity, and many others available through the new package manager.
A Segfault In Docker
I originally ran into this bug while playing a CTF. I was working on a binary exploitation challenge (a ‘pwnable’) where competitors were provided with the vulnerable binary alongside a copy of the
libc.so.6 (a standard C library, shared object) that was running on the server.
I created a docker container for the challenge materials to start work on the task, but when I tried to run
docker build I got a very unexpected message:
$ docker build -t ctf . Segmentation fault (core dumped)
Now this surprised me because I had never seen the
docker command segfault like that before. Testing from a different directory,
docker worked as expected, so something special was happening in the challenge directory.
I immediately had a suspicion that docker itself was somehow loading the challenge’s
libc.so.6 in the current directory rather than the standard C library installed on my host system.
To test this hypothesis, we can use
strace to look for libraries accessed at runtime:
$ sudo strace -f -v -e openat -s 1000 sudo -u $(whoami) docker ... [pid 50669] openat(AT_FDCWD, "/snap/docker/796/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "tls/x86_64/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "x86_64/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) [pid 50669] openat(AT_FDCWD, "libc.so.6", O_RDONLY|O_CLOEXEC) = 3
The resulting output shows
docker itself searching through various paths for
libc.so.6. The last set of paths are relative, which confirms that it is indeed loading
libc.so.6 from the current directory. This is similar to classic DLL sideloading issues on Windows. Now the question is: why is this happening?
We can see that the program is also looking in
/snap/docker/796, which gives us a clue: this is not the ordinary docker binary we are running! We are actually launching an app packaged with Snapcraft.
Snapcraft is a new package manager managed by Canonical, which is installed in Ubuntu distributions by default. It allows users to easily download and run containerized applications. This takes care of dependencies while providing “extra security” under the banner of “containerization”.
Canonical has been pushing Snapcraft for a while, even suggesting it as the first recommendation when running an unknown command on recent versions of Ubuntu:
itszn@ubuntu:~$ docker Command 'docker' not found, but can be installed with: sudo snap install docker # version 19.03.11, or sudo apt install docker.io # version 20.10.2-0ubuntu1~20.04.3 See 'snap info docker' for additional versions.
One real benefit of Snapcraft packages (i.e. snaps) is that they can auto update separately from the rest of the system, allowing users to get security patches for high risk applications quicker.
With Ubuntu 20, Canonical has started forcing users onto Snap by making certain apt packages install their Snap counterparts instead. For example
apt-get install chromium-browser will actually install the chromium snap.
Despite being containerized, most applications include the home plug interface. The home plug is a configuration option which mounts your entire home directory into the container. The
libc.so.6 we downloaded was located in a subdirectory of the user’s home, which is why the container was able to access it.
Root Cause Analysis
Since Snapcraft packages require a wrapper to launch the container around the application, it was likely that a bad
LD_LIBRARY_PATH was being introduced into the application’s environment variables. This variable controls where the executable loader will look when trying to load shared libraries.
We can trace this variable by using strace to print the environment of any execve syscalls:
$ sudo strace -f -v -e execve -s 1000 sudo -u $(whoami) docker 2>&1 | egrep 'LD_LIBRARY_PATH=[^"]+"' -o | head -n 1 LD_LIBRARY_PATH=/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void:/snap/docker/796/lib:/snap/docker/796/lib/python3.6/site-packages/cffi.libs:/snap/docker/796/lib/x86_64-linux-gnu::/snap/docker/796/usr/lib/x86_64-linux-gnu:/snap/docker/796/lib:/snap/docker/796/usr/lib:/snap/docker/796/lib/x86_64-linux-gnu:/snap/docker/796/usr/lib/x86_64-linux-gnu"
Can you see it? There is an extra colon in the middle of the path!
This may not seem like a big deal, but due to the way that ld parses
:: counts as the current directory.
The script setting this environment variable is
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/x86_64-linux-gnu:$SNAP/usr/lib/x86_64-linux-gnu" export LD_LIBRARY_PATH="$SNAP/lib:$SNAP/lib/python3.6/site-packages/cffi.libs:$SNAP/lib/x86_64-linux-gnu:$SNAP/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
LD_LIBRARY_PATH is empty, these two lines will combine to create the
This file is generated by Snapcraft’s package builder, implying that this may be an issue for any Snap application that can access the current directory.
As an example, we will demonstrate how this issue could have been abused to achieve code execution through VLC, one of the most downloaded Snap applications.
The basis of this attack might come in the form of a victim downloading a tar archive containing a popular movie
.mp4 and a malicious
libc.so.6. If a user runs
vlc ./popular_movie.mp4 in the extracted directory, the Snap-managed VLC will load and execute the malicious library.
To make the attack seem less fishy, we can actually hide the library in one of the search sub-directories we saw earlier in the
strace output, such as
tls\. To an unsuspecting victim, maybe tls stands for “TransLation Subtitles”!?
Since VLC is expecting
libc.so.6 to have specific library exports, it’s easiest to just make a copy of Snap’s normal
libc.so.6 and patch it with our malicious code prior to distributing it. By utilizing Binary Ninja and its ShellCode Compiler we can just modify the
malloc() function to run our payload.
If we can entice a user to run a vulnerable Snap application in this directory it will load and run our malicious library.
Escaping Snap Containers (Sometimes)
We now have access to the user’s home directory, but we are still sandboxed by the Snap application container. Luckily there is often a way to influence the rest of the system from this pseudo-sandbox.
Many of the popular Snap applications are GUI applications. For these to work, the container must have access to the xserver, so these snaps have the
x11 plug. This exposes the
/tmp/.X11-unix/X0 socket to the container, which gives us a lot of power.
X11 is the linux service responsible for rendering windows and applications. An application communicates to it over a unix domain socket. Having access to this socket from inside the container means we can send the same commands other windows can. With this capability, an attacker could simulate any keyboard or mouse input to the system.
Since we are already acting on the commandline, the easiest thing to do is send keyboard strokes to the terminal. We can use xdotool to easily interface with this socket, typing and executing a payload into the terminal.
env DISPLAY=:0 LD_LIBRARY_PATH=$(pwd)/tls/x11 $(pwd)/tls/xdotool.static key space s h space t l s slash p Return
Here we have it run
sh tls/p, a script file from our archive. For fun, we can make this script cover its tracks using ANSI escape codes. Specifically,
\033[2F is an ANSI code to move the cursor up a line and
\033[0J will clear the rest of the screen after the cursor.
echo -e "\033[2F\033[0J\n" # Hide the command being run # TODO something evil here?
Putting all of these ideas together, the following demo illustrates what the attack looks like in practice. Note how our malicious library is tucked away in a subdirectory extracted from the archive, so the attack can be more subtle than one might normally think of sideloading issues:
We reported the issue in the Snapcraft bug tracker on 10/26/2020.
Canonical fixed this issue on 12/03/2020. The fix changes the
LD_LIBRARY_PATH to exclude empty variables. It was assigned CVE-2020-27348.
However, snaps need to be “refreshed” for the affected packages to be fixed as this is an issue with the wrappers generated by the Snap build system. For that reason, the process took several months for problematic packages to get replaced with newer ones… yet there may be un-updated and vulnerable snap packages still out there.
The issue was also acknowledged by HackerOne’s Internet Bug Bounty and awarded $750.
Incorrect manipulation and population of environment variables left Snapcraft installed applications vulnerable to malicious library sideloading-style attacks. Any application setting
LD_LIBRARY_PATH should be diligent in ensuring it does introduce sideloading of libraries from unintended (i.e. relative) directories.
We also saw how exposing X11 to a container completely compromises any security guarantees made by the container. Between the vulnerability and the ubiquity of insecure plugs, there seems to be minimal security benefits to Snap applications beside automatic updates.