专栏名称: 黑伞安全
安全加固 渗透测试 众测 ctf 安全新领域研究
目录
相关文章推荐
巴比特资讯  ·  前高管组团创业,OpenAI没有竞业协议? ·  2 天前  
巴比特资讯  ·  因为AI,阿里终于被市场看作一家要做102年 ... ·  2 天前  
巴比特资讯  ·  杨植麟和梁文锋,论文撞车了 ·  3 天前  
巴比特资讯  ·  第一批DeepSeek开发者,已经开始逃离了 ·  3 天前  
51好读  ›  专栏  ›  黑伞安全

Java反序列化系列 ysoserial Jdk7u21

黑伞安全  · 公众号  ·  · 2020-05-28 18:32

正文

0x01 Jdk7U21漏洞简介


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


0x02 Jdk7u21漏洞原理深入讲解



漏洞执行流程


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

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

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

    private void readObject(java.io.ObjectInputStream s)        throws java.io.IOException, ClassNotFoundException {        // Read in any hidden serialization magic        s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this) instanceof LinkedHashSet ? new LinkedHashMap(capacity, loadFactor) : new HashMap(capacity, loadFactor));
// Read in size int size = s.readInt();
// Read in all elements in the proper order. for (int i=0; i E e = (E) s.readObject(); map.put(e, PRESENT); } }}

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

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

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

public V put(K key, V value) {    if (key == null)        return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    for (Entry e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }
modCount++; addEntry(hash, key, value, i); return null;}

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

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

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

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

private Boolean equalsImpl(Object var1) {    if (var1 == this) {        .....    } else {        Method[] var2 = this.getMemberMethods();        int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) { Method var5 = var2[var4]; String var6 = var5.getName(); Object var7 = this.memberValues.get(var6); Object var8 = null; AnnotationInvocationHandler var9 = this.asOneOfUs(var1); if (var9 != null) { var8 = var9.memberValues.get(var6); } else { try { var8 = var5.invoke(var1); ......

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

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

private Method[] getMemberMethods() {    if (this.memberMethods == null) {        this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction() {            public Method[] run() {                Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();




    
                AccessibleObject.setAccessible(var1, true);                return var1;            }        });    }

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


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

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


如何构造满足条件的hash值



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


public V put(K key, V value) {    if (key == null)        return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    for (Entry e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {        ......        }

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

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

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

深入看hash方法的实现

final int hash(Object k) {    int h = 0;    if (useAltHashing) {        if (k instanceof String) {            return sun.misc.Hashing.stringHash32((String) k);        }        h = hashSeed;    }
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);}

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

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

/** * Returns index for hash code h. */static int indexFor(int h, int length) {    return h & (length-1);}

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

for (Entry e = table[i]; e != null; e = e.next)

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

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

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

for (Entry e = table[i]; e != null; e = e.next)

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

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

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

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

private int hashCodeImpl() {    int var1 = 0;
Entry var3; for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Entry)var2.next(); }
return var1;}

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

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







请到「今天看啥」查看全文