
Writeup sobre rehospedagem de firmware do roteador Tenda AC15 e replicação de exploit de execução remota de comandos (CVE-2020-10987).
Este write-up mostra exatamente como emulei o servidor web do firmware AC15 (V15.03.05.19) com QEMU, tornei-o acessível a partir do navegador do host e explorei o handler vulnerável /goform/setUsbUnload (CVE-2020-10987) para obter execução de comandos dentro do rootfs emulado.
squashfs da imagem do firmware e começar a fazer engenharia reversaPrimeiro precisamos obter nossa imagem de firmware. Não consegui encontrar o download da imagem para AC15 V15.03.05.19, porém consegui encontrar um repositório no github com o sistema de arquivos squashfs já extraído.
Se tivéssemos o arquivo de imagem correto, poderíamos extraí-lo usando binwalk da seguinte forma:```
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
Como não temos o arquivo de imagem correto, mas um repositório com o sistema de arquivos já extraído, podemos simplesmente clonar o repositório.```
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;
Esta é a vulnerabilidade. O parâmetro `deviceName` é passado diretamente para `doSystemCmd`, permitindo que enviemos quaisquer comandos que quisermos.
Agora, como vamos re-hospedar este firmware usando qemu e não o hardware do roteador original, alguns programas vão tentar alcançar dispositivos que não existem e travar nossa inicialização.
Como nosso objetivo é explorar o servidor web (`httpd`), eu foquei apenas em re-hospedar esse binário, não toda a inicialização (`/rootfs/etc_ro/init.d/rcS`). _Em retrospectiva, não tenho certeza se essa foi a decisão certa_
Então, ao olhar para `rcS`, eu queria encontrar qualquer coisa que `rcS` pudesse fazer que `httpd` precisaria. Perto do final do arquivo, podemos ver:```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS inicia o cfmd em segundo plano logo antes do restante da stack subir.
Após mais algumas pesquisas e ao observar como a função vulnerável envia o comando, cheguei à conclusão de que,
httpd constrói um cfm postcfm fala com o cfmd por meio de um socket de domínio UNIX (ex.: /var/cfm_socket)Na rotina InitServer no cfmd, podemos ver```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
Ele cria um socket UIX e escuta.
No geral, o funcionamento é: o handler lê um frame fixo de 0x7e0 bytes de cada cliente (`RecvMsg`/`SendMsg`). Os primeiros 4 bytes são um código de comando; depois há um buffer de chave de 512 bytes e um buffer de valor de 1500 bytes (você pode ver os tamanhos dos objetos de stack no handler). Ele faz switch no opcode e responde com um código ACK:
- `2` -> Get: `GetCfmValue(key, value)` em seguida responde com o código `3`
- `0` -> Set: `SetCfmValue(key, value)` em seguida responde com o código `1`
- `0x11` -> Unset: `UnSetCfmValue(key)` em seguida responde com o código `0x12`
- `10` -> Commit: `SaveCfm2Flash()` em seguida responde `0x10` (OK) ou `0xB` (erro)
Então, para emular `cfmd`, criei um pequeno 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;
}
Vou mostrar como compilar isso depois da próxima parte.
O próximo arquivo auxiliar é hooks.so. Há algumas funções usadas em httpd e cfm que tentam interagir com hardware inexistente.
A seguir está o que nosso programa assume ao iniciar:
/dev/nvram) e retorna padrões sensatos.As seguintes funções tornam-se um problema e, portanto, precisamos corrigir com LD_PRELOAD=/hooks.so.
get_flash_type() -> se retornar 4, o código segue um caminho baseado em arquivo (cfm_file_init), caso contrário, tenta falar com MTD (que não temos).get_cfm_blk_size_from_cache() (e/ou a variante j_get_cfm_blk_size_from_cache) é consultada para dimensionamento do bloco de configuração.bcm_nvram_get) não devem falhar, ou a pilha assume "NVRAM destruída" e entra na lógica de restauração/reinicialização.load_l7setting_file() e restore_power() devem ter sucesso, mas tocam em hardware/arquivos inexistentes.Aqui está nosso hooks.c. Créditos ao azeria-labs pelo original.```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) }