• 本文作者: 天融信安全应急响应中心
  • |
  • 2020年12月14日
  • |
  • 未分类
  • |

Java反序列化系列 ysoserial Hibernate1

1.Hibernate简介

Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的JaveEE架构中取代CMP,完成数据持久化的重任。

2.Java动态字节码生成

通过分析Hibernate1 playload 的构造过程 使用了Java的动态字节码生成的技术,这里针对该技术来提前进行一下讲解

什么是动态字节码生成,相信大家听字面意思也能大致有个概念,众所周知java是编译型语言,所有的.java文件最终都要编译成.class后缀的字节码形式。

那我们可不可以绕过.java直接操纵编译好的字节码呢?当然可以,java的反射机制就是在程序运行期去操纵字节码从而获得像方法名,属性名,构造函数,等等并对其进行操作。

当然这个只是对已经编译好的类来进行操作,我们可不可以在java运行期让程序自动生成一个.class字节码文件,其实说是生成,给我的感觉更多像是组装一个.class文件

当然也是可以的,Java为我们提供了两种方式。

ASM :直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。

Javassit: 提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低。

javassit是一个第三方jar包我们可以通过maven以以下方式导入

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。

ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。

CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。

CtMethods:表示类中的方法。

CtFields :表示类中的字段。 接下来通过代码来进行演示

执行后的结果,可以看到在对应的目录下生成了我们输入的类名“JavassistTestResult”同名的class文件

2

我们看一看该class文件的源码

3

可以看到该类的代码与我们调用javassist所示所输入的内容完全相同,该class文件就是我们通过调用javassist所提供的类与方法在运行时期动态生成的。

我们测试一下动态生成的类是否真的可用

以下是执行结果,可以确定我们动态生成的类是确实可用的。

5

以上就是对javassist这个动态字节码生成技术的一些简介。

3.Hibernate1 源码深度解析

首先先看一下生成playload的最主要的一段代码

6

挑一些比较关键的点进行讲解,首先先看Gadgets.createTemplatesImpl()方法

7

以下是该方法的详细实现代码,我们来仔细观察,首先是通过TemplatesImpl.class实例化了一个TemplatesImpl对象,紧接着就是用到了我们刚才讲的动态字节码生成javassist

我们先看一下最终生成的.class的一个结果,这个新生成的字节码中有三个比较关键的点,首先是实现了Serializable接口,这点自不必多说,其次是继承自AbstractTranslet类,这点很关键在后续执行恶意代码时起关键作用,当然最最重要的就是这个手动加入的静态代码块,我们都知道静态代码块在类被加载的时候就会执行,整个类的生命周期中就只会执行一次。所以只需要将这个动态生成的类实例化的话就会自动执行Runtime.exec()函数 。接下来的操作就是将动态生成的类转化成字节数组的形式赋值给之前已经实例化好的TemplatesImpl对象的_bytecodes属性。同时为TemplatesImpl对象的_name和_tfactory属性赋值。

接下来的就是一系列针对恶意代码的封装操作,不是很难,但是特别繁琐,所以我画了一个脑图来帮助大家进行理解。最终GetObject执行完成后封装出来的结果是一个HashMap对象,对 没有错,这次反序列化的触发点,就是我们最常用的HashMap。HashMap在被序列化然后反序列化的过程中,经过一系列的嵌套调用最终触发了我们封存在TemplatesImpl对象的_bytecodes属性中的那个动态生成类的静态代块。

8

首先通过脑图观察最后返回的HashMap有两个属性被赋了值,size属性和table属性。而table属性里存放的是一个HashMap$Entry对象,我们都知道HashMap$Entry对象其实就是一对键值对的映射,这个映射对象的key和value存储的是同一个TypedValue对象,其实经过分析,value可以为任意值的。这个TypedValue类是存在org.hibernate.engine.spi包中的。

接下来我们进行调试分析

既然是使用jdk自带的反序列化,那么自然会调用HashMap的readObject方法

9

这个段代码里有两个需要注意的点,首先是1128行的代码mappings变量中存储的就是我们之前为HashMap对象的size属性所赋的值。下一个需要注意的点事1153行的for循环,此处是读取出我们之前为HashMap$Entry对象里的Key和Value

10

然后调用HashMap.putForCreate()方法将Key和Value传递进去。 这里就牵扯到了之前生成HashMap对象时为何要为size属性赋值,如果当初没有为size属性赋值,那么此时mappings变量就会为0,导致i<mappings判断失败,从而无法执行后续内容。

紧接着判断Key是否为空,Key不为空所以执行HashMap.hash()方法来处理key

11

在第351行我们调用了之前封装好的TypedValue对象的hashCode()方法

12

我们看到hashCode()方法里又调用了ValueHolder对象的getValue()方法。

13

可以看到hashcode变量的来历,是TypedValue对象被反序列化时调用initTransients方法所赋值的,里面存储的其实一个匿名内部类实例化的对象。

14

15

我们看一下valueInitializer变量的值.可以看到就是我们刚才所说的匿名内部类所实例化的对象。

16

自然而然接下来就是调用匿名内部类的initialize()方法。由于value的存储着一个TypedValue对象所以执行type.getHashCode() , 通过脑图可知type变量中存储的是一个ComponentType对象,所以调用ComponentType.getHashCode()方法并将value变量传入。

17

紧接着第242行调用getPropertyValue()方法。这里同理propertySpan是我们创建这个对象时通过反射赋的值,不能为0,如果为零则不会执行后续内容。

18

第414行调用PojoComponentTuplizer.getPropertyValue()方法。由于PojoComponentTuplizer类没有该方法所以会调用其父类的getPropertyValue()方法

19

20

 

这里的gatter变量存储的就是我们之前封装好的Gatter数组根据脑图可以看到该数组里存储的是一个BasicPropertyAccessor$BasicGetter对象。所以接下来调用BasicPropertyAccessor​$BasicGetter.get()方法

21

22

我们观察脑图中的BasicPropertyAccessor$BasicGetter里面的属性信息。可以看到method变量是我们提前赋好了值得是TemplatesImpl.getOutputProperties() 的method对象所以这里通过反射调用。

23

紧接着调用newTransformer()方法

24

触发点就藏在getTransletInstance()这个回调函数中,

25

 

这里也说明了为什么一开始要为TemplatesImpl的_name属性赋一个值,因为如果不赋值的话,在第一个if判断处就会直接返回null

26

27

最关键的就是第380行我们通过反射实例化了_class这个Class数组对象中下标为0的Class对象,就最终触发了我们的恶意代码。

28

29

那这个Class数组对象中下标为0的Class对象究竟是什么?是不是我们之前封装在TemplatesImpl的_bytecode属性中的那个通过javassist动态生成的类呢?这需要我们退一步去看上一步的defineTransletClasses()方法。

30

在defineTransletClasses()方法内我们看到有这么一个for循环。其中defineClass可以从byte[]还原出一个Class对象,所以当下这个操作就是将_bytecode[ ]中每一个byte[ ]都还原成Class后赋值给_class[ ],又因为_bytecode[ ]中下标为0的byte[ ]存储的正是包含了恶意代码的动态生成的类。所以_class[0]就是其Class对象。而_class[0].newInstance就是在实例化我们存有恶意代码的类。自然就会触发其静态代码块中存放的“Runtime.getRuntime().exec(“open /Applications/Calculator.app”);”。至此ysoserial Hibernate1反序列化代码执行原理分析完毕

31

4.总结

整个Hibernate1的整体流程就是,首先使用HashMap来作为一个触发点,接下来需要用到的是hibernate-core包中的TypedValue类,AbstractComponentTuplizer类,PojoComponentTuplizer类,BasicPropertyAccessor$BasicGetter类以及AbstractType类和ComponentType类。利用这类中的一些互相调用的方法,作为调用链。但是最终执行代码的是com.sun.org.apache.xalan下的TemplatesImpl,因为我们所写的恶意代码最终是存储在该类的_bytecode属性中。

Written by 天融信安全应急响应中心