Всем доброго времени суток, наверняка многие уже слышали про недавнюю уязвимость в роутерах Mikrotik, позволяющую извлечь пароли всех пользователей. В этой статье я бы хотел подробно показать и разобрать суть данной уязвимости.
Первое, с чего стоит начать, это анализ трафика между клиентом Winbox и устройством
![](https://habrastorage.org/r/w1560/webt/bw/8c/3n/bw8c3ntuyktl2j8u8aoulc1pdbq.png)
Снимаем галочку Secure Mode. Запускаем Wireshark и пробуем авторизоваться на устройстве:
![](https://habrastorage.org/r/w1560/webt/mq/2w/hs/mq2whsxb2vdhjddbedi9etvpb7c.png)
Как можно заметить ниже, после авторизации идёт запрос файла list и затем его содержимое нам полностью передаётся, может показаться, что всё хорошо, но взглянем на самое начало этой сессии:
![](https://habrastorage.org/r/w1560/webt/il/ms/fo/ilmsfoqh_24blolrg0fuhdxhfhy.png)
В самом начале Winbox отправляет точно такой же пакет с запросом файла list:
![](https://habrastorage.org/r/w1560/webt/ar/uk/qu/arukqunodb50p3a0f6xawc19cuo.png)
Рассмотрим его структуру:
В результате реверса протокола, и соответствующих бинарных файлов на стороне клиента и сервера, удалось в большей степени восстановить и понять структуру протокола, по которому Winbox общается с устройством.
Теперь зная, как устроен протокол, мы можем произвольно генерировать нужные нам пакеты и смотреть, как на них реагирует девайс.
На стороне устройства, за обработку пакетов отвечает исполняемый файл /nova/bin/mproxy. Так как названия функций не были сохранены, я назвал функцию, которая обрабатывает пакет и принимает решения о том что делать с файлом file_handler(). Взглянем на саму функцию:
![](https://habrastorage.org/r/w1560/webt/si/wn/bt/siwnbthrbta88hokr1qjmcsllvu.png)
При получении пакета на открытие файла для чтения, он начинает обработку с этого блока:
![](https://habrastorage.org/r/w1560/webt/7a/iz/x4/7aizx48ztgn_s15p0kmtkv1avdu.png)
В самом начале из пакета, с помощью функции nv::message::get<nv::string_id>() извлекается название файла.
Далее функция tokenize() разбивает полученную строку на отдельные части, используя в качестве разделителя символ "/".
Полученный массив строк передаётся в функцию path_filter(), которая проверяет полученный массив строк на наличие "..", и в случае ошибок возвращает ошибку ERROR_NOTALLOWED (0xFE0009)
![](https://habrastorage.org/r/w1560/webt/nv/6a/ex/nv6aex2ifzlg9o2pqhjcevvw_3g.png)
Если же всё нормально, то к началу названия файла конкатенируется путь, к директории webfig или pckg
![](https://habrastorage.org/r/w1560/webt/08/tj/xv/08tjxvub-j4ymwlkx8vbyvibu48.png)
Если всё прошло успешно, открывается файл и его дескриптор сохраняется в глобальный объект.
Если файл открыть не удалось, то в ответе мы получаем ошибку: cannot open source file.
![](https://habrastorage.org/r/w1560/webt/eb/gz/bq/ebgzbqqw37zawbh6-1rad67ql4g.png)
Таким образом, чтобы получить содержимое файла, должно быть соблюдено 3 условия:
Теперь давайте попробуем отправить несколько пакетов для проверки работоспособности этой функции:
Так! А вот это уже странно… Мы помним, что ERROR_NOTALLOWED появляется если не прошла проверка в path_filter(), иначе мы бы ещё получили сообщение об отсутствии прав доступа, но в последнем случае, получается, что поиск файла производился в директории верхнего уровня?
Попробуем такой способ:
И это сработало. Но почему? Давайте взглянем на код функции path_filter():
![](https://habrastorage.org/r/w1560/webt/0r/qj/oe/0rqjoeg644_tcta0gave71d1n-e.png)
По коду отлично видно, что действительно происходит поиск вхождения ".. ", в полученный массив строк. Но дальше самое интересное, я выделил красным этот фрагмент.
Суть этого кода в том, что: Если предыдущий элемент так же является "..", то проверка считается проваленной. В противном случае — считать, что всё хорошо.
Т.е. чтобы всё сработало, нужно просто чередовать "/./" и "/../" чтобы успешно перемещаться по любым каталогам и спускаться на любой уровень ФС.
![](https://habrastorage.org/r/w780q1/webt/w8/v4/2s/w8v42ssnrykzph96vl6ngf1_muq.jpeg)
Давайте посмотрим, как разработчики Mikrotik это исправили:
![](https://habrastorage.org/r/w1560/webt/un/05/tm/un05tmw-tylgkiewgfutcshjssw.png)
Теперь выход из цикла проверки происходит при первом же обнаружении ".. ". Правда мне не совсем понятно, зачем добавили проверку вхождения одной точки. А из-за изменения механизма активации пользователя devel, к сожалению, нет возможности посмотреть это в динамике.
Учитывая предыдущие пункты, мы без проблем можем: создавать, удалять, читать, и записывать файлы, а так же создавать произвольные директории
Так что не удивительно, что имея доступ на чтение любых файлов без авторизации, первым что было сделано, это чтение файла с паролями пользователей. Благо в сети предостаточно информации о том, где он расположен, и как извлечь из него данные.
Так же данная уязвимость может стать отличной заменой для известной ранее возможности активации режима разработчика, ведь перезагружать устройство, делать backup\restore файла конфигурации теперь не нужно.
Весь материал предоставляется лишь в ознакомительных целях, поэтому кода, эксплуатирующего уязвимость, тут не будет. Если вам не интересно узнать о причинах и внутреннем устройстве той или иной уязвимости, можете не читать дальше.
Начнём
Первое, с чего стоит начать, это анализ трафика между клиентом Winbox и устройством
Winbox — приложение для ОС WIndows, которое в точности повторяет веб-интерфейс и предназначено для администрирования и конфигурирования устройства с Router OS на борту. Поддерживается 2 режима работы, по протоколу TCP и UDPПеред началом стоит отключить шифрование трафика в Winbox. Делается это следующим образом: нужно включить галочку Tools -> Advanced Mode. После этого интерфейс изменится следующим образом:
![](https://habrastorage.org/webt/bw/8c/3n/bw8c3ntuyktl2j8u8aoulc1pdbq.png)
Снимаем галочку Secure Mode. Запускаем Wireshark и пробуем авторизоваться на устройстве:
![](https://habrastorage.org/webt/mq/2w/hs/mq2whsxb2vdhjddbedi9etvpb7c.png)
Как можно заметить ниже, после авторизации идёт запрос файла list и затем его содержимое нам полностью передаётся, может показаться, что всё хорошо, но взглянем на самое начало этой сессии:
![](https://habrastorage.org/webt/il/ms/fo/ilmsfoqh_24blolrg0fuhdxhfhy.png)
В самом начале Winbox отправляет точно такой же пакет с запросом файла list:
![](https://habrastorage.org/webt/ar/uk/qu/arukqunodb50p3a0f6xawc19cuo.png)
Рассмотрим его структуру:
- 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 общается с устройством.
Описание протокола NvMessage
В начале любого поля идёт его тип — 4 байта (3 байта — назначение переменной, об этом позже, 1 байт — непосредственно тип этой переменной) затем длина 1-2 байта и непосредственно значение.
Образно массив можно описать следующей структурой:
Тип (4 байта) / Кол-во элементов (1 байт) / Элементы (4 байта) / В завершении байт \x00
Строки не нуль-терминированны, а имеют четко заданную длину:
Самый простой тип в пакете, его можно представить как тип-значение:
В зависимости от типа, значение имеет соответствующую размерность бит.
Размер поля 4 байта, старший байт отвечает за значение (True\False), младшие 3 байта за назначение переменной
Дополнительно каждый пакет содержит:
Режимы доступа к файлу
Типы полей (Название: Цифровое обозначение)
- 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(). Взглянем на саму функцию:
![](https://habrastorage.org/webt/si/wn/bt/siwnbthrbta88hokr1qjmcsllvu.png)
P.S. Код который нас будет интересовать отмечен стрелочками.
Шаг 1
При получении пакета на открытие файла для чтения, он начинает обработку с этого блока:
![](https://habrastorage.org/webt/7a/iz/x4/7aizx48ztgn_s15p0kmtkv1avdu.png)
В самом начале из пакета, с помощью функции nv::message::get<nv::string_id>() извлекается название файла.
Далее функция tokenize() разбивает полученную строку на отдельные части, используя в качестве разделителя символ "/".
Полученный массив строк передаётся в функцию path_filter(), которая проверяет полученный массив строк на наличие "..", и в случае ошибок возвращает ошибку ERROR_NOTALLOWED (0xFE0009)
![](https://habrastorage.org/webt/nv/6a/ex/nv6aex2ifzlg9o2pqhjcevvw_3g.png)
P.S. ERROR_NOTALLOWED так же будет получен в ответе, если нет прав доступа к файлу
Если же всё нормально, то к началу названия файла конкатенируется путь, к директории webfig или pckg
Шаг 2
![](https://habrastorage.org/webt/08/tj/xv/08tjxvub-j4ymwlkx8vbyvibu48.png)
Если всё прошло успешно, открывается файл и его дескриптор сохраняется в глобальный объект.
Если файл открыть не удалось, то в ответе мы получаем ошибку: cannot open source file.
![](https://habrastorage.org/webt/eb/gz/bq/ebgzbqqw37zawbh6-1rad67ql4g.png)
Таким образом, чтобы получить содержимое файла, должно быть соблюдено 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():
![](https://habrastorage.org/webt/0r/qj/oe/0rqjoeg644_tcta0gave71d1n-e.png)
По коду отлично видно, что действительно происходит поиск вхождения ".. ", в полученный массив строк. Но дальше самое интересное, я выделил красным этот фрагмент.
Суть этого кода в том, что: Если предыдущий элемент так же является "..", то проверка считается проваленной. В противном случае — считать, что всё хорошо.
Т.е. чтобы всё сработало, нужно просто чередовать "/./" и "/../" чтобы успешно перемещаться по любым каталогам и спускаться на любой уровень ФС.
![](https://habrastorage.org/webt/w8/v4/2s/w8v42ssnrykzph96vl6ngf1_muq.jpeg)
Давайте посмотрим, как разработчики Mikrotik это исправили:
![](https://habrastorage.org/webt/un/05/tm/un05tmw-tylgkiewgfutcshjssw.png)
Сравнение псевдо-С кода![](https://habrastorage.org/r/w1560/webt/t9/fl/jb/t9fljb3v3tdf0ikho7mrspaoy08.png)
![](https://habrastorage.org/webt/t9/fl/jb/t9fljb3v3tdf0ikho7mrspaoy08.png)
Теперь выход из цикла проверки происходит при первом же обнаружении ".. ". Правда мне не совсем понятно, зачем добавили проверку вхождения одной точки. А из-за изменения механизма активации пользователя devel, к сожалению, нет возможности посмотреть это в динамике.
Подведём итог
- Router OS без проблем обрабатывает входящие пакеты ещё до авторизации пользователя
- Из-за некорректного фильтра мы получаем доступ к любому файлу
Учитывая предыдущие пункты, мы без проблем можем: создавать, удалять, читать, и записывать файлы, а так же создавать произвольные директории
Так что не удивительно, что имея доступ на чтение любых файлов без авторизации, первым что было сделано, это чтение файла с паролями пользователей. Благо в сети предостаточно информации о том, где он расположен, и как извлечь из него данные.
Так же данная уязвимость может стать отличной заменой для известной ранее возможности активации режима разработчика, ведь перезагружать устройство, делать backup\restore файла конфигурации теперь не нужно.