Routeur TP-Link TL-WR840N
TP-Link TL-WR840N — notre cible

Partie 1 : Acquisition du firmware

Les firmwares TP-Link sont faciles à obtenir car ils sont téléchargeables gratuitement en ligne. Mais c'est un peu trop facile, non ? Y a-t-il un autre moyen de l'obtenir ?

Port série sur le PCB du routeur
Port série identifié sur le PCB

La plupart de ces appareils ont un port série pour le débogage. S'y connecter devrait être trivial avec un adaptateur USB vers UART :

Adaptateur USB vers UART
Adaptateur USB vers UART pour la communication série

Avec cela, on pourrait potentiellement interrompre la séquence de démarrage et accéder à un shell bas niveau permettant de lire la puce EEPROM à différents offsets. Mais je voulais essayer quelque chose de nouveau :

Puce EEPROM dessoudée
Puce EEPROM dessoudée du PCB

Dessouder l'EEPROM contenant le firmware était ma méthode. Je venais d'acquérir un TL866II+, et je voulais m'amuser avec ! La puce s'insère parfaitement dans l'adaptateur SOP8 200mm :

EEPROM dans le programmeur TL866II+
Puce EEPROM dans le programmeur TL866II+

Avant de pouvoir la lire, il faut identifier le type de puce :

Marquages de la puce EEPROM montrant GD25Q32
Giga Device GD25Q32 — AH1903 25Q32CS1G

On peut lire AH1903 25Q32CS1G, et le logo est Giga Device. On dump ensuite le firmware avec le fork Linux de l'outil minipro :

Bash
minipro -p GD25Q32 -r firmware.bin
Found TL866II+ 04.2.86 (0x256)
Chip ID OK: 0xC84016
Reading Code...  6.99Sec  OK

Partie 2 : Reverse engineering du firmware

Le fichier fait 4 Mo et contient les signatures suivantes :

Bash
binwalk firmware.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
-----------------------------------------------
53488         0xD0F0          U-Boot version string, "U-Boot 1.1.3 (Jun 14 2018)"
66048         0x10200         LZMA compressed data, properties: 0x5D
1048576       0x100000        Squashfs filesystem, little endian, version 4.0,
                              compression:xz, size: 2955905 bytes, 610 inodes
4063248       0x3E0010        XML document, version: "1.0"

Nous nous intéressons principalement au squashfs, qui contient le système de fichiers du routeur. Tous les binaires sont compilés pour MIPS, l'architecture la plus courante pour les routeurs.

Le dossier /etc contient plusieurs fichiers intéressants. Notamment, deux fichiers XML chiffrés : default_config.xml et reduced_data_model.xml. En recherchant ces noms de fichiers dans tout le squashfs, on obtient une correspondance intéressante : libcmm.so.

Du code compilé qui référence des fichiers chiffrés pourrait en réalité les déchiffrer. On désassemble donc la bibliothèque partagée avec Ghidra :

C
int dm_decryptFile(uint param_1, undefined4 param_2, uint param_3, int param_4)
{
  int iVar1;
  char acStack40 [8];
  int local_20;
  
  memcpy(acStack40, &encryption_key, 8);
  if (param_3 < param_1) {
    cdbg_printf(8, "dm_decryptFile", 0xb83,
      "Buffer exceeded, decrypt buf size is %u, but dm file size is %u",
      param_3, param_1);
    local_20 = 0;
  }
  else {
    local_20 = cen_desMinDo(param_2, param_1, param_4, param_3, acStack40, 0);
    // ...
  }
  return local_20;
}

Quelle chance... On a tous les symboles ! Le code copie un tableau de 8 octets dans un buffer local (les clés DES font 7+1 pour la parité) et appelle cen_desMinDo de libcutil.so, un wrapper pour les fonctions de chiffrement DES.

On obtient rapidement les résultats :

Bash
openssl enc -d -des-ecb -nopad -K XXXXXXXXXXXXXXXX -in default_config.xml > default_config_decrypted.xml

Dans ce fichier XML, on trouve du butin :

XML
<StorageService>
  <UserAccount instance=1>
    <Enable val=1 />
    <Username val=admin />
    <Password val=admin />
    <X_TP_SupperUser val=1 />
  </UserAccount>

Autre point intéressant dans /etc : shadow n'existe pas. Les mots de passe sont stockés à l'ancienne, dans le fichier passwd :

Bash
admin:$1$$iC.dUsGpxNNJGeOm1dFio/:0:0:root:/:/bin/sh
dropbear:x:500:500:dropbear:/var/dropbear:/bin/sh
nobody:*:0:0:nobody:/:/bin/sh

Voyons ce qu'en pense hashcat :

Bash
hashcat -a 0 -m 500 hash /usr/share/wordlists/rockyou.txt

$1$$iC.dUsGpxNNJGeOm1dFio/:1234

Status...........: Cracked
Hash.Name........: md5crypt, MD5 (Unix)
Time.Started.....: Wed Oct 28 14:10:47 2020 (1 sec)

admin:1234 est le mot de passe root du routeur.

Partie 3 : Dropbear

Un autre utilisateur intéressant est dropbear — un serveur SSH léger pour systèmes embarqués. En recherchant les données liées à dropbear, on trouve dans libcmm.so la chaîne /var/tmp/dropbear/dropbearpwd. Le fichier n'existe pas dans le squashfs, il est donc créé au démarrage :

C
undefined4 setDropbearLogin(int conf_obj)
{
  // ...
  __stream = fopen("/var/tmp/dropbear/dropbearpwd", "wb+");
  if (__stream != (FILE *)0x0) {
    fprintf(__stream, "username:%s\n", conf_obj + 0x22);
    password = (char *)(conf_obj + 0x32);
    length = strlen(password);
    cen_md5MakeDigest(dest, password, length);
    // ... encodage hex du hash MD5 ...
    fprintf(__stream, "password:%s\n", dest);
    fclose(__stream);
  }
  return uVar1;
}

La fonction ouvre dropbearpwd, écrit le nom d'utilisateur, hashe le mot de passe en MD5 via cen_md5MakeDigest de libcutil.so, et écrit le hash encodé en hexadécimal.

Après avoir tracé tous les appels à dm_setObj (135 références croisées), les identifiants proviennent d'un modèle de données global alimenté par le fichier XML de configuration que nous avions déchiffré :

Graphe d'appels montrant le flux des identifiants du XML vers dropbear
Graphe d'appels : les identifiants transitent de default_config.xml à l'initialisation de dropbear

Partie 4 : Injections de commandes

Tout commence avec l'utilisation prédominante de system(), qui est largement déconseillée. Mais system en lui-même n'est pas le problème — c'est la fonction util_execSystem qui en dépend, et cette fonction possède 495 références croisées dans le code.

C
  memset(command_line, 0, 0x200);
  iVar1 = vsnprintf(command_line, 0x1ff, cmd, &local_res8);
  // Aucune sanitisation
  if (0 < iVar1) {
    do {
      local_22c = system(command_line);
      // ...
    } while (waitpid);

Le buffer est mis à zéro, puis rempli par snprintf à partir de la commande passée en paramètre, et directement — sans aucune sanitisation — passé à system().

Voici un bon exemple de situation potentiellement injectable :

C
void oal_ipt_addBridgeIsolationRules(undefined4 param_1)
{
  util_execSystem("oal_ipt_addBridgeIsolationRules",
    "iptables -t filter -I BRIDGE_ISOLATION -i br+ -o %s -j DROP", param_1);

param_1 pourrait contenir le nom d'une interface réseau saisi depuis l'interface web. Étant directement concaténé, il pourrait contenir du code bash exécutable pour obtenir un accès root sur l'appareil.

Partie 5 : Aller plus loin

Il n'y a pas de réelle conclusion à tirer de cette étude. J'ai acheté ce routeur principalement pour m'amuser avec, et c'est exactement ce que j'ai fait. Les mots de passe sont faibles, le firmware n'est pas chiffré, etc. — mais on a ce pour quoi on paie.

Beaucoup de choses restent à vérifier :

  1. Que contient la zone avant le bootloader ? (une partie semble contenir la configuration matérielle)
  2. Le noyau est en 2.6 — y a-t-il des ajouts propriétaires TP-Link ?
  3. Le module tp_domain.ko essaie de contacter des domaines morts : tplinklogin.net — pourquoi ?
  4. Trouver une injection de commande exploitable via util_execSystem

Peu après la rédaction de cet article, je suis tombé sur le blog de Pierre Kim, qui présente des vulnérabilités très similaires. Je soupçonne que le code est significativement similaire.

Nous avons contacté TP-Link et créé le CVE-2020-36178 lié au problème d'injection de commandes. L'équipe sécurité de TP-Link mérite une mention spéciale — ils sont très sérieux et réactifs.

Références

Restez classe, amateurs de cybersécurité.


← Retour aux articles

Besoin d'un audit de sécurité ou d'un accompagnement sur mesure ?

Découvrir nos services →