
Analyse approfondie de CVE-2021-3560, une vulnérabilité d'élévation de privilèges locale dans PolKit sous Linux. Comprend l'analyse de la cause racine, les mécanismes de l'exploit et une démonstration de la condition de course menant à l'accès root.
[toc]
Identifiant de vulnérabilité : CVE-2021-3560
Score de vulnérabilité :
Produit vulnérable : linux PolKit (polkitd)
Périmètre impacté : introduit à partir du code source 0.113 ; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
Conditions d'exploitation : linux local ; dbus + polkitd présents
Effet de l'exploitation : élévation de privilèges locale
Obtention du code source :
apt source accountsserviceapt source dbusL'image Ubuntu par défaut ubuntu-20.04.2 peut être répliquée : http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
Regardons d'abord le code en cause
/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;
}
Dans la fonction polkit_system_bus_name_get_creds_sync , celle-ci effectue l'authentification de certaines requêtes (nous détaillerons le scénario logique plus loin). Elle appelle successivement les méthodes GetConnectionUnixUser et GetConnectionUnixProcessID via le bus org.freedesktop.DBus . Ces deux méthodes, fournies par le bus org.freedesktop.DBus , récupèrent le PID et l'UID de l'utilisateur du processus demandeur, puis les transmettent à la fonction de rappel on_retrieved_unix_uid_pid pour traiter les données retournées. Ensuite, elle bloque en attendant la fin de l'exécution du processus côté bus.
On peut voir dans la fonction on_retrieved_unix_uid_pid que, selon le résultat renvoyé par le bus, elle définit si une erreur s'est produite (error ?) puis positionne le drapeau d'erreur. Si l'exécution réussit, elle retourne normalement l'UID de l'utilisateur ou le PID du processus. Mais dans la fonction polkit_system_bus_name_get_creds_sync , après avoir bloqué en attendant le retour du bus (la condition de traitement terminé est que l'UID et le PID soient tous deux traités ou qu'une erreur se soit produite). Cela signifie qu'il y a logiquement trois résultats possibles du côté bus : récupération de l'UID 0 (utilisateur privilégié) ; récupération d'un UID non nul (utilisateur normal) ; erreur. Mais dans le traitement ultérieur, il n'y a pas de gestion d'erreur : on définit directement out_uid (destiné à l'étage supérieur) avec la valeur data.uid retournée par le bus, puis on met directement 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?
Mais on oublie un cas : l'exécution anormale du côté du bus DBUS, qui amène la fonction de rappel à positionner le drapeau d'erreur et à revenir sans traiter les données data. Comme data est initialisé à 0, cela conduit ensuite à définir out_uid à 0, c'est-à-dire utilisateur privilégié.
Alors, où est utilisée cette fonction défectueuse polkit_system_bus_name_get_creds_sync ?
Le processus vulnérable est le processus polkitd :
polkit est un ensemble d'outils au niveau applicatif qui, en définissant et en auditant des règles d'autorisation, permet la communication entre processus de différentes priorités : la décision de contrôle est centralisée dans un cadre unifié, déterminant si un processus de basse priorité a le droit d'accéder à un processus de haute priorité.
En bref, il s'agit d'un processus root s'exécutant en arrière‑plan qui, via la communication inter‑processus, effectue un jugement d'autorisation lorsque d'autres processus de faible privilège accèdent aux fonctionnalités fournies par des processus de haut privilège. Comme attacher ce processus avec gdb le fait redémarrer, nous devons analyser le chemin de déclenchement de la fonction vulnérable à partir du code source :
polkitd utilise massivement le framework de communication inter‑processus gio DBUS. Vous pouvez consulter le manuel au préalable pour vous familiariser avec certaines fonctions importantes.
Analysons maintenant le chemin de déclenchement de la vulnérabilité dans polkitd :
D'abord le 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); //循环启动
··· ···
··· ···
}