
Writeup pour le réhébergement du firmware du routeur Tenda AC15 et l'exécution de commandes à distance (CVE-2020-10987) pour la réplication d'exploit.
Ce compte-rendu montre exactement comment j'ai émulé le serveur web du firmware AC15 (V15.03.05.19) avec QEMU, l'ai rendu accessible depuis le navigateur hôte, et exploité le gestionnaire vulnérable /goform/setUsbUnload (CVE-2020-10987) pour exécuter des commandes dans le rootfs émulé.
squashfs de l'image du firmware et commencer le rétro-ingénierieD'abord, nous devons obtenir notre image du firmware. Je n'ai pas pu trouver le téléchargement de l'image pour AC15 V15.03.05.19, mais j'ai pu trouver un dépôt GitHub avec le système de fichiers squashfs déjà extrait.
Si nous avions le fichier image correct, nous pourrions l'extraire en utilisant binwalk comme suit,```
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
Puisque nous n'avons pas le fichier image correct mais un repo avec le filesystem déjà extrait, nous pouvons simplement cloner le repo.```
git clone https://github.com/lapinpt/Tenda-AC15-Firmware-V15.03.05.19-9061
J'ai créé un répertoire appelé VR et cloné ce dépôt à l'intérieur. Mon rootfs se trouve dans $HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs
Parfait, nous avons maintenant notre firmware AC15 V15.03.05.19 firmware. Passons à du reverse engineering pour voir ce qui se passe.
J'utiliserai Ghidra 11.4.2 pour mon reverse engineering.
Naviguons vers notre binaire cible ici rootfs/bin/httpd et chargeons-le dans Ghidra.
Si nous allons à la fonction formsetUsbUnload, nous pouvons voir,```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;
Voici la vulnérabilité. Le paramètre `deviceName` est passé directement dans `doSystemCmd`, ce qui nous permet d'envoyer les commandes de notre choix.
Maintenant, comme nous allons réhéberger ce firmware en utilisant qemu et non le matériel du routeur d'origine, certains programmes vont tenter d'atteindre des périphériques inexistants et planter notre démarrage.
Notre objectif étant d'exploiter le serveur web (`httpd`), je me suis concentré uniquement sur le réhébergement de ce binaire, et non de l'intégralité du démarrage (`/rootfs/etc_ro/init.d/rcS`). _A posteriori, je ne suis pas sûr que cela ait été la bonne décision._
Donc, en examinant `rcS`, je voulais trouver tout ce que `rcS` pourrait faire dont `httpd` aurait besoin. Vers la fin du fichier, on peut voir :```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS démarre cfmd en arrière-plan juste avant que le reste de la pile ne s'active.
Après quelques recherches supplémentaires et en examinant comment la fonction vulnérable envoie la commande, je suis arrivé à la conclusion que,
httpd construit un cfm postcfm communique avec cfmd via un socket de domaine UNIX (par exemple /var/cfm_socket)Dans la routine InitServer de cfmd, on peut voir```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
Il crée un socket UIX et écoute.
Globalement, le fonctionnement est que le gestionnaire lit une trame fixe de 0x7e0 octets de chaque client (`RecvMsg`/`SendMsg`). Les 4 premiers octets sont un code de commande ; ensuite il y a un tampon de clé de 512 octets et un tampon de valeur de 1500 octets (vous pouvez voir les tailles des objets de la pile dans le gestionnaire). Il effectue un branchement sur l'opcode et répond avec un code ACK :
- `2` -> Get : `GetCfmValue(key, value)` puis code de réponse `3`
- `0` -> Set : `SetCfmValue(key, value)` puis code de réponse `1`
- `0x11` -> Unset : `UnSetCfmValue(key)` puis code de réponse `0x12`
- `10` -> Commit : `SaveCfm2Flash()` puis réponse `0x10` (OK) ou `0xB` (erreur)
Donc pour émuler `cfmd`, j'ai créé un court 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;
}
Je montrerai comment compiler cela après la partie suivante.
Le fichier d'assistance suivant est hooks.so. Il y a quelques fonctions utilisées dans httpd et cfm qui tentent d'interagir avec du matériel inexistant.
Voici ce que notre programme suppose lors du démarrage :
/dev/nvram) et renvoie des valeurs par défaut cohérentes.Les fonctions suivantes deviennent problématiques et nous devons donc les corriger avec LD_PRELOAD=/hooks.so.
get_flash_type() -> si elle renvoie 4, le code emprunte un chemin basé sur les fichiers (cfm_file_init), sinon il tente de communiquer avec MTD (que nous n’avons pas).get_cfm_blk_size_from_cache() (ou sa variante j_get_cfm_blk_size_from_cache) est consultée pour la taille des blocs de configuration.bcm_nvram_get) ne doivent pas échouer, sinon la pile suppose que « NVRAM est détruite » et entre dans une logique de restauration/redémarrage.load_l7setting_file() et restore_power() sont censées réussir mais touchent du matériel/fichiers inexistants.Voici notre hooks.c. Merci à azeria-labs pour l'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 }