Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2021-3156-Heap-Based-Buffer-Overflow-in-Sudo-Baron-Samedit- — تحليل تقني وكود استغلال لـ CVE-2021-3156، وهو تجاوز سعة مخزن مؤقت قائم على الكومة (heap-based buffer overflow) في Sudo يسمح بتصعيد الامتيازات المحلية إلى الجذر دون مصادقة. | Kitploit
أدوات/GitHubGitHub/sornphut/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit-
تصعيد الامتيازاتتحليل الثغرات الأمنيةالاستغلالاختبار الاختراقالأوراق والأبحاثالتعلم والتعليماستغلال الملفات الثنائية
GitHubsornphut/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit-

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

CVE-2021-3156-Heap-Based-Buffer-Overflow-in-Sudo-Baron-Samedit-

تحليل تقني وكود استغلال لـ CVE-2021-3156، وهو تجاوز سعة مخزن مؤقت قائم على الكومة (heap-based buffer overflow) في Sudo يسمح بتصعيد الامتيازات المحلية إلى الجذر دون مصادقة.

عرض المستودع
منذ سنة واحدةلم تتم المراجعة بعد

Qualys Security Advisory

Baron Samedit: تجاوز سعة المخزن المؤقت في الكومة في Sudo (CVE-2021-3156)

======================================================================== المحتويات

الملخص التحليل الاستغلال الشكر الجدول الزمني

======================================================================== الملخص

اكتشفنا تجاوزًا لسعة المخزن المؤقت في الكومة في Sudo (https://www.sudo.ws/). هذه الثغرة:

  • قابلة للاستغلال من قبل أي مستخدم محلي (المستخدمون العاديون ومستخدمو النظام، sudoers وغير sudoers)، بدون مصادقة (أي أن المهاجم لا يحتاج لمعرفة كلمة مرور المستخدم)؛

  • تم إدخالها في يوليو 2011 (commit 8255ed69)، وتؤثر على جميع الإصدارات القديمة من 1.8.2 إلى 1.8.31p2 وجميع الإصدارات المستقرة من 1.9.0 إلى 1.9.5p1، في تكوينها الافتراضي.

قمنا بتطوير ثلاثة استغلالات مختلفة لهذه الثغرة، وحصلنا على صلاحيات الجذر الكاملة على Ubuntu 20.04 (Sudo 1.8.31)، وDebian 10 (Sudo 1.8.27)، وFedora 33 (Sudo 1.9.2). من المحتمل أن تكون أنظمة التشغيل والتوزيعات الأخرى قابلة للاستغلال أيضًا.

======================================================================== التحليل

إذا تم تنفيذ Sudo لتشغيل أمر في وضع "shell" (shell -c command):

  • إما عبر الخيار -s، الذي يضع علامة MODE_SHELL الخاصة بـ Sudo؛

  • أو عبر الخيار -i، الذي يضع علامتي MODE_SHELL و MODE_LOGIN_SHELL الخاصتين بـ Sudo؛

عندها، في بداية main() الخاصة بـ Sudo، تقوم parse_args() بإعادة كتابة argv (الأسطر 609-617)، عن طريق دمج جميع وسائط سطر الأوامر (الأسطر 587-595) وهروب جميع الأحرف الوصفية باستخدام الخطوط المائلة العكسية (الأسطر 590-591):


571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { 572 char **av, *cmnd = NULL; 573 int ac = 1; ... 581 cmnd = dst = reallocarray(NULL, cmnd_size, 2); ... 587 for (av = argv; *av != NULL; av++) { 588 for (src = *av; src != '\0'; src++) { 589 / quote potential meta characters */ 590 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$') 591 *dst++ = '\'; 592 *dst++ = *src; 593 } 594 dst++ = ' '; 595 } ... 600 ac += 2; / -c cmnd */ ... 603 av = reallocarray(NULL, ac + 1, sizeof(char *)); ... 609 av[0] = (char )user_details.shell; / plugin may override shell */ 610 if (cmnd != NULL) { 611 av[1] = "-c"; 612 av[2] = cmnd; 613 } 614 av[ac] = NULL; 615 616 argv = av; 617 argc = ac; 618 }

لاحقًا، في sudoers_policy_main()، تقوم set_cmnd() بدمج وسائط سطر الأوامر في مخزن مؤقت قائم على الكومة "user_args" (الأسطر 864-871) وتفكيك الأحرف الوصفية (الأسطر 866-867)، "لأغراض مطابقة sudoers وتسجيل الدخول":


819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { ... 852 for (size = 0, av = NewArgv + 1; *av; av++) 853 size += strlen(*av) + 1; 854 if (size == 0 || (user_args = malloc(size)) == NULL) { ... 857 } 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { ... 864 for (to = user_args, av = NewArgv + 1; (from = *av); av++) { 865 while (*from) { 866 if (from[0] == '\' && !isspace((unsigned char)from[1])) 867 from++; 868 *to++ = *from++; 869 } 870 *to++ = ' '; 871 } ... 884 } ... 886 }

لسوء الحظ، إذا انتهت وسيطة سطر أوامر بحرف خط مائل عكسي واحد، فعندها:

  • في السطر 866، "from[0]" هو حرف الخط المائل العكسي، و"from[1]" هو المنهي الصفري للوسيطة (أي ليس حرف مسافة)؛

  • في السطر 867، يتم زيادة "from" ويشير إلى المنهي الصفري؛

  • في السطر 868، يتم نسخ المنهي الصفري إلى المخزن المؤقت "user_args"، ويتم زيادة "from" مرة أخرى ويشير إلى الحرف الأول بعد المنهي الصفري (أي خارج حدود الوسيطة)؛

  • حلقة "while" في الأسطر 865-869 تقرأ وتنسخ أحرفًا خارج الحدود إلى المخزن المؤقت "user_args".

بمعنى آخر، set_cmnd() معرضة لتجاوز سعة المخزن المؤقت في الكومة، لأن الأحرف خارج الحدود التي يتم نسخها إلى المخزن المؤقت "user_args" لم تكن مدرجة في حجمه (المحسوب في الأسطر 852-853).

من الناحية النظرية، ومع ذلك، لا يمكن لأي وسيطة سطر أوامر أن تنتهي بحرف خط مائل عكسي واحد: إذا تم تعيين MODE_SHELL أو MODE_LOGIN_SHELL (السطر 858، شرط ضروري للوصول إلى الكود الضعيف)، فإن MODE_SHELL يتم تعيينها (السطر 571) وقد قامت parse_args() بالفعل بهروب جميع الأحرف الوصفية، بما في ذلك الخطوط المائلة العكسية (أي أنها هربت كل خط مائل عكسي بخط مائل عكسي ثانٍ).

من الناحية العملية، ومع ذلك، فإن الكود الضعيف في set_cmnd() وكود الهروب في parse_args() محاطان بشروط مختلفة قليلاً:


819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { ... 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

مقابل:


571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {

سؤالنا، إذن، هو: هل يمكننا تعيين MODE_SHELL وإما MODE_EDIT أو MODE_CHECK (للوصول إلى الكود الضعيف) ولكن ليس MODE_RUN الافتراضي (لتجنب كود الهروب)؟

الجواب، على ما يبدو، هو لا: إذا قمنا بتعيين MODE_EDIT (خيار -e، السطر 361) أو MODE_CHECK (خيار -l، السطران 423 و519)، فإن parse_args() تزيل MODE_SHELL من "valid_flags" (السطران 363 و424) وتخرج مع وجود خطأ إذا حددنا علامة غير صالحة مثل MODE_SHELL (الأسطر 532-533):


358 case 'e': ... 361 mode = MODE_EDIT; 362 sudo_settings[ARG_SUDOEDIT].value = "true"; 363 valid_flags = MODE_NONINTERACTIVE; 364 break; ... 416 case 'l': ... 423 mode = MODE_LIST; 424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST; 425 break; ... 518 if (argc > 0 && mode == MODE_LIST) 519 mode = MODE_CHECK; ... 532 if ((flags & valid_flags) != flags) 533 usage(1);

لكننا وجدنا ثغرة: إذا قمنا بتنفيذ Sudo كـ "sudoedit" بدلاً من "sudo"، فإن parse_args() تقوم تلقائيًا بتعيين MODE_EDIT (السطر 270) ولكنها لا تعيد تعيين "valid_flags"، وتتضمن "valid_flags" MODE_SHELL افتراضيًا (السطران 127 و249):


127 #define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL) ... 249 int valid_flags = DEFAULT_VALID_FLAGS; ... 267 proglen = strlen(progname); 268 if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 269 progname = "sudoedit"; 270 mode = MODE_EDIT; 271 sudo_settings[ARG_SUDOEDIT].value = "true"; 272 }

وبالتالي، إذا قمنا بتنفيذ "sudoedit -s"، فإننا نضبط كلاً من MODE_EDIT و MODE_SHELL (ولكن ليس MODE_RUN)، ونتجنب كود الهروب، ونصل إلى الكود الضعيف، ونحدث تجاوزًا لسعة المخزن المؤقت في الكومة "user_args" من خلال وسيطة سطر أوامر تنتهي بحرف خط مائل عكسي واحد:


sudoedit -s '' perl -e 'print "A" x 65536' malloc(): corrupted top size Aborted (core dumped)

من وجهة نظر المهاجم، هذا التجاوز في سعة المخزن المؤقت مثالي:

  • نتحكم في حجم المخزن المؤقت "user_args" الذي نحدث فيه التجاوز (حجم وسائط سطر الأوامر المدمجة لدينا، في الأسطر 852-854)؛

  • نتحكم بشكل مستقل في حجم ومحتويات التجاوز نفسه (وسيطة سطر الأوامر الأخيرة لدينا متبوعة بشكل ملائم بمتغيرات البيئة الأولى لدينا، والتي لم يتم تضمينها في حساب الحجم في الأسطر 852-853)؛

  • يمكننا حتى كتابة بايتات صفرية إلى المخزن المؤقت الذي نحدث فيه التجاوز (كل وسيطة سطر أوامر أو متغير بيئة ينتهي بخط مائل عكسي واحد يكتب بايتًا صفريًا إلى "user_args"، في الأسطر 866-868).

على سبيل المثال، على Linux amd64، يقوم الأمر التالي بتخصيص مخزن مؤقت "user_args" بحجم 24 بايت (قطعة كومة بحجم 32 بايت) ويستبدل حقل حجم القطعة التالية بـ "A=a\0B=b\0" (0x00623d4200613d41)، وحقل fd الخاص بها بـ "C=c\0D=d\0" (0x00643d4400633d43)، وحقل bk الخاص بها بـ "E=e\0F=f\0" (0x00663d4600653d45):


env -i 'AA=a' 'B=b' 'C=c' 'D=d' 'E=e' 'F=f' sudoedit -s '1234567890123456789012'

--|--------+--------+--------+--------|--------+--------+--------+--------+-- | | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.| --|--------+--------+--------+--------|--------+--------+--------+--------+-- size <---- user_args buffer ----> size fd bk

======================================================================== الاستغلال

لأن Sudo يستدعي وظائف الترجمة في بداية main() الخاصة به:


154 setlocale(LC_ALL, ""); 155 bindtextdomain(PACKAGE_NAME, LOCALEDIR); 156 textdomain(PACKAGE_NAME);

ويقوم بتمرير سلاسل الترجمة (من خلال وظيفة gettext() وماكرو _()) إلى وظائف تنسيق السلسلة مثل:


301 sudo_printf(SUDO_CONV_ERROR_MSG, _("%s is not in the sudoers " 302 "file. This incident will be reported.\n"), user_name);

أردنا في البداية إعادة استخدام تقنية halfdog الرائعة من https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ وتحويل تجاوز سعة المخزن المؤقت في الكومة الخاص بـ Sudo إلى استغلال تنسيق سلسلة. بتعبير أدق:

  • في السطر 154، في setlocale()، نقوم بـ malloc() وإطلاق العديد من متغيرات بيئة LC (LC_CTYPE، LC_MESSAGES، LC_TIME، إلخ)، مما يؤدي إلى إنشاء ثقوب صغيرة في بداية كومة Sudo (قطع حرة سريعة أو tcache)؛

  • في السطر 155، يقوم bindtextdomain() بـ malloc() لهيكل binding، والذي يحتوي على مؤشر dirname لاسم دليل يحتوي على ملفات كتالوج ".mo" وبالتالي سلاسل الترجمة؛

  • في set_cmnd()، نقوم بـ malloc() للمخزن المؤقت "user_args" في إحدى الثقوب في بداية كومة Sudo، ونحدث تجاوزًا في هذا المخزن المؤقت، وبالتالي نستبدل مؤشر dirname الخاص بهيكل binding؛

  • في السطر 301 (على سبيل المثال)، يقوم gettext() (من خلال ماكرو _()) بتحميل سلسلة الترجمة الخاصة بنا من dirname المستبدل -- بمعنى آخر، نتحكم في سلسلة التنسيق التي يتم تمريرها إلى sudo_printf().

لتنفيذ هذه التقنية الأولية، كتبنا أداة تخمين بدائية تنفذ Sudo داخل gdb، وتحدث تجاوزًا للمخزن المؤقت "user_args"، وتختار بشكل عشوائي المعلمات التالية:

  • متغيرات بيئة LC التي نمررها إلى Sudo، وطولها (نستخدم اللغة "C.UTF-8" ونضيف "@modifier" عشوائيًا)؛

  • حجم المخزن المؤقت "user_args" الذي نحدث فيه التجاوز؛

  • حجم التجاوز نفسه؛

  • ما إذا كنا نمر عبر كود مصادقة Sudo (خيار -A أو -n) أم لا (خيار -u #realuid).

لسوء الحظ، فشلت هذه التقنية الأولية؛ كانت أداة التخمين الخاصة بنا قادرة على استبدال مؤشر dirname الخاص بهيكل binding:


Program received signal SIGSEGV, Segmentation fault.

0x00007f6e0dde1ea9 in __dcigettext (domainname=domainname@entry=0x7f6e0d9cc020 "sudoers", msgid1=msgid1@entry=0x7f6e0d9cc014 "user NOT in sudoers", msgid2=msgid2@entry=0x0, plural=plural@entry=0, n=n@entry=0, category=5) at dcigettext.c:619

=> 0x7f6e0dde1ea9 <__dcigettext+1257>: cmpb $0x2f,(%rax)

rax 0x4141414141414141 4702111234474983745

لكن LC_MESSAGES كانت دائمًا اللغة الافتراضية "C" (وليس "C.UTF-8")، مما يعطل ترجمة السلسلة في gettext() (أي أن gettext() تُرجع سلسلة التنسيق الأصلية، وليس سلسلتنا الخاصة).

لحسن الحظ، مع ذلك، أنتجت أداة التخمين الخاصة بنا عشرات من أعطال Sudo الفريدة وbacktraces gdb؛ من بين هذه، لفتت ثلاث منها انتباهنا، وقمنا في النهاية باستغلال الثلاثة.

======================================================================== 1/ استبدال struct sudo_hook_entry

أول عطل لفت انتباهنا هو:


Program received signal SIGSEGV, Segmentation fault.

0x000056291a25d502 in process_hooks_getenv (name=name@entry=0x7f4a6d7dc046 "SYSTEMD_BYPASS_USERDB", value=value@entry=0x7ffc595cc240) at ../../src/hooks.c:108

=> 0x56291a25d502 <process_hooks_getenv+82>: callq *0x8(%rbx)

rbx 0x56291c1df2b0 94734565372592

0x56291c1df2b0: 0x4141414141414141 0x4141414141414141

بشكل لا يُصدق، تعطلت وظيفة Sudo process_hooks_getenv() (في السطر 108) لأننا استبدلنا مباشرة مؤشر دالة، getenv_fn (عضو في struct sudo_hook_entry القائم على الكومة):


99 int 100 process_hooks_getenv(const char *name, char **value) 101 { 102 struct sudo_hook_entry *hook; 103 char *val = NULL; ... 107 SLIST_FOREACH(hook, &sudo_hook_getenv_list, entries) { 108 rc = hook->u.getenv_fn(name, &val, hook->closure);

لاستغلال هذا الاستبدال لـ struct sudo_hook_entry، نلاحظ أن:

  • استدعاء getenv_fn (في السطر 108) متوافق مع استدعاء execve():

    . name ("SYSTEMD_BYPASS_USERDB") متوافق مع وسيطة مسار execve()؛

    . &val (مؤشر لمؤشر NULL) متوافق مع argv الخاص بـ execve()؛

    . hook->closure (مؤشر NULL) متوافق مع envp الخاص بـ execve()؛

  • يمكننا هزيمة ASLR عن طريق استبدال مؤشر الدالة جزئيًا getenv_fn (الذي يشير إلى الدالة sudoers_hook_getenv() في المكتبة المشتركة sudoers.so)؛ ولحسن الحظ، تحتوي بداية sudoers.so على استدعاء لـ execve() (أو execv()):


0000000000008a00 execv@plt: 8a00: f3 0f 1e fa endbr64 8a04: f2 ff 25 65 55 05 00 bnd jmpq *0x55565(%rip) # 5df70 <execv@GLIBC_2.2.5> 8a0b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

  • يمكننا قراءة /dev/kmsg (dmesg) كمستخدم غير مميز على Ubuntu، وبالتالي الحصول على معلومات مفصلة حول أعطال Sudo لدينا.

وبالتالي، نتبنى الاستراتيجية التالية:

  • أولاً، نقوم بتخمين معاملات الاستغلال حتى نستبدل getenv_fn بعنوان مستخدم غير صالح (فوق 0x800000000000) -- حتى نلاحظ خطأ حماية عام في موقع استدعاء getenv_fn:

sudoedit[15904] general protection fault ip:55e9b645b502 sp:7ffe53d6fa40 error:0 in sudo[55e9b644e000+1a000] ^^^

  • بعد ذلك، نعيد استخدام معاملات الاستغلال هذه ولكن نستبدل getenv_fn بنمط منتظم من عناوين مستخدم صالحة (أقل من 0x800000000000) ولكن غير معمّرة -- في هذا المثال، getenv_fn هو المؤشر الثاني والعشرون الذي نستبدله (0x32 هو '2'، جزء من نمطنا):

sudoedit[15906]: segfault at 323230303030 ip 0000323230303030 sp 00007ffeeabf2868 error 14 in sudo[55b036c16000+5000] ^^^^

  • أخيرًا، نستبدل getenv_fn جزئيًا (نستبدل البايتين الأقل أهمية بـ 0x8a00، إزاحة execv() في sudoers.so، والبايت الثالث بـ 0x00، المنهي الصفري لـ user_args في set_cmnd()) حتى نهزم ASLR -- لدينا فرصة جيدة لاستبدال getenv_fn بعنوان execv() بعد 2^(3*8-12) = 2^12 = 4096 محاولة، وبالتالي تنفيذ ملفنا الثنائي، المسمى "SYSTEMD_BYPASS_USERDB"، كجذر.

اختبرنا هذا الاستغلال الأول بنجاح على Ubuntu 20.04.

======================================================================== 2/ استبدال struct service_user

ثاني عطل لفت انتباهنا هو:


Program received signal SIGSEGV, Segmentation fault.

0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344

=> 0x7f6bf9c294ee <nss_load_library+46>: cmpq $0x0,0x8(%rbx)

rbx 0x41414141414141 18367622009667905

تعطلت وظيفة glibc nss_load_library() (في السطر 344) لأننا استبدلنا المؤشر "library"، وهو عضو في struct service_user القائم على الكومة:


327 static int 328 nss_load_library (service_user ni) 329 { 330 if (ni->library == NULL) 331 { ... 338 ni->library = nss_new_service (service_table ?: &default_table, 339 ni->name); ... 342 } 343 344 if (ni->library->lib_handle == NULL) 345 { 346 / Load the shared library. / 347 size_t shlen = (7 + strlen (ni->name) + 3 348 + strlen (__nss_shlib_revision) + 1); 349 int saved_errno = errno; 350 char shlib_name[shlen]; 351 352 / Construct shared object name. */ 353 __stpcpy (__stpcpy (__stpcpy (_"), 355 ni->name), 356 ".so"), 357 __nss_shlib_revision); 358 359 ni->library->lib_handle = __libc_dlopen (shlib_name);

يمكننا بسهولة تحويل هذا الاستبدال لـ struct service_user إلى تنفيذ كود عشوائي:

  • نستبدل ni->library بمؤشر NULL، لدخول الكتلة في الأسطر 330-342، وتجنب العطل في السطر 344، ودخول الكتلة في الأسطر 344-359؛

  • نستبدل ni->name (مصفوفة من الأحرف، في البداية "systemd") بـ "X/X"؛

  • الأسطر 353-357 تقوم ببناء اسم مكتبة مشتركة "libnss_X/X.so.2" (بدلاً من "libnss_systemd.so.2")؛- في السطر 359، نقوم بتحميل المكتبة المشتركة الخاصة بنا "libnss_X/X.so.2" من دليل العمل الحالي وتنفيذ المُنشئ _init() الخاص بنا كجذر.

لقد اختبرنا هذا الاستغلال الثاني بنجاح على Ubuntu 20.04 وDebian 10 وFedora 33.

======================================================================== 3/ استبدال def_timestampdir

الاستغلال الثالث لدينا لا ينبثق من أحد أعطال Sudo، بل من ملاحظة عابرة: أثناء هجوم القوة العمياء، أنشأ Sudo عشرات الدلائل الجديدة في دليل العمل الحالي لدينا (AAAAAA، AAAAAAAAA، إلخ). كل من هذه الدلائل مملوكة للجذر وتحتوي فقط على ملف صغير واحد، مسمى باسم المستخدم الخاص بنا: ملف الطابع الزمني الخاص بـ Sudo — من الواضح أننا قمنا باستبدال def_timestampdir، اسم دليل الطابع الزمني الخاص بـ Sudo.

إذا قمنا باستبدال def_timestampdir باسم دليل غير موجود بالفعل، فيمكننا أن نتسابق مع الدالة ts_mkdirs() الخاصة بـ Sudo، وننشئ رابطًا رمزيًا لملف عشوائي، و:

3أ/ إما chown() هذا الملف العشوائي إلى المستخدم الجذر ومجموعة الجذر؛

3ب/ أو فتح (أو إنشاء) هذا الملف العشوائي كجذر، وكتابة هيكل timestamp_entry إليه.

لم نتمكن من تحويل 3أ/ إلى صلاحيات جذر كاملة (على سبيل المثال، إذا قمنا بعملية chown() لملف SUID الخاص بنا إلى الجذر، فإن النواة تقوم تلقائيًا بإزالة بت SUID الخاص بملفنا). إذا وجدت، أيها القارئ العزيز، حلًا لهذه المشكلة، فيرجى نشره في القائمة البريدية العامة oss-security!

في النهاية، تمكنا من تحويل 3ب/ إلى صلاحيات جذر كاملة، لكننا واجهنا مشكلتين في البداية:

  • الدالة timestamp_open() الخاصة بـ Sudo تحذف رابطنا الرمزي العشوائي إذا كان الملف الذي يشير إليه أقدم من وقت الإقلاع. تمكنا من حل هذه المشكلة الأولى عن طريق إنشاء ملف طابع زمني قديم جدًا (من عصر يونكس)، والانتظار حتى تقوم timestamp_open() بحذفه، والتسابق مع timestamp_open() لإنشاء الرابط الرمزي النهائي والعشوائي.

  • لا نتحكم في محتويات هيكل timestamp_entry الذي يتم كتابته إلى الملف العشوائي. حسب علمنا، نتحكم فقط في ثلاثة بايتات (معرف عملية أو هيكل timespec)، ولم نتمكن من تحويل هذه الكتابة ثلاثية البايت إلى صلاحيات جذر كاملة. إذا وجدت، أيها القارئ العزيز، حلًا لهذه المشكلة، فيرجى نشره في القائمة البريدية العامة oss-security!

مع ذلك، تمكنا من تجاوز هذه المشكلة الثانية عن طريق إساءة استخدام خطأ بسيط في الدالة timestamp_lock() الخاصة بـ Sudo. إذا فزنا في السباقين ضد ts_mkdirs() وtimestamp_open()، وإذا كان رابطنا الرمزي العشوائي يشير إلى /etc/passwd، فسيتم فتح هذا الملف كجذر، و:


65 struct timestamp_entry { 66 unsigned short version; /* رقم الإصدار / 67 unsigned short size; / حجم الإدخال / 68 unsigned short type; / TS_GLOBAL, TS_TTY, TS_PPID */ .. 78 };

305 static ssize_t 306 ts_write(int fd, const char *fname, struct timestamp_entry *entry, off_t offset) 307 { ... 318 nwritten = pwrite(fd, entry, entry->size, offset); ... 350 }

619 bool 620 timestamp_lock(void *vcookie, struct passwd *pw) 621 { 622 struct ts_cookie *cookie = vcookie; 623 struct timestamp_entry entry; ... 644 nread = read(cookie->fd, &entry, sizeof(entry)); 645 if (nread == 0) { ... 652 } else if (entry.type != TS_LOCKEXCL) { ... 657 if (ts_write(cookie->fd, cookie->fname, &entry, 0) == -1)

  • في السطر 644، يتم قراءة أول 0x38 بايت من /etc/passwd ("root❌0:0:...") في هيكل timestamp_entry مخصص على المكدس، entry؛

  • في السطر 652، entry.type هو 0x783a (":x")، وليس TS_LOCKEXCL؛

  • في السطرين 657 و318، يتم كتابة entry->size بايت من الإدخال المخصص على المكدس إلى /etc/passwd، ولكن entry->size هو في الواقع 0x746f ("ot")، وليس sizeof(struct timestamp_entry).

نتيجة لذلك، نكتب المحتوى الكامل لمكدس Sudo إلى /etc/passwd (بما في ذلك وسائط سطر الأوامر ومتغيرات البيئة الخاصة بنا): نقوم بحقن مستخدم عشوائي في /etc/passwd وبالتالي نحصل على صلاحيات جذر كاملة. لقد اختبرنا هذا الاستغلال الثالث بنجاح على Ubuntu 20.04.

ملاحظة: تم إصلاح هذا الخطأ البسيط في timestamp_lock() في يناير 2020 عن طريق الالتزام 586b418a، ولكن لم يتم إرجاع هذا الإصلاح إلى الإصدارات القديمة.

======================================================================== شكر وتقدير

نشكر Todd C. Miller على احترافيته واستجابته السريعة واهتمامه الدقيق بكل تفاصيل تقريرنا. كما نشكر أعضاء distros@openwall.

======================================================================== الجدول الزمني

2021-01-13: إرسال الاستشارة إلى Todd.Miller@sudo.

2021-01-19: إرسال الاستشارة والتصحيحات إلى distros@openwall.

2021-01-26: تاريخ الإصدار المنسق (6:00 مساءً بالتوقيت العالمي المنسق).

تنزيل الأداة
stpcpy (shlib_name, 354 "libnss