See part one of this two-part blog posting: Rootpipe Reborn (Part I)
There’s a general bug type on macOS.
When a privileged (or loosely sandboxed) user space process accepts an IPC message from an unprivileged or sandboxed client, it decides whether the operation is valid by enforcing code signature (bundle id, authority or entitlements). If such security check is based on process id, it can be bypassed via pid reuse attack.
An unprivileged client can send an IPC message, then spawns an entitled process to reuse current pid. The privileged service will then validate on the new process and accept the previous IPC request, leading to privilege escalation or even sandbox escape. The attacker can stably win the race by spawning multiple child processes to fill up the message queue.
Security checks based on pid, like sandbox_check
, SecTaskCreateWithPID
suffer from this attack.
The idea and the initial PoC was borrowed from Ian Beer:
Samuel Groß has also been aware of this scenario:
“Don’t Trust the PID! Stories of a simple logic bug and where to find it“
“Pwn2Own: Safari sandbox part 2 — Wrap your way around to root“
Put another way, the IPC server should never use xpc_connection_get_pid
or [NSXPCConnection processIdentifier]
to check the validity of incoming client. It should use the audit_token_t
instead (note: there was an exception).
Unfortunately these functions are undocumented and private:
xpc_connection_get_audit_token
[NSXPCConnection auditToken]
For more details on utilizing these methods, see the previous Objective-See blog post, “Reversing to Engineer: Learning to ‘Secure’ XPC from a Patch“
Since, as noted, these methods are private, third-party developers are trapped in this issue repeatedly:
Apple please consider opening these functions to developers!
Oh wait. Actually audit_token_t
was not so trustworthy. @5aelo has just pointed out another bug before iOS 12.2 / macOS 10.14.4: Issue 1757: “XNU: pidversion increment during execve is unsafe” 🤦♂
CVE-2019-8565
)The privileged XPC service com.apple.appleseed.fbahelperd
has exported the following interface:
@protocol FBAPrivilegedDaemon <NSObject>
- (void)copyLogFiles:(NSDictionary *)arg1;
- (void)gatherInstallLogsWithDestination:(NSURL *)arg1;
- (void)gatherSyslogsWithDestination:(NSURL *)arg1;
- (void)sampleProcessWithPID:(unsigned long long)arg1 withDestination:(NSURL *)arg2;
- (void)runMDSDiagnoseWithDestination:(NSURL *)arg1;
- (void)runTMDiagnoseWithDestination:(NSURL *)arg1;
- (void)runBluetoothDiagnoseWithDestination:(NSURL *)arg1 shortUserName:(NSString *)arg2;
- (void)runWifiDiagnoseWithDestination:(NSURL *)arg1;
- (void)runSysdiagnoseWithDestination:(NSURL *)arg1 arguments:(NSArray *)arg2;
- (void)runSysdiagnoseWithDestination:(NSURL *)arg1;
- (void)runMobilityReportWithDestination:(NSURL *)arg1;
- (void)runSystemProfileWithDetailLevel:(NSString *)arg1 destination:(NSURL *)arg2;
- (void)stopDaemon;
- (void)showPrivileges;
- (void)performReadyCheck;
@end
Look at the implementation of -[FBAPrivilegedDaemon listener:shouldAcceptNewConnection:]
method. It only allows XPC messages from one client: /System/Library/CoreServices/Applications/Feedback Assistant.app/Contents/MacOS/Feedback Assistant
:
But since it performs the security check based on process id, we can bypass it.
You can now refer to the proof of concept by Ian Beer or see my full exploit at the end.
The steps to trigger the race condition are as follows:
Create multiple client processes via posix_spawn
or NSTask
Better not to use fork because Objective-C runtime may crash if you call it between fork and exec, which is required by this attack. On 10.13 you can add a OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
environment variable before process creation or add a __DATA,__objc_fork_ok
section to your executable as a workaround. But these workarounds are not compatible with previous macOS. For more information, please refer to “Objective-C and fork() in macOS 10.13 “
Send multiple XPC messages to the server to block the message queue
Ian Beer uses execve
to replace the binary to a trusted one and write to its buffer to prevent the new process from terminating. Instead, I chose to pass these flags POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED
to posix_spawn
to create a suspended child process and reuse the pid of the parent
Since the child process has been replaced, there won’t be any callback. You have to use a “canary” to detect whether the race is successful based on the server’s behavior, e.g., the existence of a newly created file:
As can be seen in the above image, the check has been passed!
Now continue code audit on FBAPrivilegedDaemon
.
The method copyLogFiles:
accepts one NSDictionary
argument, whose keys as the sources and the correspond NSString
as destination to perform file copy. It supports multiple tasks at once, and the path can be both directory or file. However, note that the source path must starts with one of the specific directories:
Specifically, the destination must match the rule:
The source must start with /Library/Logs
or /var/log
, and the destination must match one the following patterns:
^\/var\/folders
^\/private\/var\/
^\/tmp
Library\/Caches\/com\.apple\.appleseed\.FeedbackAssistant
Also, it will not override an existing destination.
These constraints can be bypassed by path traversal. So now we can copy arbitrary file or folder to anywhere unless rootless protected.
Additionally, after each copy, it will call -[FBAPrivilegedDaemon fixPermissionsOfURL:recursively:]
to set the copied files’ owner to the XPC client process’s gid
and uid
. This is extremely ideal for macOS LPE challenges. I used this zero day exploit during #35C3 CTF to simply copy the flag and read it. LOL.
If you don’t mind a reboot, getting root privilege is simple. Copy the executable to the places that will be automatically launched with privilege during startup. For example, the bundles in /Library/DirectoryServices/PlugIns
will be loaded by the process /usr/libexec/dspluginhelperd
, who has root privilege and is not sandboxed.
Can we have an instant trigger solution?
Since it will never override existing file, we can not:
override administrator account’s password digest (/var/db/dslocal/nodes/Default/users) ❌
override suid binaries (not to mention file permission and rootless) ❌
override one of the PrivilegedHelpers ❌
And it will fix file permissions, none of these would work:
add sudoer ❌
add an entry to /Library/LaunchDaemons to register a new XPC service ❌
We need more primitives.
The daemon has other methods named run*diagnoseWithDestination
:
They are various external command wrappers just like those diagnose helpers mentioned from my previous post. What’s interesting is that runTMDiagnoseWithDestination:
acts the same as timemachinehelper
, thus we can trigger the CVE-2019-8513
command injection.
At first I was looking at runMDSDiagnoseWithDestination:
, which launches /usr/bin/mddiagnose
that will finally spawn /usr/local/bin/ddt
after around 10 seconds, waiting for the /usr/bin/top
command to end. Remember the previous post? This location does not exist by default and we can put custom executable with the arbitrary file copy bug.
Another exploit path is method runMobilityReportWithDestination:
. It invokes this shell script: /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/Resources/get-mobility-info
The script checks for the existence of /usr/local/bin/netdiagnose
. If found, the script is executed as root. The exploit will success within milliseconds.
By the way, I was surprised by how many diagnostic tools depending on the non-existing directory /usr/local/bin
:
The bug has been fixed in macOS 10.14.4 and iOS 12.2:
However, you can still exploit macOS 10.14.3 or lower. Grab the source for my POC from here.
It’ll give you a root shell within milliseconds!
If you enjoyed this blog post and are interested in similar macOS/iOS security topics, check out our upcoming Mac Security Conference:
“Objective by the Sea” v2.0 (Europe, June 1st-2nd, 2019).