Команда sudo в UNIX и Linux позволяет обычному пользователю выполнять команды от имени других пользователей, в частности — от root. Ребята из Qualys нашли в sudo уязвимость типа LPE, которая дает атакующему возможность повысить привилегии в системе. Посмотрим, как это работает.

Уязвимость затрагивает все версии sudo, начиная 1.8.6p7 и заканчивая 1.8.20p2, а связана ошибка с неверной логикой обработки результатов /proc/[pid]/stat в функции get_process_ttyname(). Используя специально сформированные символические ссылки, злоумышленник может вызвать команду sudo и подменить в ее контексте пользовательский терминал симлинком на нужный файл. Таким образом, он может переписать содержимое этого файла результатом работы выполняемой через sudo команды.

 

Подготовка

Начнем с выбора операционной системы. Я буду использовать CentOS 7 в качестве гостевой ОС, поэтому все мои манипуляции будут актуальны именно для этой версии системы.

Запускать ОС через Docker я не советую — SELinux в нем работает не так же, как в полноценном дистрибутиве. Лучше использовать VMware или VirtualBox.

Чтобы протестировать уязвимость, нам понадобятся три условия:

  1. Обычный пользователь с sudo-привилегиями выполнения какой-нибудь программы.
  2. Активированный SELinux (Security Enhanced Linux).
  3. Сам бинарник sudo должен быть собран с поддержкой SELinux (поддержка sudo -r role).

Разберемся с каждым пунктом.

Для начала создадим юзера attacker. Я уже сделал это на этапе установки системы, но если тебе по каким-то причинам такой вариант не подходит, то это можно провернуть следующей командой в консоли:

useradd -d /home/attacker -s /bin/bash -p $(echo verysecretpass | openssl passwd -1 -stdin) attacker

Теперь делегируем новоиспеченному юзеру возможность выполнять какую-нибудь команду от суперпользователя. Не буду далеко отходить от репорта Qualys и для демонстрации уязвимости использую бинарник sum, который занимается подсчетом контрольной суммы указанного файла. Для этого можно воспользоваться командой visudo или же просто добавить строчку attacker ALL=(ALL) NOPASSWD: /usr/bin/sum в файл /etc/sudoers.

Добавление пользователю прав на выполнение команды через sudo
Добавление пользователю прав на выполнение команды через sudo

В CentOS 7 SELinux включен и прекрасно работает из коробки, поэтому никаких дополнительных действий второй и третий пункты от нас не потребуют. Проверить текущий статус подсистемы можно командой sestatus.

SELinux работает в CentOS из коробки
SELinux работает в CentOS из коробки

С подготовкой закончили, самое время скачивать эксплоит и переходить к разбору деталей уязвимости.

 

Детали уязвимости

В самом начале я упомянул, что проблема находится в функции get_process_ttyname, которая объявлена в файле ttyname.c.

Давай разберемся, что делает эта функция. При выполнении команды она получает информацию о статусе процесса /proc/[pid]/stat и читает номер терминала tty из поля под номером 7 — tty_nr.

Поле tty_nr (7) — номер терминала, который использует процесс
Поле tty_nr (7) — номер терминала, который использует процесс

Саму структуру полей stat можно посмотреть в файле /usr/src/linux/fs/proc/array.c.

array.c

493:    seq_printf(m, "%d (%s) %c", pid_nr_ns(pid, ns), tcomm, state); [1 2 3]
494:    seq_put_decimal_ll(m, " ", ppid); [4]
495:    seq_put_decimal_ll(m, " ", pgid); [5]
496:    seq_put_decimal_ll(m, " ", sid); [6]
497:    seq_put_decimal_ll(m, " ", tty_nr); [7]
498:    seq_put_decimal_ll(m, " ", tty_pgrp);

Поля, как видишь, отделяются друг от друга пробелами. По ним функция get_process_ttyname и разбивает строку (результат работы /proc/[pid]/stat) для дальнейшего парсинга.

/src/ttyname.c

480: get_process_ttyname(char *name, size_t namelen)
481: {
...
490:     /* Try to determine the tty from tty_nr in /proc/pid/stat. */
491:     snprintf(path, sizeof(path), "/proc/%u/stat", (unsigned int)getpid());
492:     if ((fp = fopen(path, "r")) != NULL) {
493:    len = getline(&line, &linesize, fp);
494:    fclose(fp);
495:    if (len != -1) {
496:        /* Field 7 is the tty dev (0 if no tty) */
...
501:        while (*++ep != '\0') {
502:        if (*ep == ' ') {
503:            *ep = '\0';
504:            if (++field == 7) {
505:            dev_t tdev = strtonum(cp, INT_MIN, INT_MAX, &errstr);

Теперь посмотрим на поле под номером 2 (comm). Это не что иное, как имя исполняемого файла, взятое в круглые скобки. А ведь оно вполне может содержать пробелы. Например, возьмем стандартный бинарник cat, скопируем и сохраним его под новым именем — cat with spaces. Теперь выполним команду cat\ with\ spaces /proc/self/stat и посмотрим на результат.

Парсинг по пробелам вызывает проблему верного определения tty_nr
Парсинг по пробелам вызывает проблему верного определения tty_nr

Как видишь, если теперь, следуя коду функции get_process_ttyname, разбивать строку по пробелам, то элементом под номером 7 будет совсем не tty_nr, как ожидается. Выходит, что, используя определенное количество пробелов в имени, мы можем нарушить заложенную разработчиками логику работы функции get_process_ttyname.

Как можно управлять именем команды? Конечно, при помощи симлинков! Если мы вызовем sudo через симлинк с именем ./ 1, то get_process_ttyname вызовет функцию sudo_ttyname_dev для поиска несуществующего tty-устройства с номером 1 в массиве search_devs.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии