The RubyGems installer attempts to prevent a gem from writing any files outside the install directory; however it is possible to bypass the check with a symbolic link in a crafted gem.
$ tar -xvf symlink.gem
metadata.gz
data.tar.gz
$ tar -tvf data.tar.gz
-rw-r--r-- 0/0 12 1969-12-31 16:00 README
lrw-r--r-- 0/0 0 1969-12-31 16:00 link -> /tmp
-rw-r--r-- 0/0 6 1969-12-31 16:00 link/HACKED
Using the attached symlink.gem
:
gem install symlink.gem
# or
gem unpack symlink.gem
This will create a file /tmp/HACKED.
The name and contents of the written file, as well as the file permissions, are arbitrary.
Using this technique, an attacker could easily get code execution, for example by overwriting a system binary or writing into a user's .profile
.
Note that the exploit will even work with gem unpack
, which is supposed to be safe of system-level side-effects.
For comparison, this exploit is more powerful than #243156 (and #270068) as the target directory doesn't need to contain a dash.
The code in install_location is supposed to check if the target filename is outside the destination directory. It does this by fully resolving (using File.realpath
) the destination directory and then seeing if the target filename .start_with?
that directory.
This test succeeds for a symlink that points outside the gem's install directory, because its "destination directory" is the directory where it's located (not where it points), which is local.
The test also succeeds for a file that uses the symlink to "escape" the local directory, because the symlink really is its prefix.
However, in combination, these files can allow for arbitrary writes, as shown.
The root cause vulnerability is the ability of symlinks to point outside the gem. This was actually forbidden in a commit from 2015, but was made more permissive in a later commit, creating this vulnerability.
The course of action we recommend is to (again) disallow symlinks that point outside the gem directory.
symlink.gem
, an example of a vulnerable gem. Note: extract this using tar
instead of gem
to avoid triggering the vulnerability (e.g., tar -Oxf symlink.gem data.tar.gz | tar -tzvf -
)make-symlink-gem.py
, sample code that generates the proof of concept (to run: ./make-symlink-gem.py > symlink.gem
)0001-Add-a-test-test_extract_symlink_parent.patch
, test code that can be added to RubyGems to test for this vulnerability