Содержание статьи
Уязвимость затрагивает все версии sudo, начиная 1.8.6p7 и заканчивая 1.8.20p2, а связана ошибка с неверной логикой обработки результатов /proc/[pid]/stat
в функции get_process_ttyname()
. Используя специально сформированные символические ссылки, злоумышленник может вызвать команду sudo и подменить в ее контексте пользовательский терминал симлинком на нужный файл. Таким образом, он может переписать содержимое этого файла результатом работы выполняемой через sudo команды.
Подготовка
Начнем с выбора операционной системы. Я буду использовать CentOS 7 в качестве гостевой ОС, поэтому все мои манипуляции будут актуальны именно для этой версии системы.
Запускать ОС через Docker я не советую — SELinux в нем работает не так же, как в полноценном дистрибутиве. Лучше использовать VMware или VirtualBox.
Чтобы протестировать уязвимость, нам понадобятся три условия:
- Обычный пользователь с sudo-привилегиями выполнения какой-нибудь программы.
- Активированный SELinux (Security Enhanced Linux).
- Сам бинарник 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
.
В CentOS 7 SELinux включен и прекрасно работает из коробки, поэтому никаких дополнительных действий второй и третий пункты от нас не потребуют. Проверить текущий статус подсистемы можно командой sestatus
.
С подготовкой закончили, самое время скачивать эксплоит и переходить к разбору деталей уязвимости.
Детали уязвимости
В самом начале я упомянул, что проблема находится в функции get_process_ttyname
, которая объявлена в файле ttyname.c.
Давай разберемся, что делает эта функция. При выполнении команды она получает информацию о статусе процесса /proc/[pid]/stat
и читает номер терминала tty из поля под номером 7 — tty_nr
.
Саму структуру полей stat можно посмотреть в файле /usr/src/linux/fs/proc/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
) для дальнейшего парсинга.
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
и посмотрим на результат.
Как видишь, если теперь, следуя коду функции 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»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»