Bash Privileged-Mode Vulnerabilities in Parallels Desktop and CDPATH Handling in MacOS
April 06, 2023 | Reno RobertIn the last few years, we have seen multiple vulnerabilities in Parallels Desktop leading to virtual machine escapes. Interested readers can check our previous blog posts about vulnerabilities across interfaces such as RDPMC hypercalls, the Parallels ToolGate, and the VGA virtual device. This post explores another set of issues we received last year - local privilege escalations through setuid root binaries.
Parallels Desktop has a couple of setuid binaries: prl_update_helper
and Parallels Service
. Both binaries run with root privileges and both invoke bash scripts to run commands with the privileges of root. For such use cases, bash specifically provides a privileged mode using the “-p” flag. Parallels Desktop prior to version 18.1.0 does not take advantage of bash privileged mode, nor does it filter untrusted environment variables. This leads to local privilege escalation.
In the case of Parallels Desktop, the setuid binaries use the setuid() system call to set the real user identifier to that of the effective user identifier. The problem with this implementation is that sensitive environment variables such as BASH_ENV, ENV, SHELLOPTS, BASHOPTS, CDPATH, GLOBIGNORE, and other shell functions are processed by bash. This is because bash is not aware of the setuid or setgid execution and trusts its environment. A local unprivileged user with control over environment variables can exploit this bug to execute code with the privileges of root.
The Bash Privileged Mode
Bash shell drops privileges when started with the effective user identifier not equal to that of the real user identifier. The effective user identifier is reset by setting it to the value of the real user identifier. The same is also applicable for group identifiers. In privileged mode, bash does not drop the effective privileges and ignores sensitive variables and shell functions from the environment. Here’s the relevant source code in bash that can be found in shell.c
file:
The functions of interest here are uidget
and disable_priv_mode
. The uidget
function sets running_setuid
if bash is launched from a setuid/setgid
process. Later in the code, if privileged mode is not specified, the setuid
and setgid
calls are used to drop privileges to that of the real identifiers:
Note that, since the Bourne shell sh
is linked to bash in macOS, the Apple bash code for invoking disable_priv_mode
is slightly different from that of the upstream version. Interested readers can search for the __APPLE__
macro to narrow down changes made to the upstream version of bash by Apple.
The other functions of interest in the bash startup code are run_startup_files
and shell_initialize
, as they handle information passed through the untrusted environment variables. When privileged mode is not specified, these functions provide at least a couple of generic ways to exploit the vulnerability. To begin, the BASH_ENV is an environment variable specifying a path to a shell script that will be executed by bash during a non-interactive start-up. One can set up an arbitrary startup script to be executed by bash running without privileged mode. Shown below is the code snippet of run_startup_files
in shell.c
:
A second approach is by using bash shell functions. When commands are executed in bash without an absolute path, it is possible to hijack those commands by exporting shell functions having the same name as that of the command being executed. This is possible even when the PATH environment variable is set to trusted paths. The corresponding source code can be found across shell.c
and variables.c
files:
Knowing this, let’s take a look at some of the privileged mode bugs in Parallels Desktop and their exploitation.
CVE-2023-27322 - Local Privilege Escalation Through Parallels Service
This bug was submitted by Grisha Levit and is also identified as ZDI-23-216. Parallels Service
forks a child process and executes an embedded script using a non-interactive bash shell invoked as /bin/bash -s
. The parent process writes the embedded script through a pipe to the child process running the bash shell. Before invoking the bash shell, Parallels Service calls setuid(0)
to set the real user identifier to the effective user identifier (root). Here is the relevant code snippet from the executable in Parallels Desktop version 17.1.4:
The execv
function is a wrapper around execve
, which fetches the environment using _NSGetEnviron() and passes it to execve
. Therefore, the bash shell spawned as a child process has access to all the environment variables set by the user who launched Parallels Service
, who may be an unprivileged user. Interestingly, the execution of an embedded shell script turned out to be not immediately vulnerable. This is because Parallels Service
also has the setgid
bit set and there is no corresponding call setgid(getegid())
as there was for the uid. Because of this, the real group identifier is not equal to that of the effective group identifier when bash is invoked. In such cases, bash identifies this as setgid
execution, drops group privileges, and does not trust the environment. However, any further subshell launched from this bash shell will also have all the environment variables as well as the privileges of the parent shell, which is running as root and has the group privileges set after the call to disable_priv_mode
. Considering this, the next interesting target is the watchdog
script invoked from the embedded script as seen below:
The watchdog
script uses /bin/bash as shebang and does not use privileged mode:
In this instance, bash trusts the environment. Because of this, the watchdog script can be exploited to gain root either by using the BASH_ENV
environment variable or by exporting shell functions. Here is an example of exploitation using BASH_ENV
:
To exploit using shell functions, we must identify a command to hijack. The watchdog script uses the echo
command for printing some debug messages:
A shell function with the same name can be exported such that the malicious function is executed instead of the expected echo
command. Note that exporting functions is a feature of bash. We must therefore use the bash shell to export the target function instead of using the default zsh shell in macOS.
This issue was fixed in Parallels Desktop 18.1.0 by adding the “-p” flag, indicating privileged mode, to the shebang interpreter directive:
CVE-2023-27324 and CVE-2023-27325 - Local Privilege Escalation Through Parallels Updater
The next two bugs were found in the Parallels Updater prl_update_helper
binary. These bugs were submitted by the researcher known as kn32 and are also identified as ZDI-23-218 and ZDI-23-219. In the case of CVE-2023-27324, the prl_update_helper
binary invokes a bash script named inittool
without setting privileged mode:
Before invoking the inittool
script, the real user identifier is set to that of the effective user identifier, which is root. This means bash will run as root and will trust its execution environment, which can lead to local privilege escalation.
This vulnerability can be exploited by using the BASH_ENV
environment variable or by exporting the shell function for the dirname
command.
The next bug (CVE-2023-27325) in the Parallels Updater affects the inittool2
executable invoked from the inittool
script. Like the Parallels Service
, inittool2
forks a child process and executes an embedded script using a non-interactive bash shell invoked as /bin/bash -s
. Exploitation is similar to that of CVE-2023-27324. In this case, the rm
command can be hijacked to execute arbitrary code as root. Below is the embedded script from Parallels Desktop version 17.1.4:
Both CVE-2023-27324 and CVE-2023-27325 were fixed in Parallels Desktop 18.1.0 by clearing the environment during the call to posix_spawn. Instead of passing the environ
array to the child process, the envp
argument is now provided with a pointer to a NULL array during the call to posix_spawn
. Below is the patch diff between 17.1.4 and 18.1.0:
Additionally, the privileged mode flag “-p” is also added to the shebang interpreter directive of the inittool
script as well as the embedded script within inittool2
. Note that the shebang of the embedded script is ignored since it is explicitly run using the bash interpreter.
CDPATH Handling in MacOS
During the analysis of these submissions, we also observed some differences in the way Apple bash handles “privileged mode” as compared to the upstream bash. Apple’s bash in macOS 13.0.1 is based on GNU Bash 3.2:
The upstream bash in privileged mode ignores many variables such as SHELLOPTS, BASHOPTS, CDPATH, and GLOBIGNORE as mentioned below:
Based on the CHANGES, here is a timeline of various changes related to privileged mode. Parsing of SHELLOPTS was ignored starting from bash-2.02-alpha1 and therefore ignored in version 3.2 too.
BASHOPTS was introduced at a later stage in bash-4.1-alpha and therefore not applicable to version 3.2.
The CDPATH and GLOBIGNORE variables were ignored only since bash-4.0-beta and therefore still get processed in Apple’s bash, which is based on version 3.2.
The CDPATH environment variable can be set to a colon-separated list of directories, which can then be used as a directory root by the built-in “cd” command instead of the current working directory (CWD). In the case of Apple’s bash, if a bash script executed through a setuid wrapper uses “cd [Absolute Path to Trusted Directory]” to change the CWD and further uses “cd subdirectory” to change the CWD, the later cd command with the relative path can be hijacked to a location controlled by an attacker by setting the CDPATH variable. Consider the sample code below:
Here is the outcome as tested in Ubuntu:
It is seen that the CDPATH environment variable is ignored in bash privileged mode (-p
). While repeating the same in macOS, it is honored.
This can become problematic when a script is written assuming bash privileged mode behavior in macOS to be the same as that of the upstream version. You may note the duplicated /tmp/secure
line. This is not a typo. It comes from the POSIX standard. If CDPATH is used for a directory change, the new directory path is echoed to stdout, which is the first line /tmp/secure
. The second line comes from the pwd
command.
Here is the comparison of builtins/cd.def
which handles CDPATH in Apple bash versus the upstream version:
Similarly, differences in GLOBIGNORE handling can be seen by diffing variables.c
source file:
Since macOS Catalina, zsh is used as the default shell. The official announcement for the same can be found here. Bash is deprecated on macOS and likely exists only for backward compatibility. For any bash scripts executed through a setuid wrapper, one must ensure privileged mode “-p” is enabled. In addition to that, beware of the differences in privileged mode between Apple bash and the upstream version. This is specifically noticed in the handling of the CDPATH and GLOBIGNORE environment variables.
Conclusion
Parallels Desktop is a popular target for researchers. We’ve already published seven advisories in the product in 2023 to go along with the 10 we published in 2022. With Parallels Desktop being one of the major virtualization solutions used in macOS, it’s understandable why it can be an enticing target for threat actors. My research into Parallels continues, and I’ll blog about any significant findings in the future. Of course, if you find similar vulnerabilities, we’d be interested in seeing those as well.
Until then, you can follow me @renorobertr and follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.