The entire line is a shell command that adds the current user to theecho 'echo "$(whoami) ALL=(ALL) NOPASSWD:ALL" >&3' | DYLD_PRINT_TO_FILE=/etc/sudoers newgrp; sudo -s # via reddit: numinit (shorter)
/etc/sudoersfile 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' | newgrpThe sub-shell started by newgrp runs the command
echo hellowhich prints out
helloto the terminal. In the exploit, the command passed to the sub-shell is
echo "$(whoami) ALL=(ALL) NOPASSWD:ALL" >&3which 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
Before dyld could write to a file, it has to open the file and obtain a file descriptor. Here
- 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.
DYLD_PRINT_TO_FILE=/etc/sudoerstells dyld to open
/etc/sudoerswhich 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
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/sudoersso 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.
- 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_FILEcould 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 four will prevent any of these components from becoming part of a pathway for a future exploit.