Remote Code Execution in Alpine Linux
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:
Vulnerability
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 apk
exits.
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.apk-new
. - Create a symlink to
/etc/apk/commit_hooks.d/x
named anything – say,link
. This gets expanded to be calledlink.apk-new
but still points to/etc/apk/commit_hooks.d/x
. - Create a regular file named
link
(which will also be expanded tolink.apk-new
). This will write through the symlink and create a file at/etc/apk/commit_hooks.d/x
. - When
apk
realizes that the package’s hash doesn’t match the signed index, it will first unlinklink.apk-new
– but/etc/apk/commit_hooks.d/x
will persist! It will then fail to unlink/etc/apk/commit_hooks.d/
withENOTEMPTY
because 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 apk
process:
So we:
- Find the
pid
of theapk
process usingpidof
- Find the process’s executable memory using
/proc/<pid>/maps
, and - 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.
When apk
resumes execution after our commit hook exits, it will run our shellcode.
Conclusion
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.
Shameless plug
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?
This is why I launched BountyGraph last month. BountyGraph provides a mechanism to crowdfund bug bounty programs for important dependencies. I hope you’ll check it out!