
Казалось бы, учетная запись в GitLab — не самая удачная стартовая точка для инфраструктурного пентеста. Ни VPN в корпоративную сеть, ни доменного аккаунта, ни даже RDP на рабочую станцию, только креды рядового разработчика. Вряд ли в начале проекта заказчик ожидал серьезного импакта, но мы доказали, что при типовых настройках CI/CD такая учетка находится на расстоянии нескольких прыжков до контроля над облаком.
Давайте вместе пройдем эту цепочку.
Начальная точка: учетка в GitLab
Итак, на старте у нас была только учетная запись разработчика в GitLab. Мы зашли в систему, создали тестовый проект и добавили в него .gitlab-ci.yml — файл, который описывает логику пайплайна.

.gitlab-ci.ymlСконфигурировали файл так, чтобы выполнить на раннере базовые команды разведки и попробовать достучаться до внешнего вебхука на нашем сервере.
Прежде всего нужно было понять, сможем ли мы вообще запускать произвольные сборки на раннерах и, если да, то с какими правами. Результат не заставил себя ждать:

После нескольких тестовых сборок мы пришли к следующим выводам:
Мы действительно можем запускать произвольные команды на раннере
PROD-K8S-RUNNER01.Раннер успешно достучался до нашего внешнего вебхука.
Код на раннере выполняется от имени пользователя
gitlab_runner.Пользователь
gitlab_runnerвходит в группуdocker.
Вывод команды id:
uid=999(gitlab-runner) gid=999(gitlab-runner) groups=999(gitlab-runner),112(docker)
Последний факт — крупная удача, так как членство в группе docker в Linux‑окружении дает прямой путь повышения привилегий до root. Дело в том, что сокет Docker (/var/run/docker.sock) принадлежит к этой группе, и любой ее член может запускать контейнеры с любыми параметрами namespaces. Поэтому формально у нас права рядового раннера, а фактически — заявка на весь хост.
От gitlab_runner к root
Самый надежный способ повысить привилегии в такой ситуации — запустить контейнер с монтированной файловой системой хоста и добавить наш SSH‑ключ в /root/.ssh/authorized_keys.
Проблема в том, что команды через .gitlab-ci.yml выполняются неинтерактивно, в рамках одного пайплайна. Это создает определенные сложности, да и хотелось побыстрее уйти от выполнения кода через сборки. Поэтому мы предварительно развернули на раннере C2-маяк, он связался с нашим управляющим сервером, а дальше мы работали уже по C2-каналу:
docker run --rm -v /:/host alpine sh -c "echo 'ssh-rsa AAAAB3NzaC1yc2E...' >> /host/root/.ssh/authorized_keys"

Пара минут, и мы заходим на PROD-K8S-RUNNER01 как root (добраться до 22-го TCP‑порта помог тот же C2-канал).

Лутинг на хосте
Мы не стали спешить с запуском скриптов для автоматического сбора секретов и сначала вручную осмотрели переменные окружения (env), домашние директории пользователей и обычные места хранения конфигов Kubernetes.
В домашней директории одного из пользователей лежал файл config — обычный kubeconfig с токеном пользователя CICD для кластера prod-cluster-k8s.

Сразу проверили, есть ли на хосте kubectl, и, найдя искомое, выполнили проверку прав:
kubectl --kubeconfig=config auth can-i --list
В вывод попали:
secretsсgetиlist, которые позволяют чтение или изменение секретов;pods/execсcreate, позволяющие выполнять команды внутри подов;pods/portforwardиservices/proxyсcreate, позволяющие пробрасывать трафик во внутренние сервисы и обходить сетевые политики;bindingsсcreate— позволяет связывать роль с группой или сервисным аккаунтом. Тривиальный способ получитьcluster-admin.
По сути, этот набор прав дает полный контроль над кластером.
Переход на ноду K8s
Чтобы перейти к исследованию нод кластера, мы создали манифест и подняли под с примонтированной файловой системой хоста.

Зайдя в под, получили доступ к файловой системе ноды PROD-K8S01-MS1-MAIN-1.

Проверяя директорию /etc/kubernetes, мы обнаружили то, что искали — некий cloud-config. Анализ конфига показал, что речь идет об OpenStack облаке.

Переход в облако
Подготовив клиент OpenStack, мы выполнили команды:
openstack server list openstack network list openstack user list

И в результате перечислили роли полученной учетной записи.

Роли admin в списке нет, но в данном случае это не имеет значения: набор ролей учетки дает настолько широкие права, что де‑факто это администратор облака.
Имя роли |
Возможности роли |
Импакт |
|---|---|---|
|
Полное управление инстансами ВМ (создание, удаление, остановка, перезагрузка, изменение флейвора, доступ к консоли) |
Можно поднять свою ВМ, украсть данные, запустить майнер, атаковать соседей по облаку |
|
Управление дисками (томами) и снимками |
Позволяет красть и уничтожать данные, подключать чужие тома, читать закрытую информацию |
|
Работа с образами ОС |
Загрузить бэкдорированный образ, подменить существующий, распространить вредоносное ПО |
|
Управление сетями, подсетями, роутерами, security groups |
Полный контроль сетевой изоляции, перехват трафика, открытие доступа извне, подмена DNS |
|
Управление балансировщиками нагрузки |
Перенаправить трафик на себя, снять нагрузку с целевых серверов (DoS) |
|
Работа с оркестрацией (стэки) |
Задеплоить вредоносную инфраструктуру одной командой, используя доверие платформы |
|
Управление кластерами Kubernetes в OpenStack |
Создать свой кластер для майнинга, украсть учетные данные от других кластеров, атаковать cloud‑контроллер |
|
Скорее всего, роль администратора в управляемых кластерах K8s |
Позволяет управлять кластерами Kubernetes заказчика, которые работают на этом OpenStack |
|
Управление пулами ресурсов |
Узнать, где какие ресурсы лежат, влиять на планировщик |
Итоги
Полная цепочка компрометации умещается в пару строк: учетка разработчика в GitLab → запуск сборки на раннере → группа docker → root на хосте → kubeconfig → доступ к Kubernetes → повторное монтирование ноды через под → учетные данные OpenStack → полный контроль над облаком.
Использование ошибок в настройках, которые по отдельности кажутся незначительными, приводит к компрометации инфраструктуры безо всяких сложных эксплоитов. Если внутри не проведен полноценный аудит безопасности и сервисы доверяют друг другу по умолчанию, стартовая точка почти не важна. Опытный хакер найдет способ эскалировать привилегии до прав администратора практически из любой точки.
Что с этим делать на практике:
держать CI/CD‑раннеры вне группы docker и без доступа к хостовому Docker‑сокету в изоляции, по возможности rootless;
не хранить рабочие kubeconfig'и и облачные конфиги на раннерах и хостах;
ограничивать права сервисных токенов (
CICDи подобных) по RBAC до минимума и делать их короткоживущими;запрещать привилегированные поды и hostPath политиками кластера;
выдавать узлам облачные учетки строго под задачу и регулярно аудировать роли.

PURP — Telegram-канал, где кибербезопасность раскрывается с обеих сторон баррикад
t.me/purp_sec — инсайды и инсайты из мира этичного хакинга и бизнес‑ориентированной защиты от специалистов Бастиона