[Из песочницы] Обзор уязвимости в Winbox от Mikrotik. Или большой фейл

Всем доброго времени суток, наверняка многие уже слышали про недавнюю уязвимость в роутерах Mikrotik, позволяющую извлечь пароли всех пользователей. В этой статье я бы хотел подробно показать и разобрать суть данной уязвимости.

Весь материал предоставляется лишь в ознакомительных целях, поэтому кода, эксплуатирующего уязвимость, тут не будет. Если вам не интересно узнать о причинах и внутреннем устройстве той или иной уязвимости, можете не читать дальше.

Начнём


Первое, с чего стоит начать, это анализ трафика между клиентом Winbox и устройством

Winbox — приложение для ОС WIndows, которое в точности повторяет веб-интерфейс и предназначено для администрирования и конфигурирования устройства с Router OS на борту. Поддерживается 2 режима работы, по протоколу TCP и UDP

Перед началом стоит отключить шифрование трафика в Winbox. Делается это следующим образом: нужно включить галочку ToolsAdvanced Mode. После этого интерфейс изменится следующим образом:

bw8c3ntuyktl2j8u8aoulc1pdbq.png


Снимаем галочку Secure Mode. Запускаем Wireshark и пробуем авторизоваться на устройстве:

mq2whsxb2vdhjddbedi9etvpb7c.png


Как можно заметить ниже, после авторизации идёт запрос фала list и затем его содержимое нам полностью передаётся, может показаться, что всё хорошо, но взглянем на самое начало этой сессии:

ilmsfoqh_24blolrg0fuhdxhfhy.png


В самом начале Winbox отправляет точно такой же пакет с запросом файла list:

arukqunodb50p3a0f6xawc19cuo.png


Рассмотрим его структуру:

  1. 37010035 — размер пакета
  2. M2 — константа, обозначающая начало пакета
  3. 0500ff01 — переменная 0xff0005 в значении True
  4. 0600ff09 01 — переменная 0xff0006 в значении 1 (Номер передаваемого пакета)
  5. 0700ff09 07 — переменная 0xff0007 в значении 7 (Открыть файл в режиме чтения)
  6. 01000021 04 6с967374 — переменная 0×01000001 строка list размером 4 байта (Обычно данная переменная отвечает за название файла)
  7. 0200ff88 02… 00 — массив 0xff0002 размером 2 элемента
  8. 0100ff88 02… 00 — массив 0xff0001 размером 2 элемента


В результате реверса протокола, и соответствующих бинарных файлов на стороне клиента и сервера, удалось в большей степени восстановить и понять структуру протокола, по которому Winbox общается с устройством.

Описание протокола NvMessage

Типы полей (Название: Цифровое обозначение)


  • u32: 0×08000000
  • u32_array: 0×88000000
  • string: 0×20000000
  • string_array: 0xA0000000
  • addr6: 0×18000000
  • addr6_array: 0×98000000
  • u64: 0×10000000
  • u64_array: 0×90000000
  • true: 0×00000000
  • false: 0×01000000
  • bool_array: 0×80000000
  • message: 0×28000000
  • message_array: 0xA8000000
  • raw: 0×30000000
  • raw_array: 0xB0000000
  • u8: 0×09000000
  • be32_array: 0×88000000

Типы ошибок (Название: Цифровое обозначение)


  • 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: 0×01
  • STATUS_ERROR: 0×02

Структура полей в пакете


В начале любого поля идёт его тип — 4 байта (3 байта — назначение переменной, об этом позже, 1 байт — непосредственно тип этой переменной) затем длина 1–2 байта и непосредственно значение.

Массивы


Образно массив можно описать следующей структурой:
struct Array {
    uint32 type;
    uint8 count;
    uint32 item1;
    uint32 item2;
    ...
    uint8 zero;
}

Тип (4 байта) / Кол-во элементов (1 байт) / Элементы (4 байта) / В завершении байт \x00

Строки


Строки не нуль-терминированны, а имеют четко заданную длину:
struct Array {
    uint32 type;
    uint8 length;
    char text[length];
}

Числа


Самый простой тип в пакете, его можно представить как тип-значение:
struct Array {
    uint32 type;
    uint8/32/64 value;
}

В зависимости от типа, значение имеет соответствующую размерность бит.

Булевый тип


Размер поля 4 байта, старший байт отвечает за значение (True\False), младшие 3 байта за назначение переменной

Дополнительно каждый пакет содержит:

  1. специальные маркеры для обозначения начала пакета
  2. размер пакета
  3. маркеты, отвечающие за контроль больших пакетов

Найденные константы


  • 0xfe0001 — Содержит идентификатор сессии (1 байт)
  • 0xff0006 — Номер отправляемого пакета (1 байт)
  • 0xff0007 — Режим доступа к файлу (1 байт)

Режимы доступа к файлу
  • 7 — открыть для чтения
  • 1 — открыть для записи
  • 6 — создание директории
  • 4 — выполнить чтение
  • 5 — удалить


Теперь зная, как устроен протокол, мы можем произвольно генерировать нужные нам пакеты и смотреть, как на них реагирует девайс.

На стороне устройства, за обработку пакетов отвечает исполняемый файл /nova/bin/mproxy. Так как названия функций не были сохранены, я назвал функцию, которая обрабатывает пакет и принимает решения о том что делать с файлом file_handler (). Взглянем на саму функцию:

siwnbthrbta88hokr1qjmcsllvu.png


P.S. Код который нас будет интересовать отмечен стрелочками.


Шаг 1


При получении пакета на открытие файла для чтения, он начинает обработку с этого блока:

7aizx48ztgn_s15p0kmtkv1avdu.png


В самом начале из пакета, с помощью функции nv: message: get() извлекается название файла.

Далее функция tokenize () разбивает полученную строку на отдельные части, используя в качестве разделителя символ »/».

Полученный массив строк передаётся в функцию path_filter (), которая проверяет полученный массив строк на наличие »», и в случае ошибок возвращает ошибку ERROR_NOTALLOWED (0xFE0009)

nv6aex2ifzlg9o2pqhjcevvw_3g.png


P.S. ERROR_NOTALLOWED так же будет получен в ответе, если нет прав доступа к файлу


Если же всё нормально, то к началу названия файла конкатенируется путь, к директории webfig или pckg

Шаг 2


08tjxvub-j4ymwlkx8vbyvibu48.png


Если всё прошло успешно, открывается файл и его дескриптор сохраняется в глобальный объект.

Если файл открыть не удалось, то в ответе мы получаем ошибку: cannot open source file.

ebgzbqqw37zawbh6-1rad67ql4g.png


Таким образом, чтобы получить содержимое файла, должно быть соблюдено 3 условия:

  1. Путь к файлу не содержит »»;
  2. Имеются права на доступ к файлу;
  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 ():

0rqjoeg644_tcta0gave71d1n-e.png


По коду отлично видно, что действительно происходит поиск вхождения » », в полученный массив строк. Но дальше самое интересное, я выделил красным этот фрагмент.
Суть этого кода в том, что: Если предыдущий элемент так же является »…», то проверка считается проваленной. В противном случае — считать, что всё хорошо.

Т.е. чтобы всё сработало, нужно просто чередовать »/./» и »/…/» чтобы успешно перемещаться по любым каталогам и спускаться на любой уровень ФС.

w8v42ssnrykzph96vl6ngf1_muq.jpeg


Давайте посмотрим, как разработчики Mikrotik это исправили:

un05tmw-tylgkiewgfutcshjssw.png


Сравнение псевдо-С кода
t9fljb3v3tdf0ikho7mrspaoy08.png


Теперь выход из цикла проверки происходит при первом же обнаружении » ». Правда мне не совсем понятно, зачем добавили проверку вхождения одной точки. А из-за изменения механизма активации пользователя devel, к сожалению, нет возможности посмотреть это в динамике.

Подведём итог


  1. Router OS без проблем обрабатывает входящие пакеты ещё до авторизации пользователя
  2. Из-за некорректного фильтра мы получаем доступ к любому файлу


Учитывая предыдущие пункты, мы без проблем можем: создавать, удалять, читать, и записывать файлы, а так же создавать произвольные директории

Так что не удивительно, что имея доступ на чтение любых файлов без авторизации, первым что было сделано, это чтение файла с паролями пользователей. Благо в сети предостаточно информации о том, где он расположен, и как извлечь из него данные.

Так же данная уязвимость может стать отличной заменой для известной ранее возможности активации режима разработчика, ведь перезагружать устройство, делать backup\restore файла конфигурации теперь не нужно.

© Habrahabr.ru