Mikrotik Winbox vulnerability overview. Or a big fail

Good day to all, probably many have already heard about the recent vulnerability in Mikrotik routers, which allows to extract the passwords of all users. In this article, I would like to show and analyze in detail the essence of this vulnerability.
All material is provided for informational purposes only, so the code that exploits the vulnerability will not be here. If you are not interested in learning about the causes and internal structure of a particular vulnerability, you can read no further.

Let's start


The first thing to start with is analyzing the traffic between the Winbox client and the device.
Winbox is an application for WIndows operating system that exactly repeats the web interface and is intended for administering and configuring a device with an onboard Router OS. 2 modes of operation are supported, via TCP and UDP
Before you start, you should disable traffic encryption in Winbox. This is done as follows: you need to enable the Tools -> Advanced Mode checkbox . After that, the interface will change as follows:


Uncheck Secure Mode . We start Wireshark and try to log in to the device:


As you can see below, after authorization, the list file is being requested and then its contents are completely transmitted to us, it may seem that everything is fine, but let's take a look at the very beginning of this session:


At the very beginning, Winbox sends the exact same packet with a request for the list file :


Consider its structure:

  1. 37010035 - package size
  2. M2 is a constant denoting the beginning of a packet.
  3. 0500ff01 - variable 0xff0005 in the value True
  4. 0600ff09 01 - variable 0xff0006 in the value 1 (Number of the transmitted packet)
  5. 0700ff09 07 - variable 0xff0007 in the value of 7 (Open file in read mode)
  6. 01000021 04 6s967374 - variable 0x01000001 string list 4 bytes in size (Usually this variable is responsible for the file name)
  7. 0200ff88 02 ... 00 - array 0xff0002 with the size of 2 elements
  8. 0100ff88 02 ... 00 - array 0xff0001 with the size of 2 elements

As a result of the reverse of the protocol, and the corresponding binary files on the client and server side, it was possible to recover and understand more the structure of the protocol through which Winbox communicates with the device.

NvMessage protocol description

Types of fields (Name: Digital designation)


  • 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

Types of errors (Name: Digital designation)


  • 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 байта и непосредственно значение.

Массивы


Образно массив можно описать следующей структурой:

structArray {
    uint32 type;
    uint8 count;
    uint32 item1;
    uint32 item2;
    ...
    uint8 zero;
}

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

Строки


Строки не нуль-терминированны, а имеют четко заданную длину:

structString {
    uint32 type;
    uint8 length;
    char text[length];
}

Числа


Самый простой тип в пакете, его можно представить как тип-значение:

structu* {
    uint32 type;
    uint8/32/64 value;
}

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

Булевый тип


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

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

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


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


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

Режимы доступа к файлу

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


Now that we know how the protocol works, we can arbitrarily generate the packets we need and see how the device reacts to them.

On the device side, the executable file / nova / bin / mproxy is responsible for handling packages . Since the function names were not saved, I called the function that processes the package and makes decisions about what to do with the file_handler () file . Take a look at the function itself:


PS The code that will interest us is marked with arrows.

Step 1


When receiving a package to open a file for reading, it starts processing from this block:


At the very beginning of the package, using the function nv :: message :: get <nv :: string_id> (), the file name is extracted.

Next, the tokenize () function splits the resulting string into separate parts, using the " / " symbol as a separator .

The resulting array of strings is passed to the path_filter () function , which checks the resulting array of strings for the presence of " .. ", and in case of errors returns the error ERROR_NOTALLOWED (0xFE0009)


PS ERROR_NOTALLOWED will also be received in the response, if you do not have access rights to the file

If everything is fine, then the path to the webfig or pckg directory is concatenated to the beginning of the file name .

Step 2



If everything went well, the file is opened and its descriptor is saved to the global object.

If the file could not be opened, then in the answer we get the error: cannot open source file .


Thus, to obtain the contents of a file, 3 conditions must be met:

  1. The file path does not contain " .. ";
  2. There are rights to access the file;
  3. The file exists and can be opened successfully.

Now let's try sending several packages to test the functionality of this function:

$ ./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

So! But this is already strange ... We remember that ERROR_NOTALLOWED appears if it did not pass the check in path_filter () , otherwise we would have received a message about the absence of access rights, but in the latter case, it turns out that the file was searched for in the top level directory?

Let's try this way:

$ ./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

And it worked. But why? Let's take a look at the path_filter () function code :


From the code you can clearly see that the search for the entry " .. " is actually taking place in the resulting array of strings But further the most interesting, I highlighted in red this fragment.
The essence of this code is that: If the previous element is also " .. ", then the check is considered failed. Otherwise - assume that everything is fine.

Those. for everything to work, you just need to alternate between " /./ " and " /../ " in order to successfully navigate through any directories and go down to any FS level.


Let's see how the Mikrotik developers fixed this:


Pseudo-C code comparison


Now the exit from the test cycle occurs at the first detection of " .. ". True, I do not quite understand why they added the check of the occurrence of one point. And because of the change in the devel user activation mechanism , unfortunately, there is no way to see it in dynamics.

Summarize


  1. Router OS handles incoming packets without any problems even before user authorization
  2. Because of an incorrect filter, we get access to any file.

Considering the previous points, we can without problems: create, delete, read, and write files, as well as create arbitrary directories.

So it is not surprising that having access to read any files without authorization, the first thing that was done was to read the file with passwords. users. The benefit of the network is plenty of information about where it is located, and how to extract data from it.

Also, this vulnerability can be an excellent substitute for the previously known possibility of activating developer mode, because there is no longer a need to reboot the device, to backup / restore the configuration file.

Also popular now: