
Exploit em C para CVE-2021-3560, um desvio de autenticação no polkit permitindo que usuários não privilegiados criem uma conta privilegiada via DBus, com uma análise técnica detalhada.
um exploit confiável em C para CVE-2021-3560.
Ontem me deparei com este post no blog de Kevin Backhouse (descobridor desta vulnerabilidade), tentei os comandos bash fornecidos no post e, para minha surpresa, funcionou na minha máquina Kali Linux!
CVE-2021-3560 é uma bypass de autenticação no polkit, que permite que um usuário não privilegiado chame métodos privilegiados usando DBus. O PoC explora esse bug para chamar 2 métodos privilegiados fornecidos pelo accountsservice (CreateUser e SetPassword), o que nos permite criar um usuário privilegiado e depois definir uma senha para ele.
O polkit verifica se o chamador está autorizado a chamar tal método, fazendo isso primeiro verificando o ID de usuário do chamador; se for zero, assume-se que o chamador é root e a ação é permitida sem solicitar autenticação; caso contrário, solicita a senha do usuário.
A função polkit_system_bus_name_get_creds_sync() invoca 2 métodos para obter o UID e PID do chamador: GetConnectionUnixUser e GetConnectionUnixProcessID. O resultado dessas chamadas é escrito na struct data do tipo AsyncGetBusNameCredsData (esta struct é inicializada com 0) pela função de callback on_retrieved_unix_uid_pid(), e polkit_system_bus_name_get_creds_sync() bloqueia enquanto espera que a função de callback defina um erro ou o 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);
a função de callback on_retrieved_unix_uid_pid() é invocada após cada chamada de método para recuperar a resposta (UID e PID) ou definir um erro. Esta função chama g_dbus_connection_call_finish() para recuperar a resposta; se um erro ocorrer, então define data.caught_error como TRUE e retorna (data.uid e data.pid permanecem 0). Caso contrário, atribui o valor recuperado (UID ou PID) a data.uid ou data.pid (dependendo de qual valor foi recuperado) e retorna.
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;
}
}
}
Os métodos GetConnectionUnixUser e GetConnectionUnixProcessID retornarão o UID e PID se encontrados (o processo chamador ainda está conectado ao barramento), ou um erro se ocorrer um erro (por exemplo: processo chamador morto).
Uma vez que data.uid e data.pid são definidos ou data.caught_error é definido, a função polkit_system_bus_name_get_creds_sync() continua e é aqui que a vulnerabilidade existe: polkit_system_bus_name_get_creds_sync() não retorna um erro se data.caught_error está definido; em vez disso, define o valor que está em data.uid para out_uid e retorna TRUE (mesmo se data.caught_error estiver definido). out_pid é um ponteiro para uma variável guint32 passado para polkit_system_bus_name_get_creds_sync() quando chamado por polkit_system_bus_name_get_user_sync():
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;
exploração: