Команда 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. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

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

Вариант 2. Купи один материал

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


Оставить мнение

Check Also

Карманные трояны. Как работают мобильные банкеры

Одним солнечным апрельским утром мой завтрак был прерван телефонным звонком приятеля — пре…