Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2021-3156-Heap-Based-Buffer-Overflow-in-Sudo-Baron-Samedit- — Технический анализ и код эксплойта для CVE-2021-3156 — переполнение буфера в куче в Sudo, позволяющее локальное повышение привилегий до root без аутентификации. | 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 — переполнение буфера в куче в Sudo, позволяющее локальное повышение привилегий до root без аутентификации.

Репозиторий
1 год назадЕщё не проверено

Qualys Security Advisory

Baron Samedit: Переполнение буфера в куче в Sudo (CVE-2021-3156)

======================================================================== Содержание

Краткое описание Анализ Эксплуатация Благодарности Временная шкала

======================================================================== Краткое описание

Мы обнаружили переполнение буфера в куче в Sudo (https://www.sudo.ws/). Эта уязвимость:

  • эксплуатируется любым локальным пользователем (обычные пользователи и системные пользователи, sudoers и не-sudoers), без аутентификации (т.е. атакующему не нужно знать пароль пользователя);

  • была внесена в июле 2011 года (коммит 8255ed69) и затрагивает все устаревшие версии с 1.8.2 по 1.8.31p2 и все стабильные версии с 1.9.0 по 1.9.5p1, в их конфигурации по умолчанию.

Мы разработали три различных эксплойта для этой уязвимости и получили полные привилегии root на Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27) и Fedora 33 (Sudo 1.9.2). Другие операционные системы и дистрибутивы, вероятно, также уязвимы.

======================================================================== Анализ

Если Sudo выполняется для запуска команды в «командном» режиме (shell -c command):

  • либо через опцию -s, которая устанавливает флаг MODE_SHELL в Sudo;

  • либо через опцию -i, которая устанавливает флаги MODE_SHELL и MODE_LOGIN_SHELL;

то в начале функции 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 -c cmnd */ ... 603 av = reallocarray(NULL, ac + 1, sizeof(char *)); ... 609 av[0] = (char 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).

Например, в amd64 Linux следующая команда выделяет 24-байтовый буфер "user_args" (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|AA=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()им и free()им несколько переменных окружения 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 и бэктрейсов 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 (указатель на нулевой указатель) совместимо с argv execve(); . hook->closure (нулевой указатель) совместимо с 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 является 22-м указателем, который мы перезаписываем (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" от root.

Мы успешно протестировали этот первый эксплойт на 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 нулевым указателем, чтобы войти в блок строк 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() как root.

Мы успешно протестировали эту вторую эксплуатацию на Ubuntu 20.04, Debian 10 и Fedora 33.

======================================================================== 3/ перезапись def_timestampdir

Наша третья эксплуатация основана не на одной из ошибок Sudo, а на случайном наблюдении: во время нашего перебора Sudo создавал десятки новых каталогов в нашем текущем рабочем каталоге (AAAAAA, AAAAAAAAA и т.д.). Каждый из этих каталогов принадлежит root и содержит только один маленький файл, названный в честь нашего собственного пользователя: временной файл Sudo — мы, очевидно, перезаписали def_timestampdir, имя каталога временных меток Sudo.

Если мы перезапишем def_timestampdir именем каталога, который ещё не существует, то мы можем устроить гонку с ts_mkdirs() Sudo, создать символическую ссылку на произвольный файл и:

3а/ либо выполнить chown() для этого произвольного файла на пользователя root и группу root;

3б/ либо открыть (или создать) этот произвольный файл как root и записать в него struct timestamp_entry.

Нам не удалось преобразовать 3а/ в полные привилегии root (например, если мы выполняем chown() для нашего собственного SUID-бинарника на root, то ядро автоматически удаляет SUID-бит нашего бинарника). Если вы, дорогой читатель, найдёте решение этой проблемы, пожалуйста, опубликуйте его в общедоступном списке рассылки oss-security!

В конце концов, нам удалось преобразовать 3б/ в полные привилегии root, но изначально мы столкнулись с двумя проблемами:

  • timestamp_open() Sudo удаляет нашу произвольную символическую ссылку, если файл, на который она указывает, старше времени загрузки. Нам удалось решить эту первую проблему, создав очень старый временной файл (от начала эпохи Unix), дождавшись, пока timestamp_open() удалит его, и устроив гонку с timestamp_open(), чтобы создать нашу окончательную произвольную символическую ссылку.

  • Мы не контролируем содержимое struct timestamp_entry, которое записывается в произвольный файл. Насколько нам известно, мы контролируем только три байта (идентификатор процесса или struct timespec), и нам не удалось преобразовать эту запись из трёх байт в полные привилегии root. Если вы, дорогой читатель, найдёте решение этой проблемы, пожалуйста, опубликуйте его в общедоступном списке рассылки oss-security!

Однако нам удалось обойти эту вторую проблему, злоупотребив незначительной ошибкой в timestamp_lock() Sudo. Если мы выиграем две гонки с ts_mkdirs() и timestamp_open(), и если наша произвольная символическая ссылка указывает на /etc/passwd, то этот файл открывается как root, и:


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:...") читаются в стековую struct timestamp_entry, entry;

  • в строке 652 entry.type равно 0x783a (":x"), а не TS_LOCKEXCL;

  • в строках 657 и 318 entry->size байт из стекового entry записываются в /etc/passwd, но entry->size на самом деле равно 0x746f ("ot"), а не sizeof(struct timestamp_entry).

В результате мы записываем всё содержимое стека Sudo в /etc/passwd (включая наши аргументы командной строки и переменные окружения); мы внедряем произвольного пользователя в /etc/passwd и, таким образом, получаем полные привилегии root. Мы успешно протестировали эту третью эксплуатацию на Ubuntu 20.04.

Примечание: эта незначительная ошибка в timestamp_lock() была исправлена в январе 2020 года коммитом 586b418a, но это исправление не было перенесено в устаревшие версии.

======================================================================== Благодарности

Мы благодарим Тодда К. Миллера за профессионализм, быструю реакцию и скрупулёзное внимание к каждой детали в нашем отчёте. Мы также благодарим участников distros@openwall.

======================================================================== Хронология

2021-01-13: Уведомление отправлено Todd.Miller@sudo.

2021-01-19: Уведомление и исправления отправлены на distros@openwall.

2021-01-26: Согласованная дата публикации (18:00 UTC).

Скачать инструмент
dst++ = ' '; 595 } ... 600 ac += 2; /
)user_details.shell; /
stpcpy (shlib_name, 354 "libnss