SQL注入

SQL注入

  • 概念

  • 危害

  • 原理

  • 实例

  • 防御

  • 基础

      - ### SQL语句所用符号
    
    • 不同数据库的sql注入与提权

    • 常见SQL注入语句构造方法

  • 工具

  • 参考文献


概念

  • SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。
  • 根据相关技术原理,SQL注入可以分为平台层注入代码层注入。前者由不安全的数据库配置数据库平台的漏洞所致;后者主要是由于程序员对输入未进行细致地过滤,从而执行了非法的数据查询。

SQL注入产生的原因

基于上文,SQL注入的产生原因通常表现在以下几方面:

  • ① 不当的类型处理;
  • ② 不安全的数据库配置;
  • ③ 不合理的查询集处理;
  • ④ 不当的错误处理;
  • ⑤ 转义字符处理不合适;
  • ⑥ 多个提交处理不当。

危害

  • 数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。
  • 网页篡改:通过操作数据库对特定网页进行篡改。
  • 网站被挂马,传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
  • 数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
  • 服务器被远程控制,被安装后门。经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
  • 破坏硬盘数据,瘫痪全系统。

通过典型的SQL注射漏洞,黑客是可以根据所能控制的内容在SQL语句的上下文导致不同的结果的,这种不同主要体现在不同的数据库特性上和细节上。
同时,后端的数据库的不同导致黑客能利用SQL语句进行的操作也并不相同,因为很多的数据库在标准的SQL之外也会实现一些自身比较特别的功能和扩展,常见的有:

  • Sqlserver的多语句查询
  • Mysql的高权限可以读写系统文件
  • Oracle经常出现的一些系统包提权漏洞

即使一些SQL注射本身无法对数据本身进行一些高级别的危害,譬如一些数据库里可能没有存储私密信息,利用SQL查询的结果一样可能对应用造成巨大的灾难,因为应用可能将从数据库里提取的信息做一些其他的比较高危险的动作,譬如进行文件读写,这种本身无价值的数据和查询一旦被应用本身赋予较高的意义的话,可能一样导致很高的危害。
评估一个SQL注射的危害需要取决于注射点发生的SQL语句的上下文,SQL语句在应用的上下文,应用在数据库的上下文,综合考虑这些因素来评估一个SQL注射的影响,在无上述利用结果的情况下,通过web应用向数据库传递一些资源要求极高的查询将导致数据库的拒绝服务,这将是黑客可能能进行的最后的利用。


原理

应用为了和数据库进行沟通完成必要的管理和存储工作,必须和数据库保留一种接口
目前的数据库一般都是提供api以支持管理,应用使用底层开发语言如Php,Java,asp,Python与这些api进行通讯。
对于数据库的操作,目前普遍使用一种SQL语言(Structured Query Language语言,SQL语言的功能包括查询、操纵、定义和控制,是一个综合的、通用的关系数据库语言,同时又是一种高度非过程化的语言,只要求用户指出做什么而不需要指出怎么做),SQL作为字符串通过API传入给数据库,数据库将查询的结果返回。
数据库自身是无法分辨传入的SQL是合法的还是不合法的,它完全信任传入的数据,如果传入的SQL语句被恶意用户控制或者篡改,将导致数据库以当前调用者的身份执行预期之外的命令并且返回结果,导致安全问题。


那么恶意用户如何控制传入的SQL语句的呢?
我们知道,既然传入的SQL是以字符串的方式传入的,这个字符串由应用生成,那么如果应用生成这个字符串的方式不对将可能导致问题,譬如:

$sql="select * from members where userid=".$_GET[userid];

$sb->query($sql); 

这段代码的逻辑是根据用户请求的Userid进入数据库查询出不同的用户并且返回给用户,可以看到最终传入的字符串有一部分是根据用户的输入来控制的。


一旦用户提交

poc.php?userid=1 or 1=1

最终进入程序之后传入数据库的逻辑将是

$sb->query("select * from members where userid=1 or 1=1");

用户完全可以根据传入的内容来控制整个SQL的逻辑,实现间接控制和管理数据库的目的,这种命令(SQL语句)和数据(用户提交的查询)不分开的实现方式导致了安全漏洞的产生。
由于不同的开发语言可能对api进行了不同的封装,并且各种语言内部对数据的校验会有不同的要求,譬如java和python属于变量强类型并且各种开发框架的流行导致出现SQL注射的几率较小,php属于弱类型不会对数据进行强制的验证加上过程化的程序编写思路导致出现注射的几率会较大。


实例

我们通过一个具体的例子来理解
这里我们以pubs数据库作为例子。(pubs 示例数据库以一个图书出版公司为模型,用于演示 Microsoft® SQL Server™ 数据库中可用的许多选项。该数据库及其中的表经常在文档内容所介绍的示例中使用。)


pubs实例数据库实体关系图描述

pubs实例数据库实体关系图描述


数据介绍

数据介绍


我们通过Web页面查询job表中的招聘信息。
实现Web程序,它根据工作Id(job_id)来查询相应的招聘信息,示意代码如下:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        // Gets jobId from http request.
        string queryString = Request.QueryString["jobid"];
        if (!string.IsNullOrEmpty(queryString))
        {
            // Gets data from database.
            gdvData.DataSource = GetData(queryString.Trim());

            // Binds data to gridview.
            // DataBind在ASP.net中是很重要的东西,
            //几乎所有的控件都需要它来控制数据的操作。
            //也可以说是ASP.net的数据核心。
            // 详细说明:http://www.cnblogs.com/Garden-blog/archive/2011/04/21/2023399.html
            gdvData.DataBind();
        }
    }
}

完成Web程序后查询相应招聘信息

查询相应招聘信息

如图所示,我们查询到了数据库中工作Id值为1的工作信息,而且在页面显示了该工作的Id,Description,Min Lvl和Max Lvl等信息。

SQL示意代码如下:

SELECT     job_id, job_desc, min_lvl, max_lvl
FROM         jobs
WHERE     (job_id = 1)

假设现在要求我们获取jobs表中的所有数据,而且必须保留WHERE语句,那我们只要确保WHERE恒真就好了,SQL示意代码如下:

SELECT     job_id, job_desc, min_lvl, max_lvl
FROM         jobs
WHERE     (job_id = 1) OR 1 = 1

上面我们使得WHERE恒真,所以该查询中WHERE已经不起作用了,其查询结果等同于以下SQL语句。

SELECT     job_id, job_desc, min_lvl, max_lvl
FROM         jobs

SQL查询代码实现如下:

string sql1 = string.Format( "SELECT job_id, job_desc, min_lvl, max_lvl FROM jobs WHERE job_id='{0}'", jobId);

现在我们要通过页面请求的方式,让数据库执行我们的SQL语句,我们要在URL中嵌入恶意表达式1=1(或2=2等等),如下URL所示:

http://localhost:3452/ExcelUsingXSLT/Default.aspx?jobid=1'or'1'='1 

等效SQL语句如下:

SELECT job_id, job_desc, min_lvl, max_lvlFROM jobs WHERE job_id = '1' OR '1' = '1'

jobs表查询结果

jobs表查询结果


现在我们把job表中的所有数据都查询出来了,仅仅通过一个简单的恒真表达式就可以进行了一次简单的攻击。

虽然我们把job表的数据都查询出来了,但数据并没有太大的价值,所以接着我们要找出该表真正表名。
首先我们假设表名就是job,然后输入以下URL:

  http://localhost:3452/ExcelUsingXSLT/Default.aspx?jobid=1' or 1=(select count(*) from job)-- 

等效SQL语句如下:

SELECT job_id, job_desc, min_lvl, max_lvl FROM jobs WHERE job_id='1' or 1=(select count(*) from job) --'

job表查询结果

当我们输入了以上URL后,结果服务器返回我们错误信息,这证明了我们的假设是错误的。
通过错误信息,我们可以知道,首先它证明了该表名不是job,而且它还告诉我们后台数据库是SQL Server,不是MySQL或Oracle,这也涉及一个漏洞,即把错误信息直接返回给了用户。


接下假定表名是jobs,然后输入以下URL:

http://localhost:3452/ExcelUsingXSLT/Default.aspx?jobid=1'or1=(select count(*) from jobs) -- 

等效SQL语句如下:

SELECT job_id, job_desc, min_lvl, max_lvl FROM jobs WHERE job_id='1' or 1=(select count(*) from jobs) --'
jobs表查询结果

现在证明了该表名是jobs,这可以迈向成功的一大步,因为我们知道了表名就可以对该表进行增删改操作了,而且我们还可以猜测出更多的表对它们作出修改。


防御

防止SQL Injection总的来说有以下几点:

  1. 永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对单引号和双"-"进行转换等。
  2. 永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取。
  3. 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
  4. 不要把机密信息明文存放,请加密密码和敏感的信息。
  5. 应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装,把异常信息存放在独立的表中。

通过正则表达校验用户输入

通过正则表达式校验用户输入数据中是包含特殊字符,然后继续校验输入数据中是否包含SQL语句的保留字,如:WHERE,EXEC,DROP等。
正则表达式定义如下:

private static readonly Regex RegSystemThreats =
        new Regex(@"\s?or\s*|\s?;\s?|\s?drop\s|\s?grant\s|^'|\s?--|\s?union\s|\s?delete\s|\s?truncate\s|" +
            @"\s?sysobjects\s?|\s?xp_.*?|\s?syslogins\s?|\s?sysremote\s?|\s?sysusers\s?|\s?sysxlogins\s?|\s?sysdatabases\s?|\s?aspnet_.*?|\s?exec\s?",
            RegexOptions.Compiled | RegexOptions.IgnoreCase);

上面我们定义了一个正则表达式对象RegSystemThreats,并且给它传递了校验用户输入的正则表达式。


通过前面正则表达式来校验用户输入是否合法,由于.NET已经帮我们实现了判断字符串是否匹配正则表达式的方法——IsMatch(),所以我们这里只需给传递要匹配的字符串就好了。
示意代码如下:

public static bool DetectSqlInjection(string whereClause)
{
    return RegSystemThreats.IsMatch(whereClause);
}
public static bool DetectSqlInjection(string whereClause, string orderBy)
{
    return RegSystemThreats.IsMatch(whereClause) || RegSystemThreats.IsMatch(orderBy);
}

在页面中添加校验功能。

protected void Page_Load(object sender, EventArgs e){
    if (!IsPostBack){
        // Gets departmentId from http request.
        string queryString = Request.QueryString["jobId"];
        if (!string.IsNullOrEmpty(queryString)){
            if (!DetectSqlInjection(queryString) && !DetectSqlInjection(queryString, queryString)){
                // Gets data from database.
                gdvData.DataSource = GetData(queryString.Trim());

                // Binds data to gridview.
                gdvData.DataBind();
            }
            else{
                throw new Exception("Please enter correct field");
            }
        }
    }
}

当我们再次执行以下URL时,被嵌入的恶意语句被校验出来了,从而在一定程度上防止了SQL Injection。

http://localhost:3452/ExcelUsingXSLT/Default.aspx?jobid=1'or'1'='1 
sqlinjection9

但使用正则表达式只能防范一些常见或已知SQL Injection方式,而且每当发现有新的攻击方式时,都要对正则表达式进行修改,这是吃力不讨好的工作。


sql语句预编译和绑定变量

采用sql语句预编译和绑定变量,是防御sql注入的最佳方法

String sql = "select id,no from user where id=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, id);
ps.executeQuery();

如上所示,就是典型的采用 sql语句预编译和绑定变量 。
为什么这样就可以防止sql 注入呢?


采用了PreparedStatement,就会将sql语句:"select id, no from user where id=?" 预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,无论你输入的是什么,都不会影响该sql语句的语法结构了,因为语法分析已经完成了,而语法分析主要是分析sql命令,比如 select ,from ,where ,and, or ,order by 等等。
所以即使你后面输入了这些sql命令,也不会被当成sql命令来执行了,因为这些sql命令的执行, 必须先通过语法分析,生成执行计划,既然语法分析已经完成,已经预编译过了,那么后面输入的参数,是绝对不可能作为sql命令来执行的,只会被当做字符串字面值参数。所以sql语句预编译可以防御sql注入。
但有一些场景必须采用字符串拼接的方式,此时,应该严格检查参数的数据类型,可以使用一些安全函数,来方式sql注入。


使用参数化查询避免

cmd.CommandText="select count(*) from 表名 where username=@a and password=@b";
cmd.parameters.Add(new SqlParameter("a",".."));
cmd.parameters.Add(new SqlParameter("b",".."));


基础

SQL注入攻击的总体思路

  1. 寻找到SQL注入的位置
  2. 判断服务器类型和后台数据库类型
  3. 针对不通的服务器和数据库特点进行SQL注入攻击

SQL语句所用符号

操作符 用途 例子
+ - 表示正数或负数,正数可省去+ -1234.56
+ 将两个数或表达式进行相加 A=c+b
- 将两个数或表达式进行相减 34-12
* 将两个数或表达式进行相乘 12*34
/ 除以一个数或表达式 18*11
NULL 空值判断 Where name is null;

操作符 用途 例子
|| 字符串连接 ‘101-’ tel_num
= 等于测试 Select * from emp where name=’赵元杰’;
!= 或<>或^= 不等于测试 Select * from emp where name !=’赵元杰’;
< 小于测试 Select * from emp Where sal < 5000;
> 大于测试 Select * from emp Where sal > 5000;
<= 小于等于测试 Select * from emp Where sal <= 5000;
>= 大于等于测试 Select * from emp Where sal >= 5000;

操作符 用途 例子
Not in 测试某值是否在一个指定的结果集中 Select name,addr from expert where local not in(‘北京’,’上海’);
ANY 将一个值与一组值进行比较,返回满足条件的结果。必须跟!=,<,>,<=,>= select ename,sal from emp where sal<= any(select sal from emp where deptno=10)
SOME 同ANY,必须跟!=,<,>,<=,>=
ALL 将一个值与一组值比较,返回满足条件的所有列值。必须跟!=,<,>,<=,>= Select name,sal from emp Where sal<=all ( 500,800,1200);

操作符 用途 例子
Not between A and B 判断某个值是否界于两者之间。 Select name,sal from emp Where sal between 500 and 1200;
[not]exists 判断某个列是否存在于一组值中。 select dname,deptno from dept where exists(select * from emp where dept.deptno=emp.deptno)
A [not]like b [Escape ‘char’] 比较两个模式是否相似,当使用like 语句时Oracle不去访问索引。 Select * from emp Where ename like ‘TH%’;
Is [not] null 测试值是否为空。 Select ename,deptno from emp Where comm. Is null or comm.=0;

操作符 用途 例子
Not 对结果的否定。 Select * from emp Where sal not(sal<1000); 等价于 select ename,sal from emp where sal>=1000;
AND 用于判断两个条件十分都满足。 Select * from emp where Ename=’SIMTH’ and sal>=1000;
OR 用于判断两个条件中是否有一个满足。 Select * from emp where Ename=’SIMTH’ or ename=’SCOTT’;
UNION 用于返回(组合)两个查询中所有唯一的行。 Select ename from emp union Select ename from emp;

操作符 用途 例子
UNION ALL 用于返回(组合)两个查询中所有所有的行。
INTERSECT 用于返回两个查询中相同的行。 Select ename from emp1 intersect select ename from emp2;
MINUS 用于返回两个查询中的不同的行。

不同数据库的sql注入与提权

详细见


SQL手工注入

sql手工注入有两种,第一种是union联合查询,第二种是Ascii逐字解码法。

union联合查询语句如下:

首先判断一个网站有没有注入点的语句是

  • 加单引号'
  • and 1=1
  • and 1=2

and 1=1 网站返回正常,and 1=2 网站返回错误,证明有可能存在注入点。
order by语句来查询当前数据表中的列数,如果order by 11返回正常,12返回错误 那么列数就是11。
可参考sql注入之order by猜列数问题
得到数据表的列数后,接着来用 and 1=2 union select 1,2,3,4,5,6,7,8,9,10,11 from 表名 (如admin)


如果返回结果是数字如下图

20150629031350078.png

那么就说明存在admin这个表,如果出现错误就说明不存在admin表(也有可能是语句输错了),就需要换表名了,然后就是慢慢猜了(这里可以写一个脚本猜表名)。
图中用红色矩形框,框住的数字,它所对应的就是 3 4 8 10 在1,2,3,4,5,6,7,8,9,10,11 里面把 3 4 8 10 替换成admin表里存储网站管理员账号和密码的字段,
如user ,pwd 替换后就变成了语句 and 1=2 union select 1,2,3,4,5,6,7,user,9,pwd,11 from admin 如下图所示


20150629032109479.png

之前的8 和10 都被替换成了admin admin 是因为这个网站后台的用户名和密码默认的就是admin,这样管理员的帐户和密码就被爆了出来。


Ascii逐字解码

第一步-猜表名

and (select count(*) from 表名) >=0

如果and (select count(*) from admin) >=0 网页返回正常 说明存在admin这个表,返回不正常,即不存在。

第二步-猜字段

and (select count(字段名) from 猜到的表名)>=0

如果and (select count (user) from admin ) >=0 网页返回正常 说明存在user这个字段,返回不正常即不存在。


第三步-猜字段的位数

and (select top 1 len(字段名) from 表名) >0

如果and (select top 1 len (user) from admin) >4 返回正常 >5 返回错误,则说明字段的位数为5。

第四步 猜Ascii码的字段值

and (select top 1 asc(mid(字段,位数,1)) from 表名) >ascii(1~128)

MID 函数用于从文本字段中提取字符。

例如:and (select top 1 asc (mid(user,1,1)) from admin) >96 返回正常 97返回错误 那么这个user表里面的第一个ascii码就是97,ascii码对应的就是字母a。

user字段里面的第一位已经被猜出来了是a,对应的ascii码是97

接着猜第二位到最后一位。


user字段猜完了开始猜pwd字段也就是网站后台的密码

and (select top 1 asc(mid (pwd,1,1)) from admin) >ascii(1——128)

和猜user字段里的内容是一样的,把字段替换成pwd ,然后在猜的时候替换要猜解的位数,和ascii码.


工具

  • 火狐插件HackBar
图片.png

HackBar 小工具包,包含一些常用的工具(SQL injection,XSS,加密等),web开发人员可以利用它,快速构建一个http请求,或者用它快速实现某种算法等。


  • BurpSuite
图片.png

Burp Suite 是用于攻击web 应用程序的集成平台。它包含了许多工具,并为这些工具设计了许多接口,以促进加快攻击应用程序的过程。
所有的工具都共享一个能处理并显示HTTP 消息,持久性,认证,代理,日志,警报的一个强大的可扩展的框架。

  • Sqlmap

SQLmap是一款用来检测与利用SQL注入漏洞的免费开源工具,对检测与利用进行自动化处理(数据库指纹、访问底层文件系统、执行命令)。


参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 姓名:于川皓 学号:16140210089 转载自:https://baike.baidu.com/item/sq...
    道无涯_cc76阅读 1,872评论 0 2
  • [SQL注入攻击] SQL注入攻击是黑客对数据库进行攻击的常用手段之一。随着B/S模式应用开发的发展,使用这种模式...
    James黄杰阅读 2,557评论 0 30
  • 什么是SQL注入攻击?引用百度百科的解释: sql注入_百度百科: 所谓SQL注入,就是通过把SQL命令插入到We...
    leftshine阅读 4,939评论 0 24
  • Sql 注入基础原理介绍一、实验说明1.1 实验内容SQL注入攻击通过构建特殊的输入作为参数传入Web应用程序,而...
    FreaxJJ阅读 1,776评论 3 23
  • 当梦想已被现实强奸 当真理已被虚伪玷污 当整个世界已被黑暗笼罩 而我却企图用微茫的正义祛除黑暗 泪水欺凌着我的明眸...
    墨尔河阅读 513评论 0 3