Всем доброго времени суток, наверняка многие уже слышали про недавнюю уязвимость в роутерах Mikrotik, позволяющую извлечь пароли всех пользователей. В этой статье я бы хотел подробно показать и разобрать суть данной уязвимости/
Весь материал предоставляется лишь в ознакомительных целях, поэтому кода, эксплуатирующего уязвимость, тут не будет. Если вам не интересно узнать о причинах и внутреннем устройстве той или иной уязвимости, можете не читать дальше.
Начнём
Первое, с чего стоит начать, это анализ трафика между клиентом Winbox и устройством
Winbox — приложение для ОС WIndows, которое в точности повторяет веб-интерфейс и предназначено для администрирования и конфигурирования устройства с Router OS на борту. Поддерживается 2 режима работы, по протоколу TCP и UDP
Перед началом стоит отключить шифрование трафика в Winbox. Делается это следующим образом: нужно включить галочку Tools -> Advanced Mode. После этого интерфейс изменится следующим образом:
Снимаем галочку Secure Mode. Запускаем Wireshark и пробуем авторизоваться на устройстве:
Как можно заметить ниже, после авторизации идёт запрос файла list и затем его содержимое нам полностью передаётся, может показаться, что всё хорошо, но взглянем на самое начало этой сессии:
В самом начале Winbox отправляет точно такой же пакет с запросом файла list:
Рассмотрим его структуру:
- 37010035 — размер пакета
- M2 — константа, обозначающая начало пакета
- 0500ff01 — переменная 0xff0005 в значении True
- 0600ff09 01 — переменная 0xff0006 в значении 1 (Номер передаваемого пакета)
- 0700ff09 07 — переменная 0xff0007 в значении 7 (Открыть файл в режиме чтения)
- 01000021 04 6с967374 — переменная 0x01000001 строка list размером 4 байта (Обычно данная переменная отвечает за название файла)
- 0200ff88 02… 00 — массив 0xff0002 размером 2 элемента
- 0100ff88 02… 00 — массив 0xff0001 размером 2 элемента
В результате реверса протокола, и соответствующих бинарных файлов на стороне клиента и сервера, удалось в большей степени восстановить и понять структуру протокола, по которому Winbox общается с устройством.
Типы полей (Название: Цифровое обозначение)
- u32: 0x08000000
- u32_array: 0x88000000
- string: 0x20000000
- string_array: 0xA0000000
- addr6: 0x18000000
- addr6_array: 0x98000000
- u64: 0x10000000
- u64_array: 0x90000000
- true: 0x00000000
- false: 0x01000000
- bool_array: 0x80000000
- message: 0x28000000
- message_array: 0xA8000000
- raw: 0x30000000
- raw_array: 0xB0000000
- u8: 0x09000000
- be32_array: 0x88000000
Типы ошибок (Название: Цифровое обозначение)
- SYS_TO: 0xFF0001
- STD_UNDOID: 0xFE0006
- STD_DESCR: 0xFE0009
- STD_FINISHED: 0xFE000B
- STD_DYNAMIC: 0xFE0007
- STD_INACTIVE: 0xFE0008
- STD_GETALLID: 0xFE0003
- STD_GETALLNO: 0xFE0004
- STD_NEXTID: 0xFE0005
- STD_ID: 0xFE0001
- STD_OBJS: 0xFE0002
- SYS_ERRNO: 0xFF0008
- SYS_POLICY: 0xFF000B
- SYS_CTRL_ARG: 0xFF000F
- SYS_RADDR6: 0xFF0013
- SYS_CTRL: 0xFF000D
- SYS_ERRSTR: 0xFF0009
- SYS_USER: 0xFF000A
- SYS_STATUS: 0xFF0004
- SYS_FROM: 0xFF0002
- SYS_TYPE: 0xFF0003
- SYS_REQID: 0xFF0006
Значения ошибок (Название: Цифровое обозначение)
- ERROR_FAILED: 0xFE0006
- ERROR_TOOBIG: 0xFE000A
- ERROR_EXISTS: 0xFE0007
- ERROR_NOTALLOWED: 0xFE0009
- ERROR_BUSY: 0xFE000C
- ERROR_UNKNOWN: 0xFE0001
- ERROR_BRKPATH: 0xFE0002
- ERROR_UNKNOWNID: 0xFE0004
- ERROR_UNKNOWNNEXTID: 0xFE000B
- ERROR_TIMEOUT: 0xFE000D
- ERROR_TOOMUCH: 0xFE000E
- ERROR_NOTIMP: 0xFE0003
- ERROR_MISSING: 0xFE0005
- STATUS_OK: 0x01
- STATUS_ERROR: 0x02
Структура полей в пакете
В начале любого поля идёт его тип — 4 байта (3 байта — назначение переменной, об этом позже, 1 байт — непосредственно тип этой переменной) затем длина 1-2 байта и непосредственно значение.
Массивы
Образно массив можно описать следующей структурой:
struct Array {
uint32 type;
uint8 count;
uint32 item1;
uint32 item2;
...
uint8 zero;
}
Тип (4 байта) / Кол-во элементов (1 байт) / Элементы (4 байта) / В завершении байт \x00
Строки
Строки не нуль-терминированны, а имеют четко заданную длину:
struct String {
uint32 type;
uint8 length;
char text[length];
}
Числа
Самый простой тип в пакете, его можно представить как тип-значение:
struct u* {
uint32 type;
uint8/32/64 value;
}
В зависимости от типа, значение имеет соответствующую размерность бит.
Булевый тип
Размер поля 4 байта, старший байт отвечает за значение (True\False), младшие 3 байта за назначение переменной
Дополнительно каждый пакет содержит:
- специальные маркеры для обозначения начала пакета
- размер пакета
- маркеты, отвечающие за контроль больших пакетов
Найденные константы
- 0xfe0001 — Содержит идентификатор сессии (1 байт)
- 0xff0006 — Номер отправляемого пакета (1 байт)
- 0xff0007 — Режим доступа к файлу (1 байт)
Режимы доступа к файлу
- 7 — открыть для чтения
- 1 — открыть для записи
- 6 — создание директории
- 4 — выполнить чтение
- 5 — удалить
Теперь зная, как устроен протокол, мы можем произвольно генерировать нужные нам пакеты и смотреть, как на них реагирует девайс.
На стороне устройства, за обработку пакетов отвечает исполняемый файл /nova/bin/mproxy. Так как названия функций не были сохранены, я назвал функцию, которая обрабатывает пакет и принимает решения о том что делать с файлом file_handler(). Взглянем на саму функцию:
P.S. Код который нас будет интересовать отмечен стрелочками.
Шаг 1
При получении пакета на открытие файла для чтения, он начинает обработку с этого блока:
В самом начале из пакета, с помощью функции nv::message::get<nv::string_id>() извлекается название файла.
Далее функция tokenize() разбивает полученную строку на отдельные части, используя в качестве разделителя символ "/".
Полученный массив строк передаётся в функцию path_filter(), которая проверяет полученный массив строк на наличие "..", и в случае ошибок возвращает ошибку ERROR_NOTALLOWED (0xFE0009)
P.S. ERROR_NOTALLOWED так же будет получен в ответе, если нет прав доступа к файлу
Если же всё нормально, то к началу названия файла конкатенируется путь, к директории webfig или pckg
Шаг 2
Если всё прошло успешно, открывается файл и его дескриптор сохраняется в глобальный объект.
Если файл открыть не удалось, то в ответе мы получаем ошибку: cannot open source file.
Таким образом, чтобы получить содержимое файла, должно быть соблюдено 3 условия:
- Путь к файлу не содержит "..";
- Имеются права на доступ к файлу;
- Файл существует и может быть успешно открыт.
Теперь давайте попробуем отправить несколько пакетов для проверки работоспособности этой функции:
$ ./untitled.py -t 192.168.88.1 -f /etc/passwd
Error: SYS_ERRNO => ERROR_FAILED
Error: SYS_ERRSTR => cannot open source file
$ ./untitled.py -t 192.168.88.1 -f /../../../etc/passwd
Error: SYS_ERRNO => ERROR_NOTALLOWED
$ ./untitled.py -t 192.168.88.1 -f //./././././../etc/passwd
Error: SYS_ERRNO => ERROR_FAILED
Error: SYS_ERRSTR => cannot open source file
Так! А вот это уже странно… Мы помним, что ERROR_NOTALLOWED появляется если не прошла проверка в path_filter(), иначе мы бы ещё получили сообщение об отсутствии прав доступа, но в последнем случае, получается, что поиск файла производился в директории верхнего уровня?
Попробуем такой способ:
$ ./untitled.py -t 192.168.88.1 -f //./.././.././../etc/passwd
xvM2����� � 1Enobody:*:99:99:nobody:/tmp:/bin/sh
root::0:0:root:/home/root:/bin/sh
И это сработало. Но почему? Давайте взглянем на код функции path_filter():
По коду отлично видно, что действительно происходит поиск вхождения ".. ", в полученный массив строк. Но дальше самое интересное, я выделил красным этот фрагмент.
Суть этого кода в том, что: Если предыдущий элемент так же является "..", то проверка считается проваленной. В противном случае — считать, что всё хорошо.
Т.е. чтобы всё сработало, нужно просто чередовать "/./" и "/../" чтобы успешно перемещаться по любым каталогам и спускаться на любой уровень ФС.
Давайте посмотрим, как разработчики Mikrotik это исправили:
Теперь выход из цикла проверки происходит при первом же обнаружении ".. ". Правда мне не совсем понятно, зачем добавили проверку вхождения одной точки. А из-за изменения механизма активации пользователя devel, к сожалению, нет возможности посмотреть это в динамике.
Подведём итог
- Router OS без проблем обрабатывает входящие пакеты ещё до авторизации пользователя
- Из-за некорректного фильтра мы получаем доступ к любому файлу
Учитывая предыдущие пункты, мы без проблем можем: создавать, удалять, читать, и записывать файлы, а так же создавать произвольные директории
Так что не удивительно, что имея доступ на чтение любых файлов без авторизации, первым что было сделано, это чтение файла с паролями пользователей. Благо в сети предостаточно информации о том, где он расположен, и как извлечь из него данные.
Так же данная уязвимость может стать отличной заменой для известной ранее возможности активации режима разработчика, ведь перезагружать устройство, делать backup\restore файла конфигурации теперь не нужно.