martin carpenter

contents

most popular
2012/05/05, updated 2012/12/15
Ubuntu unity lens for vim
2010/04/14
ckwtmpx

abuse fuse to pwn pt_chown

2013/08/14, updated 2014/02/14

tags: glibc pt_chown vulnerability CVE-2013-2207

Summary

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.

Preconditions

Technical details

Outline:

The filesystem stub only has to implement the following callbacks:

/* 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;
}
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.

Mitigation

Firstly the obvious configuration steps:

(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 calls isatty(3) on the passed file descriptor. This in turn calls tcgetattr(3), which is finally translated into ioctl(TCGETA, ...). The FUSE ioctl(2) callback command encoding contains a permitted data length of zero, ie it cannot write struct termios into ioctl's *data argument. tcgetattr() or isatty() could make some checks on the validity of the passed struct termios but instead currently only care that the ioctl()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.

Weaponization

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.

Historical spelunking

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.

Related attacks

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.

Timeline