Product: GNU chown and chgrp (coreutils)
Versions-affected: 8.29 and earlier (all)
Author: Michael Orlitzky
Bug-report:
http://lists.gnu.org/archive/html/coreutils/2017-12/msg00045.html
== 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.
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, don't also use "-L".