
Ce dépôt contient les résultats de mes recherches d'août 2020 sur le firmware des IPC/NVR de Tiandy. J'ai trouvé deux vulnérabilités qui pourraient être utilisées pour récupérer à distance le mot de passe administrateur et obtenir un accès root à l'appareil.
Ce dépôt contient les résultats de mes recherches d'août 2020 sur le firmware des IPC/NVR de Tiandy (ces appareils sont également vendus sous le nom OMNY). Ces « recherches » n'étaient pas exhaustives, mais j'ai trouvé plusieurs méthodes pour récupérer le mot de passe administrateur à distance, activer telnet et changer le mot de passe root.
Il est difficile de dire exactement quelles versions sont concernées, car nous ne pouvons télécharger que les plus récentes. Toutes ces versions téléchargeables sont concernées :
DVRS_V9.12.7.20200422
DVRS_V11.7.4.20200721
NVSS_V13.6.1.20200723
NVSS_V22.1.0.20200722
Non seulement il existe différentes branches pour différents appareils, mais certains composants sont versionnés et mis à niveau séparément, comme l'API web, où j'ai trouvé un contournement de l'authentification qui ne fonctionne que pour les versions publiées depuis mi-2019, quel que soit le numéro de version du firmware. Si vous en savez plus sur les versions concernées, votre aide serait appréciée.
Je fais ici une divulgation complète, mais c'est raisonnable. Un correctif du fournisseur (peu probable puisqu'ils ne répondent pas) ne ferait pas disparaître le problème, surtout quand aucun appareil en ligne n'a le dernier firmware (et de loin). La véritable vulnérabilité est que ces appareils sont exposés à Internet. Et c'est quelque chose que les utilisateurs finaux doivent corriger, pas Tiandy.
En bonus, j'inclus également l'outil de dépaquetage du firmware et quelques informations sur la façon d'accéder aux flux via RTSP/RTMP (bonne chance pour trouver cela dans le manuel).
D'abord, je présente les scripts :
Ensuite, j'essaie d'expliquer brièvement ce que font ces scripts et pourquoi. Je ne répète pas le code, mais j'essaie de fournir assez de contexte pour que vous puissiez comprendre le code :
Enfin, cela devient relativement technique :
Vous aurez besoin de Python 3 avec PyCrypto.
D'abord, essayez recover.py. Cela nécessite que le port 3001 soit accessible :
python3 recover.py [HOST]
si tout se passe bien, les identifiants administrateur devraient s'afficher.
Si ce port n'est pas accessible, la méthode web pourrait fonctionner. Cela nécessite l'URL :
python3 cgi_recover.py http://123.45.67.89
python3 cgi_recover.py https://123.45.67.89
Si rien de tout cela ne fonctionne, vérifiez si telnet est activé. Si c'est le cas, vous pouvez obtenir directement un accès root à l'appareil, il suffit de casser ce hash :
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(assurez-vous d'ouvrir une PR si vous arrivez à le casser :D)
Dans les anciens firmwares NVR V7, vous pouvez exécuter des commandes directement :
python3 ftpupdate.py [host] [adminpass] '[cmd]'
mais cela ne produit aucune sortie. Pour faciliter cela, j'ai inclus ce raccourci :
python3 ftpupdate.py [host] [adminpass] adduser [username] [password]
Cela ajoutera un autre utilisateur avec l'uid 0.
D'abord, activez telnet en utilisant :
python3 telnet.py [host] [adminpw]
ou pour les appareils récents :
python3 cgi_recover.py [host] telnet
Ensuite, vous pouvez écraser /etc/passwd (je suppose que vous savez comment cela fonctionne). Essayez d'abord filetransport.py (pour les NVR) :
python3 filetransport.py [host] [adminpass] put /etc/passwd <[source_file]
cela ne donne aucun retour, vous devez tester en essayant de vous connecter...
Pour les modèles IPC où filetransport.py ne fonctionne pas, essayez upgrade_rw.py :
python3 upgrade_rw.py [url] [adminpass] /etc/passwd <[source_file]
(cela ne donne aucun retour non plus)
Enfin, pour les appareils encore plus récents, cela peut aussi être fait via l'API web :
python3 cgi_recover.py [url] write /etc/passwd <[source_file]
Si aucune de ces méthodes n'a fonctionné (vérifiez si vous pouvez vous connecter), réessayez toutes ces méthodes mais écrasez /config/etc/passwd à la place. Dans certaines versions du firmware, /etc/passwd est un lien symbolique vers ce fichier. Enfin, vous pouvez aussi essayer d'écraser /tdfs/etc/passwd, mais après cela, un redémarrage de l'appareil pourrait être nécessaire ; pour redémarrer, utilisez :
python3 reboot.py [host] [adminpass]
Les anciens firmwares V7 (IPC et NVR) ne semblent pas être concernés, mais si vous avez le mot de passe administrateur, pour les NVR il y a une RCE authentifiée (ftpupdate.py), et pour les IPC, le script upgrade-rw.py peut être utilisé pour écraser /etc/passwd.
Les versions ultérieures du firmware NVR (V9 et V11) comportent un compte par défaut qui, combiné à une élévation de privilèges « passive », permet de récupérer le mot de passe administrateur. Nous pouvons ensuite écraser /etc/passwd avec filetransport.py.
Bien que le compte par défaut ne soit pas présent dans le firmware IPC, une autre méthode de récupération apparaît : la méthode PSW. C'est un mécanisme de récupération de mot de passe sans aucune sécurité. Il est présent dans toutes les versions téléchargeables du firmware depuis V9. Alors que filetransport.py ne fonctionne que sur les NVR, upgrade_rw.py atteint le même objectif sur les IPC en utilisant le mécanisme de mise à niveau, nous pouvons donc toujours obtenir l'accès root.
Le firmware de 2019 introduit un autre vecteur d'attaque : un contournement de l'authentification via l'API web. En exportant le fichier de configuration sans authentification, nous pouvons récupérer le mot de passe et préparer un paquet de mise à niveau pour écraser des fichiers arbitraires.
En parlant de vulnérabilités, il y en a 4 :
Notez que je n'ai pas étudié les fonctions « cloud », c'est-à-dire s'il est possible d'énumérer les appareils et donc de se connecter à des appareils non exposés à Internet (comme c'est le cas avec les appareils Xiongmai).
Dans les anciennes versions, telnet est activé par défaut et voici ce que l'on trouve dans le fichier /etc/passwd :
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(le mot de passe root est mis à jour dynamiquement ; je n'ai pas non plus cassé ce hash, donc les pull requests sont plus que bienvenues :D)
L'utilisateur support (présent en réalité dans toutes les versions du firmware) peut sembler non privilégié, mais bien sûr, cet utilisateur a suffisamment de privilèges pour lire le mot de passe Admin et écraser les scripts d'initialisation inscriptibles par tous dans /etc/init.d ou même en créer de nouveaux :)
Conceptuellement, la méthode est très simple. Nous envoyons un paquet de connexion et lisons la réponse. C'est tout, car la réponse « connexion réussie » contient les identifiants de tous les utilisateurs, quels que soient nos privilèges. C'était déjà le cas dans V7, mais pratiquement cette méthode n'est devenue utile que lorsque le compte par défaut a été introduit dans le firmware NVR. Le « Default » inamovible n'a aucun privilège à distance, donc vous ne pouvez rien faire avec. Enfin, peut-être sauf lire le mot de passe administrateur...
Bien que cela semble trivial, cela n'a pas été si trivial à implémenter. Un protocole personnalisé est utilisé pour la communication, et les mots de passe sont chiffrés avec DES mais avec les bits inversés (la partie la plus difficile a été de comprendre cela), avec une clé transmise par le serveur. Comme il n'y a pas de dérivation de clé, un espion pourrait facilement tout déchiffrer. Néanmoins, il faut encore comprendre que les bits sont inversés, ou réimplémenter le tout à partir de zéro...
Voir la fonction recover_with_default dans le fichier recover.py pour l'implémentation.
En analysant le binaire, il est difficile de ne pas remarquer ce mécanisme. Son but unique est de... rendre possible la récupération de mot de passe, et il fait bien ce travail. Trop bien, dirais-je...
Que se passe-t-il ici ? Je pense que cela était destiné à être un mécanisme de récupération de mot de passe, probablement créé pour que les fournisseurs puissent offrir un moyen aux propriétaires d'appareils de récupérer leurs mots de passe.
Je peux émettre l'hypothèse que le processus était censé être le suivant :
Tout cela est bien, sauf qu'il manque une chose... où est la sécurité ? Nulle part, semble-t-il. Rien ne nous empêche d'envoyer ce paquet, de dériver le code de sécurité et de récupérer le mot de passe de n'importe quel appareil accessible.
Je trouve cela étonnant, car ce n'est pas comme s'il y avait une faille dans le mécanisme qui défait la sécurité. Elle est simplement absente. Il n'y a rien à corriger, mais cela ne ressemble pas non plus à une porte dérobée évidente pour moi. Cela laisse des traces dans les journaux et comporte 3 schémas de dérivation différents, chacun plus sophistiqué que le précédent. Cela a pris du temps à implémenter...
Pour en revenir à la méthode, pour que cela fonctionne, l'appareil doit avoir un téléphone/e-mail associé. En regardant l'ancienne version, j'ai vu que seul l'administrateur pouvait le faire, mais j'ai ensuite trouvé un contournement. Étonnamment, dans le firmware plus récent, ce contournement n'est plus nécessaire, car il est explicitement possible de changer l'e-mail de l'appareil sans authentification. Et c'est là que je pense que la porte dérobée était censée se trouver :)
Sur le plan technique, ce mécanisme est en réalité assez compliqué et a été le plus difficile à rétro-concevoir et à réimplémenter. Il existe 3 versions de ce mécanisme, chacune utilisant un algorithme différent pour dériver le code de sécurité. Outre le DES avec bits inversés, un chiffrement de substitution personnalisé avec une clé codée en dur est impliqué. Mais tout cela pour rien, car Tiandy ne peut pas rendre un chiffrement symétrique avec une clé codée en dur sûr, quel que soit l'effort fourni.
Une chose que j'ai observée est que, comme le code de sécurité change chaque minute, il est possible que le processus d'origine échoue simplement parce que le code a changé entre le paquet de l'étape 3 et celui de l'étape 5, quel que soit le peu de temps écoulé entre l'envoi de ceux-ci. J'ai pris cela en compte pour que mon script réessaie le processus si le code s'avère invalide.
Tout le processus, y compris la définition de l'e-mail (que nous n'avons pas besoin de posséder), est implémenté dans le fichier recover.py.
Celui-ci fonctionne avec les versions de firmware plus récentes (2019 et après) qui ont l'interface web « moderne » (celle avec la « carte ». J'aime cette carte même si l'Australie semble un peu déformée).
Ce contournement est simple. Alors que la plupart des points de terminaison de l'API sont authentifiés, il existe quelques exceptions. Cependant, la vérification permettant de savoir s'il faut ignorer l'authentification et la correspondance du point de terminaison à activer sont implémentées à des endroits différents. Dans la plupart des cas, l'authentification est ignorée lorsque le chemin de l'URL est égal à une chaîne donnée, ce qui est sécurisé.
Mais les versions récentes introduisent une autre exception, qui est activée lorsque les chaînes Record/DownLoad et ID= existent simplement quelque part dans le chemin de l'URL.
Pour les points de terminaison où le chemin complet est comparé, cela reste sécurisé. Puisque Security/users est l'un de ces points de terminaison, nous ne pouvons pas récupérer le mot de passe directement. Heureusement pour nous, le point de terminaison d'exportation de la configuration est sélectionné en vérifiant si le chemin de l'URL commence par une chaîne donnée (via strncmp), nous pouvons donc simplement ajouter ces chaînes au chemin et exporter le fichier de configuration.
La récupération du mot de passe à l'aide de cette faille est implémentée dans cgi_recover.py.
Théoriquement, l'administrateur peut mettre à niveau le firmware, et le firmware n'est ni signé ni chiffré. Mais avons-nous vraiment besoin de préparer un paquet de firmware personnalisé ? Parfois non. Parfois oui, et j'ai été assez fou pour l'implémenter...
Si vous avez le mot de passe, il y a une vulnérabilité d'injection de commande que vous pouvez utiliser : le script ftpupdate.py. Ce qui se passe, c'est que nous demandons à l'appareil de récupérer une mise à niveau via ftp et ftpget est utilisé pour effectuer cette tâche. Sans surprise, nos paramètres vont directement dans la fonction system().
Dans le firmware NVR, le protocole binaire comporte la commande FILETRANSPORT qui fait exactement ce que son nom indique. Il n'y a en fait rien à ajouter, car vous pourriez aussi bien télécharger le SDK et utiliser la même commande. Bien sûr, je voulais la réimplémenter, donc pour voir comment cela fonctionne, regardez filetransport.py.
Les modèles IPC n'ont cependant pas cette commande, mais comme je l'ai dit plus tôt, nous pouvons toujours mettre à niveau le firmware. Bien que préparer la flash complète soit irréaliste, les paquets de mise à niveau de Tiandy nous permettent de remplacer des fichiers individuels, ce qui est exactement ce dont nous avons besoin (voir le dépaquetage du firmware).
Seulement, ce n'est pas si simple. Le format de fichier « box » contient des métadonnées, puis un tableau de fichiers. Le premier fichier doit être nommé ProductModule et doit contenir des paramètres d'appareil correspondants, sinon la mise à niveau ne se poursuivra pas. Nous avons besoin non seulement des valeurs de ces paramètres, mais aussi de ces paramètres eux-mêmes. De plus, la partie métadonnées (y compris la version du fichier box) est également vérifiée.
Assembler cela manuellement semble irréaliste, mais heureusement il y a une autre façon. Les exports de fichiers de configuration utilisent le même format de fichier « box », avec toutes les métadonnées correspondantes et le fichier ProductModule inclus, sans le champ du type de mise à niveau.
J'ai réussi à comprendre comment remplir ce champ, et donc à implémenter ce processus. Bien que le mécanisme soit également présent dans le firmware NVR, il ne fonctionne pas de la même manière, mais il n'y a aucun intérêt à poursuivre l'analyse puisque les NVR ont la commande FILETRANSPORT décrite précédemment.
Le script upgrade_rw.py utilise le processus de mise à niveau. Le nom suggère cependant quelque chose de plus... C'est parce que lors de l'exportation du fichier de configuration, nous spécifions quels fichiers exporter par nom et, sans surprise, n'importe quel fichier fonctionne. Nous pouvons donc télécharger la box et ensuite lire ce fichier en utilisant le même code que celui utilisé pour extraire le firmware.
L'export et la mise à niveau peuvent également être effectués via l'API web. Dans ce cas, il n'est pas possible de lire des fichiers arbitraires. J'ai quand même voulu implémenter cela car l'API semble plus stable, voir cgi_recover.py.
Sur les modèles prenant en charge FTP, il pourrait être possible d'injecter des commandes shell dans le mot de passe utilisateur (lorsqu'une commande est exécutée pour ajouter cet utilisateur afin qu'il puisse se connecter via FTP).
Les appareils Tiandy ont le port 3001 ouvert. C'est le port nécessaire pour les méthodes non-web. Les modèles plus récents ont le RTSP sur le port 9100 en plus du port 554, et ils ont aussi le RTMP sur le port 1935. Les modèles IPC utilisent le port 8082 pour ONVIF. HTTP et HTTPS fonctionnent sur leurs ports standard.
Les appareils avec l'ancienne interface web (utilisant notre bien-aimée technologie ActiveX) contiennent l'un des éléments suivants dans leur réponse HTTP :
<title>Net Video Browser</title>
tdvideo.css
Les appareils avec la nouvelle interface web (cette fois en utilisant... Flash) contiennent ceci dans la réponse :
res/app-0.1.0.css
(l'en-tête Last-Modified se fait un plaisir de nous révéler la date de publication exacte)
Nous pouvons également identifier ces appareils par le certificat, bien que HTTPS ne soit pas toujours activé. Il n'y a que deux certificats utilisés pour tous les appareils et ils peuvent être trouvés dans le firmware téléchargé, ce qui rend leur épinglage inutile.
Le plus ancien :
C=CN, ST=Tianjin, O=Tiandy Tech, CN=dvr_ui
Le plus récent :
C=CN, ST=Tianjin, L=Tianjin, O=Tiandy Tech Ltd, CN=NetDevice
Au fait... Le support HTTPS est implémenté via un processus stunnel séparé. Cela fonctionne, mais sans surprise, l'adresse IP est perdue en cours de route, donc les journaux indiquent toujours 127.0.0.1.
Tiandy prétend que ses appareils prennent en charge RTSP et RTMP. C'est cool, mais ce que nous ne trouvons pas dans le manuel, c'est comment utiliser réellement ces protocoles, car nous ne trouvons pas les URL RTSP et RTMP nécessaires. Heureusement, j'ai cette information comme sous-produit de l'analyse, je peux donc la partager.
Pour NVR :
Pour voir le flux en direct du canal C (commençant à 1) avec le type de flux S (1, 2, 3) :
rtsp://username:password@host/C/S
Pour IPC :
Pour voir le flux en direct avec le type de flux S :
rtsp://username:password@host/S
Les URLs RTMP ne sont pas si simples car elles nécessitent un hash personnalisé pour que la requête puisse être authentifiée. Cependant, RTMP permet également de lire le contenu enregistré.
L'URL pour le flux en direct est :
rtmp://host/live/C/S/authstring
où C est le canal, S est le type de flux.
L'URL pour la lecture est :
rtmp://host/vod/START-STOP/C/S/authstring
où START et STOP sont des horodatages Unix.
authstring est calculé comme suit :
base64("username:"+md5("username:password")+":unix_timestamp")
L'outil rtmpauth.py peut le générer :
python3 rtmpauth.py username password
Comme cet horodatage est vérifié et que la différence ne peut pas dépasser 2 jours, il y a des limites :
Les mises à niveau du firmware sont emballées dans un format de fichier « box » propriétaire qui n'est ni signé ni chiffré. Ce format est en réalité très simple du point de vue du dépaqueteur. Il y a un en-tête que nous sautons, puis un tableau de fichiers à dépaqueter, où chaque fichier a un en-tête de taille fixe contenant le nom du fichier et sa taille (deux fois), puis les données suivent.
L'outil unbox.py dépaquette le fichier dans un répertoire nommé comme le fichier box ou dans le répertoire spécifié :
python3 unbox.py [box_file]
python3 unbox.py [box_file] [target_dir]
Cet outil devrait être sûr à utiliser (j'ai écrit cette ligne, puis j'ai revérifié l'outil et trouvé une vulnérabilité... oups) car les chemins absolus sont convertis en chemins relatifs, .. est remplacé par __ et il n'y a pas de liens symboliques.
Parfois, vous devrez exécuter cet outil deux fois, car vous remarquerez que le fichier contenu dans un .box est un autre .box.