最近做项目刚好遇到了反序列化漏洞,在项目中依赖了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,并且这个对象是可以进行序列化和反序列化的,当我们正常去写单例模式,进行序列化和反序列化后,实际上得到的已经不是一个对象了,可以通过下面的栗子进行证明。
首先写一个类,为了让这个类保持单例模式,只有通过getInstance方法才能获取到实例,不能通过构造方法创建实例。
1 | import java.io.Serializable; |
下面写一个测试代码,测试序列化后的对象和序列化之前的对象是否相等。
1 | import java.io.*; |
实际结果为false,可以证明反序列化后的对象和反序列化前的对象不是一个对象。
下面在HungrySingleton
中加上readResolve方法,再次运行项目,可以看到反序列化后的对象和序列化的对象是一个对象。
1 | private Object readResolve() { |
上面我们证明了如果存在readResolve方法,在反序列化的过程中会自动调用readResolve(),放到hibernate利用链中来讲,可以通过readResolve方法给method属性赋值,大致的调用栈如下。
1 | BasicPropertyAccessor.getSetterOrNull(Class, String) (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 | TemplatesImpl.TransletClassLoader.defineClass() |
而_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 | TemplatesImpl.getTransletInstance |
如何构造动态Class?
根据上述的需求,我们需要动态构造一个Class并且在它的无参构造方法写上我们想要执行的代码,可以通过javasist来实现,代码如下:
1 | ClassPool pool = ClassPool.getDefault(); |
生成的Class内容如下。
如何构造TemplatesImpl对象并完成调用?
上面我们已经动态构造好了需要执行的类并得到了该类的字节码,下面我们只要构造好TemplatesImpl对象并调用getOutputProperties即可完成利用。
1 | ClassPool pool = ClassPool.getDefault(); |
通过上面的分析,我们已经完成了从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 | Class<BasicPropertyAccessor.BasicGetter> clazz2 = (Class<BasicPropertyAccessor.BasicGetter>) Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter"); |
可以直接通过调用get方法来排错。
构造PojoComponentTuplizer对象
得到BasicGetter对象后,要将BasicGetter的内容赋值到AbstractComponentTuplizer的getters属性中,在AbstractComponentTuplizer。
AbstractComponentTuplizer是抽象类,不能创建对象,可以通过其子类来构建。
我们可以先创建一个PojoComponentTuplizer的实例,再通过反射修改getters字段的内容。但是使用这种方式我们需要构造一个Component对象,这个对象构造起来比较麻烦,参考ysoserial的实现,是通过reflectionFactory 在不调用构造方法的情况下创建对象。
1 | Class<PojoComponentTuplizer> clazz3 = (Class<PojoComponentTuplizer>) Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer"); |
下面通过反射修改getters属性的内容
1 | Class<AbstractComponentTuplizer> clazz4 = (Class<AbstractComponentTuplizer>) Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer"); |
可以通过调用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 | Class<ComponentType> CompType = (Class<ComponentType>) Class.forName("org.hibernate.type.ComponentType"); |
下面只要通过反射给相应的字段赋值即可。
1 | Field compTuplizer=CompType.getDeclaredField("componentTuplizer"); |
可以通过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 | Class clazz5 = Class.forName("java.util.HashMap$Node"); |
下面我们要将构造好的Node对象赋值给HashMap的某个属性即可,好像只有table属性接收Node对象,但table是transient修饰的,也就是说默认不会进行序列化这个字段。
但在writeObject中调用的internalWriteEntries中会遍历table并将其中的key和value进行序列化。
由于table需要的是Node的数组类型,因此还需要创建一个数组对Node进行封装。
1 | Object tbl = Array.newInstance(clazz5,1); |
但是这么做反序列化会有异常,因为我们没有给hashmap设置长度。所以还要给hashmap设置size属性。这个我就直接调用ysoserial自带的Reflections来进行设置了。Reflections.setFieldValue(map, "size", 1);
.
总体的测试代码如下:
1 | package ysoserial; |
总结
这个利用链是我第一次尝试去自己构造exp的链,从中也学到了很多东西,相信下次再分析其他利用链会更加容易理解吧。