I am writing about a dead simple and reliable sandbox escape exploit which only have one line of code. Yeah I am sure it's an exploit, not just PoC. It has nothing to do with iOS.
The bug was refactored (or killed) before the beta release of Mojave. The latest vulnerable version is macOS High Sierra 10.13.6 (17G65).
Since it's part of a browser exploit chain you'll need a renderer exploit to gain shellcode execution first. If not, disable SIP so you can debug, attach lldb to a running com.apple.WebKit.WebContent.xpc and use the following command:
This line will generate a new plist under ~/Library/LaunchAgents. With the proper arguments you can launch a Calculator or anything you like after re-logging into system.
What's going on here? Isn't that supposed to be blocked by the sandbox?
The bug
The key is that there's a TOCTOU in cfprefsd that you don't even need to race.
There are Preferences Utilities in CoreFoundation for reading and saving plist serialized preferences on macOS.
- https://developer.apple.com/documentation/corefoundation/1515497-cfpreferencescopyappvalue?language=objc
- https://developer.apple.com/documentation/corefoundation/1515528-cfpreferencessetappvalue?language=objc
Most 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/Preferencesfor root privileged processes~/Library/Preferencesfor normal, un-containerized process~/Library/Containers/{bundle_id}/Data/Library/Preferencesfor containerized apps from mac App Store
The applicationID can also be an absolute path, just like the given one-liner. But it's supposed to perform permission check on the parameter. Why did it even work?
These functions internally invoke XPC with cfprefsd, as implemented in CoreFoundation. The daemon surely will check the sandbox state and permission of the incoming message with sandbox_check.
Unfortunately there used to be an implementation error. It mistakenly caches the result of sandbox_check from the beginning, and keep trusting it during the whole session.
The vulnerable function:
The client 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.
PoC
Minimal test case here.
The following CopyAppValue request obviously will fail:
The console output 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:
If a process has accessed CFPreferences utilities before it goes into sandbox state, there will be no further sandbox check.
Study on WebKit
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)
WebKit 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:
After the initialization, it calls ChildProcess::initializeSandbox to enter sandbox and load untrusted content.
So 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:
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 of code (make it clear in case of the Criticism for the title).
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 now so I use the old one.
Demo
This bug affects at least from El Capitan to High Sierra (10.13.6), or maybe even earlier version. No CVE assigned but I think it's worth branding.
