tl;dr I found several bugs in
apk, the default package manager for Alpine Linux. Alpine is a really lightweight distro that is very commonly used with Docker. The worst of these bugs, the subject of this blog post, allows a network man-in-the-middle (or a malicious package mirror) to execute arbitrary code on the user’s machine. This is especially bad because packages aren’t served over TLS when using the default repositories. This bug has been fixed and the Alpine base images have been updated – you may want to rebuild your Alpine-derived images!
After gaining code execution, I figured out a cool way to make the original
apk process exit with a 0 exit code (without needing the
SYS_PTRACE capability) by writing to
/proc/<pid>/mem. The result is that a
Dockerfile that installs packages with
apk can be exploited and still build successfully.
Here’s a clip of me exploiting a Docker container based on Alpine as a network man-in-the-middle:
Arbitrary file creation leading to RCE
Alpine packages are distributed as
.apk files, which are actually just gzipped
tar files. When
apk is pulling packages, it extracts them into
/ before checking that the hash matches what the signed manifest says it should be. Well, kind of – while extracting the archive, each file name and hardlink target is suffixed with
.apk-new. Later, when
apk realizes that the hash of the downloaded package is incorrect, it tries to unlink all of the extracted files and directories.
Persistent arbitrary file writes can be easily turned into code execution because of
apk’s “commit hooks” feature. If we can figure out a way to extract a file into
/etc/apk/commit_hooks.d/ and have it stay there after the cleanup process, it will be executed before
With control of the tar file being downloaded, we can create a persistent “commit hook” like this:
- Create a folder at
/etc/apk/commit_hooks.d/, which doesn’t exist by default. Extracted folders are not suffixed with
- Create a symlink to
/etc/apk/commit_hooks.d/xnamed anything – say,
link. This gets expanded to be called
link.apk-newbut still points to
- Create a regular file named
link(which will also be expanded to
link.apk-new). This will write through the symlink and create a file at
apkrealizes that the package’s hash doesn’t match the signed index, it will first unlink
/etc/apk/commit_hooks.d/xwill persist! It will then fail to unlink
ENOTEMPTYbecause the directory now contains our payload.
Fixing the exit code
Now that we have arbitrary code running on the client before
apk has exited, it is important that we figure out a way to make the
apk process exit gracefully. If using
apk in a
Dockerfile build step, the step will fail if
apk returns a nonzero exit code.
If we do nothing,
apk will return an exit code equal to the number of packages it has failed to install, which is now at least one (amusingly, this value can overflow – if the
number of errors % 256 == 0, the process will return with exit code 0 and the build will succeed. This was fixed here.).
My first attempt was to use
gdb to attach to the process and just call
exit(0). Unfortunately, Docker containers don’t have the
SYS_PTRACE capability by default and so we can’t do this. Since we’re
root, however, we can read and write
/proc/<pid>/mem for the
- Find the
- Find the process’s executable memory using
- Write shellcode that will ultimately
exit(0)directly into memory. It was really surprising to me that this worked! I was expecting the write to fail.
apk resumes execution after our commit hook exits, it will run our shellcode.
If you use Alpine Linux in a production environment, you should 1. rebuild your images and 2. consider donating what you can to the developers. It seems like
apk has one main developer who fixed this bug in less than a week. The lead maintainer of Alpine cut a new release shortly thereafter.
There are probably hundreds of organizations using Alpine Linux in production environments that could have been affected by this bug. Some of those organizations almost certainly have bug bounty programs that would pay generously if a similar bug had been written by one of their own developers. If the goal of a bug bounty program is to help secure an organization, shouldn’t critical bugs in dependencies qualify to some extent?