zsh scripts and Root Escalations

There was an update for the Support.app this week which fixed a CVE. There is an argument to be had about whether this CVE deserves its high rating, but it is worth discussing the underlying issue and presenting some solutions. (Erik Gomez has some great comments on Mac Admins Slack.)

zsh is far more configurable than most other shells, most certainly more configurable than the aging bash 3.2 that comes with macOS. One aspect of being more configurable is that it has multiple different configuration files that are loaded at different times when the shell starts. The most common configuration file you will have encountered is ~/.zshrc which is loaded for all interactive shells.

Another, less commonly used, configuration file, ~/.zshenv (and its global sibling /etc/zshenv) is loaded every time a zsh process launches, including script launches. That means, when you (or something else in the system) launches a script with a #!/bin/zsh shebang, the zsh process will read the files /etc/zshenv and ~/.zshenv when they exist, and execute their code.

Update 2024-03-22: some links for further reading:

The Setup

We can see this in action.

First, create a simple zsh “hello.sh” type script with this content:

#!/bin/zsh
echo "Hello, $(whoami)"

Make the script file executable (chmod +x hello.sh) and run it. You should see output like

% ./hello.sh
Hello, armin

where armin is replaced with your current username.

Next, create a new file ~/.zshenv (~/ means at the root of your home directory) with your favored text editor and add the following line:

echo "zshenv: as $(whoami) called from $0"

If you already have a ~/.zshenv you will want to rename it for now so we don’t modify that. (mv ~/.zshenv ~/.zshenv_old)

Note that the configuration file neither has a shebang, nor does it need to be executable.

When you open a new Terminal window, you should see the line

zshenv: as armin called from -zsh

among the other output at the top of the Terminal window, since ~/.zshenv is read and evaluated every time a zsh process starts. The shebang at the beginning of the script ensures a new zsh environment is created for it, even when your interactive shell is not zsh.

When you run hello.sh you will see that ~/.zshenv is executed as well:

% ./hello.sh
zshenv: as armin called from /bin/zsh
Hello, armin

So far, so good. This is how zshenv is supposed to work. It’s purpose is to contain environment variables and other settings that apply to all processes on the system. On macOS this is undermined by the fact that most apps and process are not started from a shell, so they don’t see environment variables set anywhere in the zsh (or another shell’s) configuration files, so .zshenv is rarely used. .zshrc is far more useful.

But now we come to escalation aspect: run hello.sh with root privileges using sudo:

% sudo ./hello.sh
zshenv: as root called from /bin/zsh
Hello, root

We see that both our script and ~/.zshenv are run with root privileges. Again, that is how sudo and .zshenv are supposed to work.

Since the zshenv configuration files are evaluated every time a zsh is launched, it is far more likely that they will eventually run with elevated privileges, than the other configuration files. Other shells, like bash and sh have no equivalent configuration file that gets run on every launch.

The Escalation

The potential danger is the following: a process only needs user privileges to create or modify ~/.zshenv. A malicious attacker can inject code into your .zshenv, wait patiently for a script with a zsh shebang to be run with root privileges, and then execute some malicious code with root privileges on your system.

This can also occur with preinstall or postinstall scripts with a zsh shebang in installation package files (pkg files) when the installation is initiated by the user. A malicious attacker could scan your system for apps where they know the installer package contains zsh scripts, which makes the chance that eventually a zsh script is run with root permissions quite certain. Then all they have to do is be patient and wait…

As root escalations go, this is not a very efficient one. The attacker is at the mercy of the user to wait when they execute a zsh script with escalated root privileges. On some Macs, that may very well be never.

This also only works when the user can gain administrative privileges. Standard users cannot use sudo to gain root privileges, or run installation packages.

There are more effective means of escalating from user to root privileges on macOS. Most easily by asking the user directly for the password with a dialog that appears benign. However, if you are a frequent user or author of zsh scripts, it is important to be aware.

From what I can tell so far, this does not affect scripts launched from non-user contexts, such as scripts from Munki, Jamf Pro or other management solutions, though there may be odd, unexpected edge cases.

As an organization, you can monitor changes to all shell configuration files with a security tool (such as Jamf Protect). There are more shenanigans an attacker could achieve by modifying these files. There are, however, many legit reasons to change these files, so most changes will not indicate an attack or intrusion. Nevertheless, the information could be an important puzzle piece in combination with other behaviors, when putting together the progress or pattern of an intrusion.

Update 2024-03-22: Matteo Bolognini has added a custom analytic for Jamf Protect to detect changes to the zshenv file.

Configuration file protection

This weakness is fairly straightforward to protect against. First, since the attack requires modification of ~/.zshenv, you can protect that file.

If you did not have a ~/.zshenv, create an empty file in its place or remove the line of code from our sample .zshenv before. Then apply the following flag:

% sudo chflags schg ~/.zshenv

This command sets the system immutable flag (schg, ‘system change’), which makes it require root privileges to modify, rename or delete the file.

If you want to later modify this file, you can unlock it with

% sudo chflags noschg ~/.zshenv

Just remember to protect it again when you are done.

Script protection

Since most Mac users will never open terminal and never touch any shell configuration file, relying on protecting the configuration file is not sufficient. However, it is quite easy to protect the zsh scripts you create from this kind of abuse.

One solution is to use a $!/bin/sh shebang instead of #!/bin/zsh. Posix sh does not have a configuration file which gets loaded on every launch, so this problem does not exist for sh.

If you are using zsh features that are not part of the POSIX sh standard you will need to change the script. In my opinion, installation scripts that are too complex to use POSIX sh, are attempting to do too much and should be simplified, anyway. So, this can also work as an indicator for “good” installation scripts.

In addition, installer packages might be used in situations where zsh is not available. Installation tools that install package files when the Mac is booted to Recovery (where there is no zsh) are used far less than they used to, but it is still possible and making your installer pkgs resilient to all use cases is generally a good choice.

I believe for installation package scripts, using an sh shebang is a good recommendation.

Some scripts popular with Mac Admins, like Installomator, do not usually run from installer packages, but are regularly run with root privileges. For these cases, (or for installation scripts, where cannot or do not want to use sh) you can protect from the above escalation, by adding the --no-rcs option to the shebang:

#!/bin/zsh --no-rcs
echo "Hello, $(whoami)"

zsh’s --no-rcs option suppresses the launch of user level configuration files. The rc stands for ‘Run Command’ files and is the same rc that appears in zshrc or bashrc or several other unix configuration files.

That allows you to keep using zsh and zsh features, but still have safe scripts. This is the solution that Support.app and the latest version of Nudge have implemented. I have also created a PR for Installomator, which should be merged in the next release.

Conclusion

Modifying ~/.zshenv is not a very effective means of gaining root privileges, but it is something developers and Mac admins that create zsh script that may be run with root privileges should be aware of. Switch to an sh shebang or add --no-rcs to the zsh shebang of scripts that might be run with root privileges to protect.

Published by

ab

Mac Admin, Consultant, and Author

5 thoughts on “zsh scripts and Root Escalations”

  1. Since I’m not finding anyone discussing or looking at the possibility, and since macOS doesn’t have a /etc/zshenv file by default, could we add it and include a check to confirm whether the ~/.zshenv files should run?
    In theory it should be easier to manage than locking down the ~/.zshenv file. (being the only suggestion from a device management perspective I’ve seen).

    1. It already requires root/admin privileges to create or modify /etc/zshenv so sneaking in code doesn’t gain you anything.

      I‘d also like to point out that don’t recommend locking ~/.zshenv and a managed approach. Messing with user config files is often more trouble than it is worth. In a managed environment you need to audit the scripts running through your management system

      1. I agree, I figure something like this in /etc/zshenv should be enough…
        if [[ -o rcs ]] && [ “$0” = “-zsh” ]; then
        unsetopt rcs
        fi

        And if you want scripts started from the command line to still trigger the ~/.zsh* files, add ‘ && [ “$( ps -o ppid=,command $( ps -o ppid= $$ ) | grep -c “-zsh” )” = 0 ]’ before the semicolon (;).

        I am however still testing to see what might work best.

  2. Nuts, there is supposed to be a \ (windows slash) before the – in grep -c “-zsh”, but it seems the comment dropped it, like quotes also becoming natural quotes.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.