Thursday, July 23, 2015

Tweet sized Mac OS X 10.10 exploit

It took me a while to understand what this exploit is doing, so I thought I would make a note for my own benefit and for others.
echo 'echo "$(whoami) ALL=(ALL) NOPASSWD:ALL" >&3' | 
DYLD_PRINT_TO_FILE=/etc/sudoers newgrp; sudo -s # via reddit: numinit (shorter)
The entire line is a shell command that adds the current user to the /etc/sudoers file and grants it sudo access without password, then runs sudo to escalate to a shell with root privilege. The greyed out part is the comment. But how it accomplishes this is very clever.

Here is what the rest of the code does: newgrp is a command that starts a command line shell after setting or resetting the effective group. It is a setuid root program which means it runs with escalated root privilege, but newgrp only runs the sub-shell after downgrading the privilege back to the current user and the desired group. The shell interpret commands from its standard input, so this fragment pipes a command to the sub-shell which runs it.
echo 'echo hello' | newgrp
The sub-shell started by newgrp runs the command echo hello which prints out hello to the terminal. In the exploit, the command passed to the sub-shell is echo "$(whoami) ALL=(ALL) NOPASSWD:ALL" >&3 which writes a sudoer line for the current user to file descriptor 3.

How does file descriptor 3 come into play? When you run newgrp with the environment variable DYLD_, it gets interpreted by the dynamic loader (dyld) with a specific meaning. The purpose of the dynamic loader is to piece a program together from different parts. It runs as the same privilege of the program.

On Mac OS X 10.10, the man page describes DYLD_PRINT_TO_FILE as follows.
DYLD_PRINT_TO_FILE
This is a path to a (writable) file. Normally, the dynamic linker writes all logging output (triggered by DYLD_PRINT_* settings) to file descriptor 2 (which is usually stderr). But this setting causes the dynamic linker to write logging output to the specified file.
Before dyld could write to a file, it has to open the file and obtain a file descriptor. Here DYLD_PRINT_TO_FILE=/etc/sudoers tells dyld to open /etc/sudoers which is normally only writable by root. It succeeds because dyld runs as root since newgrp is setuid root.

In the usual case, file descriptors 0, 1, 2 are reserved for standard input, standard output, and standard error, so the next available file descriptor is 3 which is assigned to the newly opened file. Now dyld is not told to actually write anything, but it leaves the file descriptor 3 open. Unless file descriptors has O_CLOEXEC (see open), they are kept open across fork() and exec() which are system calls for starting a new program. The same file descriptor, now with access to a file that normally root can open, is seen by the sub-shell run by newgrp even though the sub-shell is not root.

The command run by the newgrp sub-shell then modifies /etc/sudoers so that the current user could run sudo without a password, by writing the line to file descriptor 3.

There are several ways to block this exploit.
  • dyld should strip all DYLD_ environment variables if it realizes that the current program is setuid. This is the approach taken by the Linux dynamic loader. These environment variables alter the behavior of dyld, which is undesirable for setuid programs.
  • newgrp should close all file descriptors except 0, 1, and 2 before running the sub-shell. Most of the times, file descriptors are not intended to be passed around across processes.
  • newgrp could also close file descriptor 0 (standard input) and instead reopen the controlling terminal. This disallows passing command via standard input.
  • In general, setuid programs that start a sub-shell should not do so without requesting password from the terminal. Programs like login, su, and sudo are generally okay.
    • Command to find setuid root programs: find /bin /usr/bin /sbin /usr/sbin -user root -perm -4000
  • The open() call might instead randomize the file descriptors returned. Conventionally it always uses the lowest numbered file descriptor, but this has no tangible benefit.
  • DYLD_PRINT_TO_FILE could open the file using O_CLOEXEC. When the current program starts a child process, the current file descriptor would be closed, but the dyld of the child process will see this environment variable again and attempt to open it using the appropriate credentials of the child. As long as the file is opened in append mode, the parent and child can both write to the same file atomically without overwriting each other's content.
Doing all will prevent any of these components from becoming part of a pathway for a future exploit.