Another fun exploit! This time with local privilege escalation through Apple’s PackageKit.framework when running ZSH-based PKGs 🎉.


  • Resolved versions:
    • macOS 14.5 Beta 2 (23F5059e) and newer
    • macOS 13.6.7 (22G720) and newer
    • macOS 12.7.5 (21H1222) and newer
  • Affected versions:
    • macOS 14.5 Beta 1 (23F5049f) and older
    • macOS 13.6.6 (22G630) and older
    • macOS 12.7.4 (21H1123) and older
    • Any version of macOS 11 or older
  • Proof of Concept:
  • CVE Associated: CVE-2024-27822
  • Compensation: None

  • Terminology Used:
    • PKG: macOS package file (.pkg, contains install scripts, and files to install on the system.)
    • Shebang: #! at the start of a script to specify the path to the interpreter to use.
    • ZSH: The default shell interpreter in macOS (others include sh and bash)
    • MDM: Mobile Device Management.
    • Munki: An open source macOS software deployment tool.

Vulnerability Details

To put it simply: Apple’s Installer.app/PackageKit.framework spawns installation scripts embedded in PKGs as root in the current user’s enviroment. This results in the shebangs having their own special logic kick in, such as #!/bin/zsh loading the user’s .zshenv file while running with root permissions.

This allows for a malicious payload to be inserted into the .zshenv file, which would then be executed as root when a ZSH-based PKG was installed by the user.

  • Note: While the majority of PKGs do run as root, some PKGs may run as the current user, such as WebEx.

This vulnerability could primarily be targeted at user-initiated PKG installations. MDM (Mobile Device Management) and Munki-based installations are not affected by this vulnerability, as they are spawned under the root user’s context.

The main attack scenario with this vulnerability is that a logic bomb-based payload could simply wait for the user to install a ZSH-based PKG at any time, and then execute the payload and gain root access.

  • Fun fact: This vulnerability looks to be an extension of CVE-2021-30892: Shrootless which abused the enviroment file in a similar manner.

Proof of Concept

To demonstrate this vulnerability, all you need is a PKG with an installation script that has the #!/bin/zsh shebang. On script execution (ie. PKG installation), the script will load the user’s .zshenv file, which could have malicious code embedded in it.

Steps to Reproduce:

  1. Inject a malicious payload into the .zshenv file:
/usr/bin/osascript -e "display dialog \"Hello from malware!

Current user: $(/usr/bin/whoami)
UID:  $UID
EUID: $EUID\""
  1. Install a PKG with the #!/bin/zsh shebang. Ex. Generic-ZSH.pkg

  2. Watch the magic happen!

An automated PoC is available in pkg_exploit.py, which will create a dummy PKG and inject a payload into the .zshenv file.

Vulnerability Discovery

On March 14th, 2024, I had noticed a new CVE pop up on the MacAdmins Slack: CVE-2024-27301

As discussed earlier, this vulnerability is based on the #!/bin/zsh shebang in a PKG’s scripts.

When learning about this, I had assumed this is an inherent issue with using shells as shebangs, and so developers need to be more cautious about how they’re invoked (ie. ensure untrusted settings are never loaded).

Internal Audit

After reviewing the scripts used in our internal PKGs, we appended the --no-rcs parameter to the ZSH shebang. This prevents the scripts from loading the current user’s environment file, thus preventing the vulnerability.

We then reviewed each of our vendors’ PKGs. One of them, Watchman Monitoring, had installers which were vulnerable to this exploit. We reported this to the vendor, and Allen Hancock/Watchman Monitoring suggested that we to report this to Apple while they worked on the issue. We reported to Apple on the same day, March 14th, and Apple assigned it OE1972568773511.

  • Watchman Monitoring published revised installers on April 5th in version 7.1.3.101.

Surprise macOS 14.5 release

On May 15th I got a message congratulating me on a CVE? I was so confused, but checked the macOS 14.5 security notes and there I was:

PackageKit
  Available for: macOS Sonoma
  Impact: An app may be able to gain root privileges
  Description: A logic issue was addressed with improved restrictions.
  CVE-2024-27822: Scott Johnson, Mykola Grymalyuk of RIPEDA Consulting, Jordy Witteman, and Carlos Polop

While I had no idea other researchers reported this vulnerability, I feel a bit odd being credited when in reality I only noticed another CVE and had a vendor request a report to Apple. Additionally since the initial report in March, I had not heard back from Apple on the status of the report.

However, I’ll take what I can get, and genuinely appreciate Apple crediting me instead of filing “duplicate” and moving on.

Timeline

Sender Topic Date
RIPEDA Discovery of ZSH-based PKG vulnerability. March 14th, 2024
RIPEDA Initial report to Watchman Monitoring. March 14th, 2024
Watchman Triaged, recommend reporting to Apple. March 14th, 2024
RIPEDA Initial report to Apple. March 14th, 2024
Apple Assigned OE1972568773511. March 14th, 2024
Watchman Revised shebangs. April 5th, 2024
Apple macOS 14.5 Beta 2 seeded to developers. April 16th, 2024
Apple macOS 14.5 released to the public. May 13th, 2024
Apple Approval for public disclosure. May 28th, 2024

Reverse Engineering PackageKit: Apple’s Fix

Now here comes the fun part: How did Apple fix this vulnerability?

To start, let’s load up PackageKit in Hopper:

/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/PackageKit

The first thing we’ll notice is that Apple added a new environment variable to PackageKit: APPLE_PKGKIT_ESCALATING_ROOT. This is used to determine if the package will be running as root or not, and is currently only referenced in two functions:

-[PKInstallDaemon startListeningForConnectionsToService:]
-[PKRunPackageScriptInstallOperation _scriptTaskEnvironmentForPackage:destination:scriptName:]

1. startListeningForConnectionsToService

The first function is the main listener for the XPC service, and checks if the service is com.apple.installd.user. If it is not, it will attempt to set the APPLE_PKGKIT_ESCALATING_ROOT environment variable to 1:

-(void)startListeningForConnectionsToService:(NSString *)process {
  /// ...
  if ([process isEqualToString:@"com.apple.installd.user"] == 0x0) {
    setenv("APPLE_PKGKIT_ESCALATING_ROOT", "1", 1);
  }
  /// ...
}

2. _scriptTaskEnvironmentForPackage

The second function simply passes the environment variable to the script’s environment, for end use:

-(int)_scriptTaskEnvironmentForPackage:(int)arg2 destination:(int)arg3 scriptName:(int)arg4 {
  /// ...
  char *rootEnv = getenv("APPLE_PKGKIT_ESCALATING_ROOT");
  if (rootEnv != NULL) {
    NSString *rootEnvStr = [NSString stringWithUTF8String:rootEnv];
    [environment setObject:rootEnvStr forKey:@"APPLE_PKGKIT_ESCALATING_ROOT"];
  }
  /// ...
}

Wait, this doesn’t do anything…

Correct! The actual fix is inside of /bin/zsh.

3. /bin/zsh

If we load this binary up in Hopper, we notice that _run_init_scripts() now checks for the environment variable APPLE_PKGKIT_ESCALATING_ROOT. If it is set, it will skip loading the user’s .zshenv file:

int _run_init_scripts(int arg0, int arg1) {
  /// ...
  if (getenv("APPLE_PKGKIT_ESCALATING_ROOT") == 0x0) {
    _sourcehome(".zshenv");
  }
  /// ...
}

Yup, the fix is as simple as that!

4. /bin/bash (Bonus!)

/bin/bash wasn’t left out either! A similar check was added in the binary’s EntryPoint:

Developer Recommendations

Something for developers to keep in mind is that they should still ensure their working environment in scripts is clean and not loading untrusted settings. This is especially important for scripts that run as root, like in PKGs, as we can’t always rely on the operating system to protect us.

Ways to prevent loading untrusted settings:

  • SH: #!/bin/sh (sh in macOS won’t load any external settings without being explicitly told)
  • ZSH: #!/bin/zsh --no-rcs
  • Bash: #!/bin/bash --noprofile --norc

Additionally, always specify the full path to binaries in scripts when possible, as PATH may not always be trusted and could point to a malicious binary.

  • ex. /bin/mkdir instead of mkdir
    • Use where <command> to find the full path to a command
    • ex. where osascript will return /usr/bin/osascript

Poor script:

function makeDirectory() {
  mkdir $1
}

Better script:

function makeDirectory() {
  /bin/mkdir $1
}

Would highly recommend reading Apple’s old documentation on shell scripting guidelines for more information:

Conclusion

A simple exploit, but really interesting to see how Apple mitigated it through PackageKit/ZSH and Bash. This does however pose a concern for packages that don’t use the standard shell shebangs, such as !#/usr/bin/perl, as they could still load untrusted settings if written poorly.

Otherwise for those wanting more PKG-based vulnerabilities, highly recommend reading the following:

And thank you again to Allen Hancock / Watchman Monitoring for getting us to report this to Apple 🎉