2013/08/14, updated 2014/02/14
tags: glibc pt_chown vulnerability CVE-2013-2207
An attacker's userland program (no privileges) can generate a file
descriptor in a FUSE filesystem that appears to be a PTY. This can then
be fed into setuid-root
executable pt_chown
where ptsname(3)
can
be fooled into giving back some other users' slave terminal by bogus
ioctl(2)
responses from FUSE. This slave will then get chown(2)
ed
to the attacking user, thus giving them complete control over it.
This affects (at least) recent versions of Ubuntu (12.x) and Fedora 18 and 19.
This exploit was presented at NullCon 2014 by Red Hat's Siddhesh Poyarekar.
ioctl(2)
supportuser_allow_other
in /etc/fuse.conf
/dev/pts
Outline:
allow_root
optionexec(2)
on pt_chown
pt_chown
then follows the TIOCGPTN
ioctl
(which we fake) to chown(2)
the PTS of our choosingThe filesystem stub only has to implement the following callbacks:
getattr
(aka stat(2)
) — this just needs to return a struct stat
of
a file owned by the victim (I borrowed /etc/passwd
whilst attacking root
's PTS)/* It doens't matter what this is as long as it's a plain file. Most unices should have a passwd(4) */ #define PATH_FOR_FAKE_STAT "/etc/passwd" int fakefs_getattr(const char *path, struct stat *statbuf) { int rc = -ENOENT; if(!strcmp(FAKE_PTS, path)) { rc = stat(PATH_FOR_FAKE_STAT, statbuf) ? -errno : 0; } return rc; }
ioctl(TCGETS, ...)
— this just needs to return 0 so that isatty(3)
is fooled and returns 1, "I am a terminal"ioctl(TIOCGPTN, ...)
— returns the PTS that we are trying to control
(this return value is carefully checked and the resulting full path /dev/pts/...
is
subsequently stat(2)
ed so we can't return anything more interesting here)int fakefs_ioctl(const char *path, int cmd, void *arg, struct fuse_file_info *fi, unsigned int flags, void *data) { int rc = -ENOENT; if(!strcmp(path, FAKE_PTS)) { switch(cmd) { case TCGETS: /* Return 0 so that isatty() returns 1. Datalen is zero so cannot fill with struct termios even if we wanted to. */ rc = 0; break; case TIOCGPTN: /* Must send a valid integer name of a file in /dev/pts else the subsequent stat() will fail. */ *(int *)data = FAKEFS_DATA->pts_id; rc = 0; break; default: rc = 1; break; } } return rc; }
I wrote the POC in C but using a higher level binding to FUSE such as fuse-python would have been less typing.
Firstly the obvious configuration steps:
fusermount(1)
user_allow_other
in /etc/fuse.conf
(You're reading this because of a security review of a Red Hat-based appliance where these mitigations were not in place).
Secondly fuse.conf
's mount_max
parameter may limit an attacker's
ability to jump in if FUSE filesystems are mounted at boot.
Thirdly there's pt_chown
itself. pt_chown
is setuid-root
, part of
glibc. The gentoo dev list already discussed removing pt_chown
earlier
this year where it was astutely noted (Mike Frysinger):
this system sucks for many reasons
Red Hat's grantpt(3)
man page says:
This is part of the Unix98 pty support, see pts(4). Many systems
implement this function via a set-user-ID helper binary called
"pt_chown". With Linux devpts no such helper binary is required.
This is the best plan (and the one that was adopted by libc maintainers).
There are safety checks that could be added either in the pt_chown
binary itself or dependent functions isatty(3)
and ptsname(3)
but setuid-root is never a great solution. An example of this sort of
"weak check" that one could envisage:
pt_chown
callsisatty(3)
on the passed file descriptor. This in turn callstcgetattr(3)
, which is finally translated intoioctl(TCGETA, ...)
. The FUSEioctl(2)
callback command encoding contains a permitted data length of zero, ie it cannot writestruct termios
intoioctl
's*data
argument.tcgetattr()
orisatty()
could make some checks on the validity of the passedstruct termios
but instead currently only care that theioctl()
s didn't fail.
This kind of protection is fragile and specific to the specific problem I outline above. It is not a good solution and I'm glad to see it was not adopted by glibc.
Finally, there's FUSE itself. Could it play a more protective role? It
entirely respects the published API with respect to this attack (and
I didn't target the sensitive (and well-reviewed) setuid and ioctl(2)
iovec
protections). It's hard to see how FUSE can do what it does and
protect applications from this sort of trickery. pt_chown
should simply
not be placing so much trust in file descriptor 3. Caveat escritor.
Note that this attack does not grant access to the master PTY. Consequently command injection via eg TIOCSTI is not possible. However, there are at least two other possibilities:
Firstly, one can inject arbitrary data into the victim's terminal. Aside from the obvious DoS there is a long history of terminal emulator attacks, either by directly targeting eg buffer overflows in the emulator itself or by targeting native emulator protocols such as ECMA-48's OSC "Operating System Command". A good starting reference is H D Moore's bugtraq posting from 2003 Terminal Emulator Security Issues.
Secondly, an attacker can stealthily shoulder-surf. Although the obvious
approach of racing the victim's emulator to read bytes and then write
them back won't work due to the banananananana problem (read a byte
from the victim's terminal, write it back and then... read the same byte
again...), modern system calls such as tee(2)
and splice(2)
enable
"peek" functionality.
As always I'm interested in knowing (a) how long this hole has been there and (b) how it first appeared. Going back through the glibc history we find the following commit:
commit 837dea7cf54827d6e43d88a9463bcc10d30472d0 Author: Ulrich Drepper <drepper@redhat.com> Date: Mon Jun 15 22:58:21 2009 -0700 Optimize pt_chown. Don't call chown and chmod if not necessary.
Pertinent diff excerpt from login/programs/pt_chown.c
:
@@ -119,12 +119,13 @@ do_pt_chown (void) /* Set the owner to the real user ID, and the group to that special group ID. */ - if (chown (pty, getuid (), gid) < 0) + if (st.st_gid != gid && chown (pty, getuid (), gid) < 0) return FAIL_EACCES;
Before this commit the call to chown(2)
was always made. After this
commit the condition short-circuits in the normal case (gid
is normally
the gid_t
of the group tty
with which PTS are created) and chown(2)
is not called. Users are prevented from stealing others users' terminals, because
they're all in the tty
group by default.
A couple of years later there is this commit from the same author:
commit f3799213a3ee8265ba47fad33d9cff71d97ab0d4 Author: Ulrich Drepper <drepper@gmail.com> Date: Mon May 16 01:43:56 2011 -0400 Remove shortcut for call of chown The UID might differ, too. Just call chown unconditionally.
This reverts the change! Pertinent diff excerpt, again from
login/programs/pt_chown.c
:
@@ -123,7 +123,7 @@ do_pt_chown (void) /* Set the owner to the real user ID, and the group to that special group ID. */ - if (st.st_gid != gid && chown (pty, getuid (), gid) < 0) + if (chown (pty, getuid (), gid) < 0) return FAIL_EACCES;
This reasoning in the commit is sensible (and in general we
strive to eliminate the "check-then-change" idiom to prevent
TOCTOU)... but it now permits
again chown(2)
of a slave from eg. root:tty
to martin:tty
, just
as pre-837de
. And the clincher is that meanwhile over in the Linux
kernel repository the following commit was made:
commit 59efec7b903987dcb60b9ebc85c7acd4443a11a1 Author: Tejun Heo <tj@kernel.org> Date: Wed Nov 26 12:03:55 2008 +0100 fuse: implement ioctl support
Using this an attacker can pretend that the fake file descriptor is a terminal device.
This is a good example of what makes modern system security so desperately
hard: no single entity controls the components and so today's "but that
can never happen" becomes tomorrow's exploit. It seems that this hole
has — apart from for a fluke 23 month period — always been
there. FUSE's ioctl
support was needed to open it right up though.
There are other similar filesystem-trust privilege attacks using FUSE (trust me on this...). In general races are not interesting: since the target filesystem must already be owned by the attacking user race attacks can already be won by ad-hoc timing methods. However attackers can make races more reliable by using FUSE (synchronization can be provided by the attacker's filesystem implementation).
Applications should (as always) be careful of what they consider to
be trusted paths. In particular the advent of a world-writable tmpfs
filesystem under /dev/shm
(POSIX shared memory) strikes me as
problematic for legacy code that assumes that anything under /dev
can be trusted. The ability to forge stat(2)
and ioctl(2)
reponses
for such files could be particularly irksome.