Прим. перев.: авторы этой статьи в подробностях рассказывают о том, как им удалось обнаружить уязвимость CVE-2020–8555 в Kubernetes. Хотя изначально она и выглядела не очень опасной, в сочетании с другими факторами её критичность у некоторых облачных провайдеров оказалась максимальной. За проведённую работу специалистов щедро вознаградили сразу несколько организаций.
Кто мы такие
Мы — два французских исследователя в области безопасности, которые совместно обнаружили уязвимость в Kubernetes. Нас зовут Brice Augras и Christophe Hauquiert, но на многих Bug Bounty-платформах мы известны как Reeverzax и Hach соответственно:
Эта статья — наш способ рассказать о том, как рядовой исследовательский проект неожиданно превратился в самое увлекательное приключение в жизни охотников за багами (по крайней мере, на данный момент).
Как вам, наверное, известно, у охотников за багами есть пара примечательных особенностей:
они живут на пиццах и пиве;
они работают тогда, когда все остальные спят.
Мы не исключение из этих правил: обычно встречаемся в выходные дни и проводим бессонные хакерские ночи. Но одна из таких ночей закончилась весьма необычно.
Изначально мы собирались встретиться, чтобы обсудить участие в CTF на следующий день. Во время беседы о безопасности Kubernetes в управляемой сервисной среде вспомнили о старой идее SSRF (Server-Side Request Forgery) и решили попробовать использовать ее в качестве сценария атаки.
В 11 вечера сели за исследования, а спать отправились рано утром, весьма удовлетворенные результатами. Именно из-за этих исследований мы наткнулись на программу MSRC Bug Bounty и придумали эксплойт с эскалацией привилегий.
Прошло несколько недель/месяцев, и наш неожиданный результат позволил получить одну из самых высоких наград в истории Azure Cloud Bug Bounty — в дополнение к той, которую мы получили от Kubernetes!
По мотивам нашего исследовательского проекта комитет Kubernetes Product Security Committee опубликовал CVE-2020–8555.
Теперь хотелось бы как можно больше распространить информацию о найденной уязвимости. Надеемся, вы оцените находку и поделитесь техническими подробностями с другими членами infosec-сообщества!
Итак, вот наша история…
Контекст
Чтобы максимально полно довести смысл произошедшего, давайте сначала рассмотрим, как Kubernetes работает в условиях облачной управляемой среды.
Когда вы создаете экземпляр кластера Kubernetes в такой среде, за работу управляющего слоя обычно отвечает поставщик облачных услуг:
Управляющий слой располагается в периметре облачного провайдера, в то время как узлы Kubernetes находятся в периметре клиента
Для динамического выделения томов используется механизм их динамического предоставления из внешнего storage-бэкенда и сопоставления с PVC (persistent volume claim, т.е. запросом на том).
Таким образом, после того, как PVC создан и привязан к StorageClass’у в кластере K8s, дальнейшие действия по предоставлению тома берет на себя kube/cloud controller manager (его точное название зависит от релиза). (Прим. перев.: Подробнее про CCM на примере его реализации для одного из облачных провайдеров мы уже писали здесь.)
Существует несколько разновидностей provisioner’ов, поддерживаемых Kubernetes: большинство из них включены в ядро оркестратора, а другие управляются дополнительными provisioner’ами, которые размещаются в pod’ах в кластере.
В своем исследовании мы сфокусировались на внутреннем механизме предоставления томов, который проиллюстрирован ниже:
Динамическое предоставление томов с использованием встроенного provisioner’а Kubernetes
Если вкратце, когда Kubernetes развернут в управляемой среде, за работу controller manager’а отвечает поставщик облачных услуг, но запрос на создание тома (номер 3 на схеме выше) покидает пределы внутренней сети облачного провайдера. И вот тут-то ситуация становится по-настоящему интересной!
Сценарий взлома
В этом разделе мы расскажем, как воспользовались упомянутым выше рабочим процессом и получили доступ ко внутренним ресурсам поставщика облачных услуг. Кроме того, будет показано, как можно осуществить определенные действия — например, получить внутренние учетные данные или провести эскалацию привилегий.
Одна простая манипуляция (в данном случае это Service Side Request Forgery) помогла выйти за пределы клиентской среды в кластерах различных поставщиков услуг по управляемому K8s.
В своих исследованиях мы сосредоточились на provisioner’е GlusterFS. Несмотря на то, что дальнейшая последовательность действий описана в таком контексте, этой же уязвимости подвержены Quobyte, StorageOS и ScaleIO.
Во время анализа класса хранилищ GlusterFS в исходниках клиента на Golang мы заметили, что при первом HTTP-запросе (3), отправленном во время создания тома, к концу пользовательского URL в параметре resturl добавляется /volumes.
Избавиться от этого дополнительного пути мы решили добавлением # в параметр resturl. Вот первая YAML-конфигурация, которую мы использовали для проверки на наличие «полуслепой» SSRF-уязвимости (подробнее про semi-blind или half-blind SSRF можно прочитать, например, здесь — прим. перев.):
Затем для удаленного управления кластером Kubernetes воспользовались бинарником kubectl. Как правило, облачные провайдеры (Azure, Google, AWS и т д.) позволяют получить учетные данные для их использования в этой утилите.
Благодаря этому и получилось применить свой «особенный» файл. Kube-controller-manager выполнил результирующий HTTP-запрос:
kubectl create -f sc-poc.yaml
Ответ с точки зрения атакующего
Вскоре после этого мы также смогли получить HTTP-ответ от целевого сервера — через команды describe pvc или get events в kubectl. И действительно: этот драйвер Kubernetes по умолчанию слишком многословен в своих предупреждениях/сообщениях об ошибках…
Вот пример с ссылкой на https://www.google.fr, установленной в качестве параметра resturl:
kubectl describe pvc poc-ssrf
# или же можете воспользоваться kubectl get events
В рамках такого подхода мы были ограничены запросами типа HTTP POST и не могли получить содержимое тела ответа, если возвращаемый код был 201. Поэтому решили провести дополнительные исследования и расширили этой сценарий взлома новыми подходами.
Эволюция наших исследований
Продвинутый сценарий №1: использование 302-го редиректа со внешнего сервера для изменения метода HTTP, чтобы получить более гибкий способ сбора внутренних данных.
Продвинутый сценарий №2: автоматизация сканирования LAN и обнаружения внутренних ресурсов.
Продвинутый сценарий №3: использование HTTP CRLF + smuggling («контрабанды» запросов) для создания адаптированных HTTP-запросов и получения данных, извлеченных из логов kube-controller’а.
Технические спецификации
В исследованиях использовался Azure Kubernetes Service (AKS) с Kubernetes версии 1.12 в регионе North Europe.
Описанные выше сценарии выполнялись на последних релизах Kubernetes за исключением третьего сценария, т.к. ему требовался Kubernetes, собранный с Golang версии ≤ 1.12.
Внешний сервер атакующего — https://attacker.com.
Продвинутый сценарий №1: редирект HTTP-запроса POST в GET и получение конфиденциальных данных
Изначальный способ был улучшен конфигурацией сервера злоумышленника на возврат 302 HTTP Retcode, чтобы конвертировать POST-запрос в GET-запрос (шаг 4 на схеме):
Первый запрос (3), исходящий от клиента GlusterFS (Controller Manager), имеет тип POST. Выполнив следующие шаги, мы смогли превратить его в GET:
В качестве параметра resturl в StorageClass указывается http://attacker.com/redirect.php.
Endpoint https://attacker.com/redirect.php отвечает статус-кодом 302 HTTP со следующим Location Header’ом: http://169.254.169.254. Это может быть любой другой внутренний ресурс — в данном случае redirect-ссылка используется исключительно в качестве примера.
По умолчанию библиотека net/http Golang’а перенаправляет запрос и конвертирует POST в GET с 302-м статус-кодом, в результате чего на целевой ресурс поступает HTTP-запрос GET.
Чтобы прочитать тело HTTP-ответа, нужно сделать describe объекта PVC:
kubectl describe pvc xxx
Вот пример HTTP-ответа в формате JSON, который нам удалось получить:
Возможности найденной уязвимости на тот момент были ограничены из-за следующих моментов:
Невозможность вставить HTTP-заголовки в исходящий запрос.
Невозможность выполнять POST-запрос с параметрами в теле (так удобно запрашивать значение ключа у экземпляра etcd, работающего на 2379 порту, если используется незашифрованный HTTP).
Невозможность получить содержимое тела ответа, когда статус-код был равен 200 и ответ не имел JSON Content-Type.
Продвинутый сценарий №2: сканирование локальной сети
Этот метод half-blind SSRF затем использовался для сканирования внутренней сети поставщика облачных услуг и опроса различных слушающих сервисов (экземпляр Metadata, Kubelet, etcd и т.д.) на основе ответов kube controller’а.
Сперва были определены стандартные слушающие порты компонентов Kubernetes (8443, 10250, 10251 и т.д.), а затем пришлось автоматизировать процесс сканирования.
Видя, что данный способ сканирования ресурсов очень специфичен и не совместим с классическими сканерами и SSRF-инструментами, мы решили создать собственные worker’ы в bash-скрипте, которые автоматизируют весь процесс.
Например, чтобы быстрее просканировать диапазон 172.16.0.0/12 внутренней сети, параллельно запускались 15 worker’ов. Вышеуказанный диапазон IP был выбран исключительно в качестве примера и может быть изменен на IP-диапазон конкретного поставщика услуг.
Чтобы просканировать один IP-адрес и один порт, необходимо сделать следующее:
Если в дополнение к этому провайдер предлагал клиентам старые версии кластера K8s и открывал им доступ к логам kube-controller-manager’а, эффект становился еще значительнее.
Злоумышленнику действительно гораздо удобнее менять по своему усмотрению HTTP-запросы, предназначенные для получения полного HTTP-ответа.
Для реализации последнего сценария должны были выполняться следующие условия:
Пользователь должен иметь доступ к логам kube-controller-manager (как, например, в Azure LogInsights).
Кластер Kubernetes должен использовать версию Golang ниже 1.12.
Мы развернули локальное окружение, имитирующее обмен данными между Go-клиентом GlusterFS и поддельным целевым сервером (пока воздержимся от публикации PoC).
Была обнаружена уязвимость, затрагивающая версии Golang ниже 1.12 и позволявшая хакерам проводить атаки типа HTTP smuggling/CRLF.
Объединив описанную выше half-blind SSRF вместе с этой, мы смогли посылать запросы по своему вкусу, включая замену заголовков, метода HTTP, параметров и данных, которые kube-controller-manager затем обрабатывал.
Вот пример рабочей «наживки» в параметре resturl StorageClass’а, которая реализует подобный сценарий атаки:
В результате возникает ошибка unsolicited response, сообщение о которой записывается в логи контроллера. Благодаря включенной по умолчанию «многословности» туда же сохраняется и содержимое ответного HTTP-сообщения.
Это была наша самая результативная «наживка» в рамках proof of concept.
Используя такой подход, мы смогли провести некоторые из следующих атак в кластерах различных поставщиков managed k8s: эскалация привилегий с получением учетных данных на metadata-инстансах, DoS мастера с помощью (незашифрованных) HTTP-запросов на мастер-экземплярах etcd и т.п.
Последствия
В официальном заявлении Kubernetes по поводу обнаруженной нами SSRF-уязвимости ей был присвоен рейтинг CVSS 6.3/10: CVSS:3.0/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N. Если рассматривать только уязвимость, связанную с периметром Kubernetes, вектор целостности (integrity vector) в ней квалифицируется как None.
Однако оценка возможных последствий в контексте управляемого сервисного окружения (и это была самая интересная часть нашего исследования!) побудила нас переквалифицировать уязвимость на рейтинг Critical CVSS10/10 для многих дистрибьюторов.
Ниже приведена дополнительная информация, которая поможет понять, чем мы руководствовались при оценке возможных последствий в облачных окружениях:
Целостность
Удаленное выполнение команд с помощью полученных внутренних учетных данных.
Воспроизведение вышеописанного сценария методом IDOR (Insecure Direct Object Reference, т.е. небезопасных прямых ссылок на объекты) с другими ресурсами, обнаруженными в локальной сети.
Конфиденциальность
Атака типа Lateral Movement благодаря краже облачных учетных данных (например, metadata API).
Сбор информации с помощью сканирования локальной сети (определение версии SSH, версии HTTP-сервера, …).
Сбор информации об экземплярах и инфраструктуре путем опроса внутренних API, таких как metadata API (http://169.254.169.254, …).
Кража данных клиентов с помощью облачных учетных данных.
Доступность
Все сценарии применения эксплойтов, связанные с векторами атаки на integrity (целостность), могут быть использованы для разрушительных действий и приводить к тому, что мастер-инстансы из клиентского периметра (или любого другого) будут недоступны.
Поскольку мы находились в управляемой среде K8s и оценивали влияние на целостность, можно представить себе множество сценариев, способных повлиять на доступность. В качестве дополнительных примеров приведем повреждение базы данных etcd или выполнение критического вызова к API Kubernetes.
Хронология
6 декабря 2019 г.: отправка сообщения об обнаруженной уязвимости в MSRC Bug Bounty.
3 января 2020: третья сторона проинформировала разработчиков Kubernetes о том, что мы работаем над проблемой в области безопасности. И попросила их рассматривать SSRF как внутренняя (in-core) уязвимость. После этого мы представили общий отчет с техническими подробностями об источнике проблемы.
15 января 2020: мы предоставили разработчикам Kubernetes технический и общий отчеты по их запросу (через платформу HackerOne).
15 января 2020: разработчики Kubernetes известили нас, что half-blind SSRF + инъекция CRLF для прошлых релизов считается уязвимостью in-core. Мы сразу же прекратили анализ периметров других поставщиков услуг: первопричиной теперь занималась команда K8s.
15 января 2020: через HackerOne получено вознаграждение от MSRC.
16 января 2020: Kubernetes PSC (Product Security Committee) признал уязвимость и попросил держать ее в тайне до середины марта из-за большого числа потенциальных жертв.
11 февраля 2020: получено вознаграждение от Google VRP.
4 марта 2020: через HackerOne получено вознаграждение от Kubernetes.
15 марта 2020: первоначально запланированное публичное раскрытие отложено из-за ситуации с COVID-19.
1 июня 2020: совместное заявление Kubernetes + Microsoft об уязвимости.
TL;DR
Мы пьем пиво и кушаем пиццу 🙂
Мы обнаружили in-core-уязвимость в Kubernetes, хотя вовсе не собирались этого делать.
Мы провели дополнительный анализ в кластерах различных облачных провайдеров и смогли увеличить ущерб, причиняемый уязвимостью, чтобы получить дополнительные обалденные бонусы.
В этой статье вы найдете множество технических подробностей. Мы с радостью обсудим их с вами (Twitter: @ReeverZax & @__hach_).
Оказалось, что всевозможные формальности и составление отчетов занимают гораздо больше времени, чем ожидалось.