
Разбор рехостинга прошивки роутера Tenda AC15 и воспроизведения эксплойта удалённого выполнения команд (CVE-2020-10987).
В этом материале я показываю, как именно я эмулировал веб-сервер прошивки AC15 (V15.03.05.19) с помощью QEMU, сделал его доступным из браузера хоста и использовал уязвимый обработчик /goform/setUsbUnload (CVE-2020-10987) для выполнения команд внутри эмулируемой корневой ФС.
squashfs из образа прошивки и начать реверсСначала нам нужно получить образ прошивки. Мне не удалось найти ссылку на загрузку образа AC15 V15.03.05.19, однако я нашёл репозиторий GitHub с уже извлечённой файловой системой squashfs.
Если бы у нас был правильный файл образа, мы могли бы извлечь его с помощью binwalk вот так:```
user@computer $ binwalk -e AC15_V15.03.05.19.bin
64 0x40 TRX firmware header, little endian, image size: 6778880 bytes, CRC32: 0x80AD82D6, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1A488C, rootfs offset: 0x0 92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4177792 bytes 1722572 0x1A48CC Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5052332 bytes, 848 inodes, blocksize: 131072 bytes, created: 2017-04-19 16:18:08
user@computer $ cd _AC15_V15.03.05.19.bin.extracted
Поскольку у нас нет нужного файла образа, но есть репозиторий с уже извлечённой файловой системой, мы можем просто клонировать репозиторий.```
git clone https://github.com/lapinpt/Tenda-AC15-Firmware-V15.03.05.19-9061
Я создал каталог VR и склонировал этот репозиторий внутри него. Мой rootfs находится по пути $HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs
Отлично, теперь у нас есть прошивка AC15 V15.03.05.19. Давайте перейдём к реверсу, чтобы понять, что здесь происходит.
Для реверса я буду использовать Ghidra 11.4.2.
Перейдём к нашему целевому бинарнику rootfs/bin/httpd и загрузим его в Ghidra.
Если мы перейдём к функции formsetUsbUnload, то увидим:```C
uVar1 = FUN_0002bd4c(param_1,"deviceName",&DAT_000f4bdc);
doSystemCmd("cfm post netctrl %d?op=%d,string_info=%s",0x33,3,uVar1);
FUN_0002c6cc(param_1,"HTTP/1.0 200 OK\r\n\r\n");
FUN_0002c6cc(param_1,"{"errCode":0}");
FUN_0002cc14(param_1,200);
return;
Это и есть уязвимость. Параметр `deviceName` напрямую передаётся в `doSystemCmd`, что позволяет нам отправлять любые команды, какие захотим.
Теперь, поскольку мы собираемся перехостить эту прошивку с помощью qemu, а не на оригинальном железе роутера, некоторые программы будут пытаться обратиться к устройствам, которых нет, и ронять наш запуск.
Так как наша цель — эксплуатировать веб-сервер (`httpd`), я сосредоточился только на перехостинге этого бинарника, а не всего процесса запуска (`/rootfs/etc_ro/init.d/rcS`). _Оглядываясь назад, я не уверен, что это было правильное решение_
Итак, просматривая `rcS`, я хотел найти всё, что `rcS` мог бы делать и что понадобилось бы `httpd`. Ближе к концу файла мы видим:```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS запускает cfmd в фоновом режиме непосредственно перед запуском остального стека.
После дополнительного исследования и изучения того, как уязвимая функция отправляет команду, я пришёл к выводу, что:
httpd формирует cfm postcfm общается с cfmd через UNIX-сокет (например, /var/cfm_socket)В процедуре InitServer в cfmd мы можем видеть```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
Он создает UIX-сокет и прослушивает его.
В целом работает это так: обработчик читает фиксированный кадр размером 0x7e0 байт от каждого клиента (`RecvMsg`/`SendMsg`). Первые 4 байта — код команды; затем идет 512-байтный буфер ключа и 1500-байтный буфер значения (размеры объектов на стеке видны в обработчике). Он коммутирует по опкоду и отвечает кодом ACK:
- `2` -> Get: `GetCfmValue(key, value)` затем код ответа `3`
- `0` -> Set: `SetCfmValue(key, value)` затем код ответа `1`
- `0x11` -> Unset: `UnSetCfmValue(key)` затем код ответа `0x12`
- `10` -> Commit: `SaveCfm2Flash()` затем ответ `0x10` (OK) или `0xB` (ошибка)
Итак, чтобы эмулировать `cfmd`, я создал короткий скрипт `cfm_stub````C
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define SOCK_PATH "/var/cfm_socket"
// minimal UNIX-domain server that httpd expects.
// Replies with an IP string when it sees the key it asks for.
int main(void) {
int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {0};
if (s < 0) { perror("socket"); return 1; }
unlink(SOCK_PATH);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path)-1);
if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
if (listen(s, 5) < 0) { perror("listen"); return 1; }
for (;;) {
int c = accept(s, NULL, NULL);
if (c < 0) { if (errno==EINTR) continue; perror("accept"); break; }
char buf[1024]; ssize_t n = read(c, buf, sizeof(buf));
if (n > 0) {
// In some builds httpd asks for "lan.webiplansslen" etc.
// Any non-empty reply that looks like an IP keeps init happy.
const char *reply = "192.168.0.1";
write(c, reply, strlen(reply));
}
close(c);
}
close(s);
return 0;
}
Я покажу, как скомпилировать это, после следующей части.
Следующий вспомогательный файл — hooks.so. Есть несколько функций, используемых в httpd и cfm, которые пытаются взаимодействовать с несуществующим оборудованием.
Ниже приведено то, что наша программа предполагает при запуске:
/dev/nvram) и возвращает разумные значения по умолчанию.Следующие функции становятся проблемой, поэтому нам приходится патчить их с помощью LD_PRELOAD=/hooks.so.
get_flash_type() -> если возвращается 4, код использует файловый путь (cfm_file_init), в противном случае он пытается обратиться к MTD (которого у нас нет).get_cfm_blk_size_from_cache() (а также вариант j_get_cfm_blk_size_from_cache) используется для определения размера блока конфигурации.bcm_nvram_get) не должны завершаться сбоем, иначе стек предполагает, что «NVRAM уничтожена», и переходит к логике восстановления/перезагрузки.load_l7setting_file() и restore_power(), будут успешными, но они обращаются к несуществующему оборудованию/файлам.Вот наш hooks.c. Авторство оригинала принадлежит azeria-labs.```C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
int j_get_cfm_blk_size_from_cache(const int i) { puts("j_get_cfm_blk_size_from_cache called....\n"); return 0x20000; // 128 KiB block — what the file path expects }
int get_flash_type() { puts("get_flash_type called....\n"); return 4; // force file-backed CFM init, not MTD }
int load_l7setting_file() { puts("load_l7setting_file called....\n"); return 1; // pretend Layer-7 settings loaded OK }
int restore_power(int a, int b) { puts("restore_power called....\n"); return 0; // success (don’t touch RF/power hardware) }