posted 2018-01-04
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.
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.
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
mjo $ cd /var/www/chown-test/bar
mjo $ while true; do ln -s -f /etc/passwd ./baz; done;
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.
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.