Огляд уразливості в Winbox від Mikrotik. Або великий фейл

Всім доброго часу доби, напевно багато хто вже чули про недавню уразливість в роутерах Mikrotik, що дозволяє отримати паролі всіх користувачів. У цій статті я б хотів докладно показати і розібрати суть даної уразливості.
Весь матеріал надається лише в ознайомлювальних цілях, тому коду, що експлуатує уразливість, тут не буде. Якщо вам не цікаво дізнатися про причини і внутрішній устрій тієї чи іншої проблеми, можете не читати далі.

Почнемо

Перше, з чого варто почати, це аналіз трафіку між клієнтом Winbox і пристроєм
Winbox — додаток для ОС WIndows, яке в точності повторює веб-інтерфейс і призначене для адміністрування та конфігурування пристрою з Router OS на борту. Підтримується 2 режиму роботи, за протоколом TCP і UDP Перед початком варто відключити шифрування трафіку в Winbox. Робиться це наступним чином: потрібно включити галочку Tools -> Advanced Mode. Після цього інтерфейс зміниться наступним чином:


Знімаємо галочку Secure Mode. Запускаємо Wireshark і пробуємо авторизуватися на пристрої:


Як можна побачити нижче, після авторизації йде запит файлу list і потім його вміст нам повністю передається, може здатися, що все добре, але поглянемо на початок цієї сесії:


На самому початку Winbox відправляє точно такий же пакет із запитом файлу list:


Розглянемо його структуру:

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

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

Опис протоколу NvMessage

Типи полів (Назва: Цифрове позначення)

 

  • 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 байта, старший байт відповідає за значення (TrueFalse), молодші 3 байти за призначення змінної

Додатково за кожний пакет містить:

  1. спеціальні маркери для позначення початку пакета
  2. розмір пакета
  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 умови:

  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():


За кодом відмінно видно, що дійсно відбувається пошук входження “.. “, отриманий масив рядків. Але далі найцікавіше, я виділив червоним цей фрагмент.
Суть цього коду в тому, що: Якщо попередній елемент так само є “..“, то перевірка вважається проваленою. В іншому випадку — вважати, що все добре.

Тобто щоб усе спрацювало, треба просто чергувати “/./” і “/../” щоб успішно переміщатися за будь-яким каталогів і спускатися на будь-який рівень ФС.


Давайте подивимося, як розробники Mikrotik це виправили:


Порівняння псевдо-З коду

Тепер вихід з циклу перевірки відбувається при першому ж виявленні “.. “. Правда, мені не зовсім зрозуміло, навіщо додали перевірку входження однієї точки. А з-за зміни механізму активації користувача devel, на жаль, немає можливості подивитися це у динаміці.

Підіб’ємо підсумок

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

Враховуючи попередні пункти, ми без проблем можемо: створювати, видаляти, читати і записувати файли, а так само створювати довільні директорії

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

Так само дана уразливість може стати відмінною заміною для відомої раніше можливості активації режиму розробника, адже перезавантажувати пристрій, робити backuprestore файлу конфігурації тепер не потрібно.