• 本文作者: 
  • |
  • 2020年12月14日
  • |
  • 未分类
  • |

Java反序列化系列 ysoserial Jdk7u21

1.Jdk7U21漏洞简介

谈到java的反序列化,就绕不开一个经典的漏洞,在ysoserial 的payloads目录下 有一个jdk7u21,以往的反序列化Gadget都是需要借助第三方库才可以成功执行,但是jdk7u21的Gadget执行过程中所用到的所有类都存在在JDK中,JRE版本<=7u21都会存在此漏洞

2.Jdk7u21漏洞原理深入讲解

2.1漏洞执行流程

整体的恶意对象的封装整理成了脑图,如下图所示

1

这里用到了TemplatesImpl对象来封装我们的恶意代码,其封装和代码执行的流程在《Java 反序列化系列 ysoserial Hibernate1》中针对这种利用已经进行了详细的讲解,基本原理是通过动态字节码生成一个类,该类的静态代码块中存储有我们所要执行的恶意代码,最终通过TemplatesImpl.newTransformer()实例化该恶意类从而触发其静态代码块中的恶意代码,关于TemplatesImpl的详细分析可以去查看java 反序列化系列 Hibernate1中去学习了解。

首先最外层的是LinkedHashSet 类,看过该类源码的同学应该都清楚,该类其实是基于HashMap实现的。我们首先来看LinkedHashSet的readObject方法。

该方法最后可以看到有一个for循环,将LinkedHashSet对象在序列化时一个一个被序列化的元素在反序列化回来。该循环体中有一行代码 map.put(e,PRESENT) 这里的map变量指向的是一个LinkedHashMap对象,PRESENT常量的值是一个空的Object对象由下图可知

2

此时的变量e指向的是我们实现封装进LinkedHashSet里的TemplatesImpl对象,里面存有我们的恶意代码

3

接下来我们来看LinkedHashMap.put方法的实现

大概流程就是判断其key值的hash是否一致 如果不一致则证明是一个新的元素从而加入到当前的HashMap对象中,如果hash一致则进行判断该元素是否存在于当前的HashMap中如果存在则返回oldValue,如果不存在则加入当前HashMap对象中。

这里核心关键点就是如何让程序执行到key.equals,此时的key指向的是我们通过动态代理生成的Proxy对象,我们知道调用Proxy对象的任何方法,本质上都是在调用,InvokcationHandler 对象中被重写的invoke方法。因为生成Proxy对象时传入的参数是InvokcationHandler的子类AnnotationInvocationHandler,所以自然要调用AnnotationInvocationHandler.invoke()方法。

我们来看该方法的具体实现4

通过观察代码我们可以看到接下来会调用equalsImpl()方法,传入的var3参数是封装了我们恶意代码的TemplatesImpl对象

在这里我们可以看到有这么一行代码var8 = var5.invoke(var1);这里就会调用TemplatesImpl.newTransformer()从而实例化恶意类,这里的var1我们清楚是我们传递进来的TemplatesImpl对象,但是var5的结果是怎么来的还需要分析一下。

从代码中可以看到Method var5 = var2[var4]; var4=0 而var2= this.getMemberMethods();

跟入getMemberMethods()方法

该方法会循环获取AnnotationInvocationHandler.type中的方法,我们可以看到type对象指向了一个Templates.class对象

5

 

Templates是一个接口,该接口中只有两个抽象方法

6

所以getMemberMethods()方法返回的结果就是两个Method对象,一个是newTransformer的Method对象,一个是getOutputProperties的Method对象,这样我们是如何通过反射调用的TemplatesImpl.newTransformer()方法的逻辑就清晰了

7

 

2.2 如何构造满足条件的hash值

但是有一个问题还没有解决,那就是刚才所讲的所有代码逻辑,都要在key.equals(k)可以执行的前提下才可以,那么究竟怎样才能执行key.equals(k)呢,我们来重新看一遍LinkedHashMap.put方法的部分实现

可以看到 需要满足一些条件 才可以执行到key.equals(k)接下来就详细讲一讲如何才能满足以上这些条件,这是笔者个人觉得整个漏洞利用中最难也是最让人拍案叫绝的思路。

首先第一次调用map.put()时传入的参数e是我们封装了恶意代码的TemplatesImpl对象,另一个参数就是一个空的Object对象

8

由下图代码可知,我们需要计算出key 也就是恶意TemplatesImpl对象的hash值

9

深入看hash方法的实现

这里调用TemplatesImpl.hashCode()方法来得出hash值然后进行固定的异或操作,得出的最终结果进行返回,下面的截图中就是此次运算得出的hash值

10

接下来通过indexFor()函数 得到其hash索引 这里返回的索引值是12,并将值符給变量i 这里传入的table.legth,table是一个Entry数组,用来存放我们通过map.put()传入的键值对,并作为后续判断新传入的键值对和旧键值对是否重复的依据

11

接着就开始了第一次判断,首先当前table变量指向的Entry对象是空的,所以自然e 为null 在这里就不符合了,所以循环体内的代码不会执行

 

跳过for循环体,然后计数器自增,并将此TemplatesImpl对象本身,还有其Hash值和索引放入到之前说到的table变量中。

12

接下来就开始第二次循环了,第二次传入的key就是触发TemplatesImpl.newTransformer()的媒介 Proxy对象了这个对象里有我们特意封装进去的AnnotationInvocationHandler对象。

13

接下来问题就来了首先for循环中要满足e不为空,这就要求这次循环并计算Proxy对象从而得出的Hash值和Hash索引必须和上一次循环中的TemplatesImpl对象相同,这样才能在Entry<K,V> e = table[i]这一步中,从table中取到对应索引的对象赋值給e,从而满足e != null

那怎么才能让两个连类型都不相同的对象通过运算却能得出一样的hash值呢?接下载关键点就来了,也就是我们为什么生成Proxy对像时要传入AnnotationInvocationHandler对象。

在计算Proxy对象的hash值的时候 我们看到最终是通过调用Proxy.hashCode()来计算hash值

14

Proxy是一个动态代理对象,所以经过对调用方法名称的判断,最终调用AnnotationInvocationHandler.hashCodeImpl()方法

15

以下是hashCodeImpl方法的实现,此时的var2是一个Iterator对象,用来遍历memberValues对象中存储的键值对

可以看到memberValues中只有一个键值对就是,就是我们在初期通过反射生成AnnotationInvocationHandler对象时传入的HashMap对象中的那个键值对 key是一个字符串”f5a5a608″ Value值适合第一次循环时用来计算hash值的同一个TemplatesImpl对象

 

16

17

18

 

我们在看一看var3此时的值。

19

 

AnnotationInvocationHandler计算hash最关键的是这一段代码。简单来说就是127乘var3 key的hash值,然后和var3的value值的hash值进行异或操作

下面贴出memberValueHashCode方法的关键代码,返回var3的value值也就是TemplatesImpl对象的Hash值。

至此所得到的结果都是和第一次循环时得到的Hash值相同,但接下来就要解决如果在经过与127 * ((String)var3.getKey()).hashCode()进行异或操作后,保持结果不变。

我们知道0和任何数字进行异或,得到的结果都是被异或数本身。所以我们要让127 * ((String)var3.getKey()).hashCode()的结果等于0 也就是(String)var3.getKey()).hashCode()的值要为零

还记得我们var3的 key是什么么?是一个字符串 值为”f5a5a608″ 这个字符串非常有意思我们看一下这个字符串的hash值是多少

20

21

结果是0,完全符合我们的要求,这样127乘以0自然结果是0,0在同TemplatesImpl对象的hash值进行异或,得到的结果自然也是TemplatesImpl对象的hash值本身。这样就符合我们的要求。通过了LinkedHashMap.put方法中的for循环的判断,由于hash值相同,所以计算出的索引相同,e的值就为之前的TemplatesImpl对象,所以e不为null 结果为true

 

接下来好要通过if 判断中的前两个条件,因为&& 和|| 有短路效果,所以这三个条件我们要符合e.hash == hash为true (k = e.key) == key为flase

首先e.hash == hash是将第一次循环时的TemplatesImpl对象的hash取出同第二次循环时TemplatesImpl对象的hash进行对比,本来就都是同一个对象,所以自然时相同的,所以结果为true

(k = e.key) == key 将第一次循环时的key取出和第二次循环时的key做比对看是否相同,第一次循环的key是TemplatesImpl对象,而第二次循环时key时Proxy对象,所以结果为flase

如此这般,我们就通过了前两个判断条件,接下来自然就会执行key.equals(k)从而调用TemplatesImpl.newTransformer()方法并最终触发我们的恶意代码

至此jdk7u21漏洞原理分析完毕

 

3.总结

此次Jdk7u21 payload中作用到的所有类,均存在于JDK自身的代码中,无需再调用任何第三方jar包,所以当时爆出漏洞时影响极大。只要目标系统中使用的jdk版本并存在反序列化数据交互点就会存在远程代码执行漏洞。漏洞的触发点在LinkedHashSet,其实我们看代码的时候可以看到LinkedHashSet里面的方法都是调用了其父类HashSet中的方法,但是之所以不直接用HashSet的原因是LinkedHashSet里数据的下标和我们插入时的顺序一样,而HashSet顺序就不一样了。通过Hash值的匹配,然后执行到key.equals(k)最终执行到TemplatesImpl.newTransformer()方法

Written by