Огляд уразливості в Winbox від Mikrotik. Або великий фейл
Всім доброго часу доби, напевно багато хто вже чули про недавню уразливість в роутерах 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 спілкується з пристроєм.
Опис протоколу 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 байти за призначення змінної
Додатково за кожний пакет містить:
- спеціальні маркери для позначення початку пакета
- розмір пакета
- маркети, відповідальні за контроль великих пакетів
Знайдені константи
- 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 без проблем обробляє вхідні пакети ще до авторизації користувача
- З-за некоректної фільтра ми отримуємо доступ до будь-якого файлу
Враховуючи попередні пункти, ми без проблем можемо: створювати, видаляти, читати і записувати файли, а так само створювати довільні директорії
Так що не дивно, що маючи доступ на читання будь-яких файлів без авторизації, першим, що було зроблено, це читання файлу з паролями користувачів. Благо в мережі достатньо інформації про те, де він розташований, і як витягти з нього дані.
Так само дана уразливість може стати відмінною заміною для відомої раніше можливості активації режиму розробника, адже перезавантажувати пристрій, робити backuprestore файлу конфігурації тепер не потрібно.