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.