
Writeup per la replicazione dell'exploit relativo al rehosting del firmware del router Tenda AC15 e all'esecuzione remota di comandi (CVE-2020-10987).
Questo write-up mostra esattamente come ho emulato il webserver del firmware AC15 (V15.03.05.19) con QEMU, come l'ho reso raggiungibile dal browser dell'host e come ho esercitato l'handler vulnerabile /goform/setUsbUnload (CVE-2020-10987) per ottenere l'esecuzione di comandi all'interno della rootfs emulata.
squashfs dall'immagine del firmware e inizia il reversingPer prima cosa dobbiamo ottenere la nostra immagine del firmware. Non sono riuscito a trovare il download dell'immagine per AC15 V15.03.05.19, ma sono riuscito a trovare un repository GitHub con il filesystem squashfs già estratto.
Se avessimo il file immagine corretto, potremmo estrarlo usando binwalk in questo modo,```
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
Poiché non abbiamo il file immagine corretto ma un repository con il filesystem già estratto, possiamo semplicemente clonare il repository.```
git clone https://github.com/lapinpt/Tenda-AC15-Firmware-V15.03.05.19-9061
I have made a directory called VR and cloned this repo inside it. My rootfs is at $HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs
Great now we have our AC15 V15.03.05.19 firmware. Lets move onto some reversing to see whats going on.
I will be using Ghidra 11.4.2 for my reversing.
Lets navigate to our target binary here rootfs/bin/httpd and load it in Ghidra.
If we go to the formsetUsbUnload function we can see,```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;
Questa è la vulnerabilità. Il parametro `deviceName` viene passato direttamente a `doSystemCmd` consentendoci di inviare qualunque comando vogliamo.
Ora, poiché rehostaremo questo firmware usando qemu e non l'hardware originale del router, alcuni programmi proveranno a raggiungere dispositivi che non ci sono e manderanno in crash il nostro avvio.
Poiché il nostro obiettivo è sfruttare il webserver (`httpd`), mi sono concentrato solo sul rehosting di quel binario, non sull'intero avvio (`/rootfs/etc_ro/init.d/rcS`). _Col senno di poi non sono sicuro che fosse la mossa giusta_
Quindi, guardando `rcS`, volevo trovare qualsiasi cosa che `rcS` potesse fare e di cui `httpd` avesse bisogno. Verso la fine del file possiamo vedere:```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS avvia cfmd in background subito prima che il resto dello stack si avvii.
Dopo ulteriori ricerche e dopo aver osservato come la funzione vulnerabile invia il comando, sono giunto alla conclusione che:
httpd costruisce un cfm postcfm comunica con cfmd tramite un socket di dominio UNIX (ad es. /var/cfm_socket)Nella routine InitServer in cfmd possiamo vedere```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
Crea una socket UIX e resta in ascolto.
In generale, il funzionamento è che l'handler legge un frame fisso di 0x7e0 byte da ogni client (`RecvMsg`/`SendMsg`). I primi 4 byte sono un codice di comando; poi c'è un buffer per la chiave di 512 byte e un buffer per il valore di 1500 byte (puoi vedere le dimensioni degli oggetti sullo stack nell'handler). Fa lo switch sull'opcode e risponde con un codice ACK:
- `2` -> Get: `GetCfmValue(key, value)` poi risponde con codice `3`
- `0` -> Set: `SetCfmValue(key, value)` poi risponde con codice `1`
- `0x11` -> Unset: `UnSetCfmValue(key)` poi risponde con codice `0x12`
- `10` -> Commit: `SaveCfm2Flash()` poi risponde `0x10` (OK) o `0xB` (errore)
Quindi, per emulare `cfmd`, ho creato un breve script `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;
}
Mostrerò come compilare questo dopo la parte successiva.
Il prossimo file helper è hooks.so. Ci sono alcune funzioni usate in httpd e cfm che tentano di interagire con hardware inesistente.
Il seguente è ciò che il nostro programma presuppone all'avvio:
/dev/nvram) che restituisce valori predefiniti sensati.Le seguenti funzioni diventano un problema e quindi dobbiamo applicare la patch con LD_PRELOAD=/hooks.so.
get_flash_type() -> se restituisce 4, il codice prende un percorso basato su file (cfm_file_init), altrimenti prova a comunicare con MTD (che non abbiamo).get_cfm_blk_size_from_cache() (e/o la variante j_get_cfm_blk_size_from_cache) viene consultata per il dimensionamento del blocco di configurazione.bcm_nvram_get) non devono fallire, altrimenti lo stack presume “NVRAM distrutta” e procede con la logica di ripristino/riavvio.load_l7setting_file() e restore_power() devono riuscire, ma toccano hardware/file inesistenti.Ecco il nostro hooks.c. Crediti a azeria-labs per l'originale.```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 }