how Homebrew invites users to get pwned

Popular macOS package manager Homebrew is a great way to easily install and manage 3rd party software. As their own tag line goes, “Homebrew installs the stuff you need that Apple didn’t.”

However, installing it recently on a new setup brought something odd to my attention. An oddness, it turns out, that is a gaping security flaw.

Homebrew’s webpage encouragingly says “you can place a Homebrew installation wherever you like”, but almost everywhere 1 else 2, the docs are more insistent:

do yourself a favor and install to /usr/local. Some things may not build when installed elsewhere. One of the reasons Homebrew just works relative to the competition is because we recommend installing to /usr/local. Pick another prefix at your peril!

Peril indeed, for those that follow that advice. Homebrew’s installer is kind enough to tell you what is happening, but it seems neither the installer nor the developers have any idea just what this means:

Danger! Danger!

As soon as I saw that, the words ‘sudo piggyback!’ sprang to mind. But wait, the brew docs say installing into /usr/local is safe, look at the screenshot at the top of this post from their FAQ, which says

3. It’s safe
Apple has left this directory for us. Which means there is no /usr/local directory by default, so there is no need to worry about messing up existing tools.

Ah, wrong kind of safe. Here, we’re not concerned with ‘messing up’ existing tools, but spoofing system tools that live further down the path search hierarchy behind the user’s back. Also, what the brew docs fail to mention is that although Apple may have “left this directory for us”, they didn’t intend for you to change its ownership and make it writable by just anything in userland. Other 3rd party software plays correctly with usr/local and doesn’t change its ownership permissions.

Why does brew do this? According to the docs, they want to avoid using sudo because of the security flaws it contains (it’s true, sudo does have security issues); unfortunately, the proposed solution is far worse and creates a far bigger security hole.

The brew docs seem to be unaware of the danger, however, only noting that:

If you need to run Homebrew in a multi-user environment, consider creating a separate user account especially for use of Homebrew.

But that just isn’t going to cut it. We’re not worried about other users, but processes running as our user that can now attempt to elevate their own privileges by stealing the admin user’s password.

How’s that possible?

To understand the crucial error being foisted upon Homebrew users here you need to understand a little about the program search path on macOS and other unix variant operating systems.

This is basically a list of directories that the shell environment uses to find programs. It’s a convenience so that no matter what directory you’re in in the shell, you can execute commands without having to specify the full path to them. This is why you can type, say, uptime in any directory and the program will run instead of having to type /usr/bin/uptime.

The program search path hierarchy is saved in a variable called PATH. You can see its value by typing echo $PATH at the command prompt:

/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

A more reader friendly version is output by doing cat /etc/paths:

/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

The order is ‘first come first served’; in other words, when you type a command on the command line, the shell will look for it first in the first path in the list. If it doesn’t find it there, it will move to the next path in the list and so on. However, and here’s the crucial bit, it will stop at the first hit, and execute that command.

As you can see from the above, /usr/local/bin occurs before the other directories, which means it gets searched first. So, if you had an executable in there called uptime that didn’t do what the normal uptime does, but say, advanced your clock by 1hr, then when you (or anyone else on the system) typed uptime in the command line, instead of getting the output of how long the system has been booted, you’d get your clock going forward an hour. The system doesn’t know which uptime you (or any other user) intended if you don’t specify the full path; it just executes the first uptime it finds in the program search path.

If you’re still thinking “so….???”, let me add two more little spicy notes into this melting pot:

i. Since Homebrew changes the permissions on /usr/local/bin to the user (see the preceding screenshot), the user (or any process running as the user) is able to write files to it and give those files executable permissions.

ii. sudo is a program that lives in /usr/bin, the path that is after (Danger! Danger!/usr/local/bin. Now if you (or someone else, or some other program) were to place a program called sudo in /usr/local/bin, then every time you typed sudo it would be that program that would be executed, not the real one.

Hopefully the picture is becoming clearer now, and I apologise if I’ve laboured the point for those of you that saw it right away, but this is worth being clear about. This hypothetical sudo program could easily capture your password before passing on your commands to the real sudo and you’d be none the wiser (until, of course, the malicious actor behind it chose to use your password for their own amusement or benefit!).

Oh, did I say ‘hypothetical’? Well, here’s a short video of me actually doing it in my VM (yes, folks, I know you don’t need sudo to execute uptime, it’s just an example; the command could be anything, such as sudo mkdir -p /Library/...):

Sure enough, I was able to use a simple script to steal the user’s password. In this case, an admin password, but it could and would have been the password of whoever is set as the owner of /usr/local/bin as a result of Homebrew’s recommended installation. Even for non-admin users this is a worry as the login password of course allows full access to the user’s Login Keychain.

Eh, run that by me again / tl;dr.

Installing Homebrew as recommended means that from then on, any process or application you launch can write anything it wants into the first directory that gets searched for command line binaries, change its mode to execute and give it the same name as a system binary. It will then run instead of the system binary whenever you type the program with the same name in the command line (unless you type the full path to it). The potential for exploitation is vast. Few people if any ever type the full path to workaday binaries like lsfindcatsudo and many others. And as shown in my example, any of these could be hijacked to perform different operations thanks to the way Homebrew is installed. This can be done and cleaned up in such a way that you’d never know it had happened.

What can you do about it?

My advice is if you’re running Homebrew from /usr/local/bin you should

i. Uninstall Homebrew; follow the instruction here under ‘How do I uninstall Homebrew?’ This will remove all your installed packages.

ii. Reset the permissions of /usr/local/bin back to ‘wheel’.
sudo chown root:wheel /usr/local/bin

iii. Reinstall Homebrew and choose a location within your home folder.

iv. You should probably change your login password just to be on the safe side.

Above all, stay safe folks! 🙂

About philastokes

Independent Software Developer, Technical Writer and Researcher at SentinelOne. Explaining the unexplainable with images, video and text. Scripting anything imaginable in AppleScript, Bash, Python and Swift.

Posted on March 21, 2018, in Security-2. Bookmark the permalink. 11 Comments.

  1. Rafiki Technology

    Neat article! Question, how do you decide on the new location for brew? By home folder, do you mean where the “cd” commands takes me back to?

  2. Rafiki Technology

    Also, doesn’t changing the owner of /usr/local/bin mess up all the other applications that had a user other than root as the owner?

    • It shouldn’t do. You’re only changing the permissions of who can write into the folder. That’s the vulnerability – brew makes it user, group writable. chown’ing it back stops that, but it’ll break brew for sure, because they need that to avoid asking you for sudo when it installs new packages.

      • Rafiki Technology

        I understand. Thanks, philstokes!

        On different note: I was going to repost your article on my blog, but OpenDNS is blocking your site probably because of this article and the command words in it… or “Due to a phishing threat” to use their notification verbiage. You should maybe reach out to them to make sure they whitelist your site.

  3. Wouldn’t you see the same problem with `~/homebrew/bin/sudo` if it’s in the path ahead of the system locations?

    Seems simple enough for a baddie to start at the front of $PATH and try writing to each directory in turn. That would pick up `/usr/local/bin` or `~/homebrew/bin` or anything else people have set up.

    • Yes and no. The difference is that /usr/local/bin is also system-wide, i.e, affects all other users, too; however, you’re right that having anything in your $PATH ahead of the system bins is a no-no unless it’s also root:wheel protected.

  4. The most straightforward solution is to make a wrapper which will temporary change permissions for running brew.
    For example /usr/loca/bin/brw

    “””
    #!/bin/bash

    /usr/bin/sudo chown $(whoami):admin /usr/local/bin &&\
    $(which brew) $1 $2 &&\
    /usr/bin/sudo chown root:wheel /usr/local/bin
    “””

    and run it
    brw install PACKAGE
    or
    brw uninstall PACKAGE

Leave a comment