一些注入方法
报错注入
基于floor
的报错注入:
count(*), rand(), group by
缺一不可
报错公式:
?id=2' and (select 1 from (select count(*), concat( floor(rand(0)*2),(select group_concat(table_name) from information_schema.tables where table_schema=database())x from information_schema.tables group by x)a)--+
原理解释:
count(): 计算选择结果的数目
rand(): 产生0-1之间的随机数,可以设置参数种子,设置参数种子时产生的随机数是有规律的,使用rand(0)是因为此时产生的随机数得到的0,1序列是固定的,并且恰好可以产生报错
group by: 根据某个列值进行分组输出,group by的理解需要理解它的具体执行过程
下面来看一下 floor(rand(0)*2)
产生报错的具体原理。
-
floor(rand(0)*2)
产生的序列:0 1 1 0 1 1 0...
-
select count(*) from test group by floor(rand(0)*2)
的具体执行过程数据库在执行
count(*)
的语句时,首先会生成一个虚表,按照实表中的记录的顺序对从上往下执行- 第一条记录:首先计算
floor(rand(0)*2)
,结果为0
,在虚拟表中查询,发现没有键值0
,于是再执行一次floor(rand(0)*2)
,这是第二次计算,计算结果为1
, 此时执行插入操作,插入键值为1
,count(*)
为1
,此时针对实表的第一条记录的操作完成 - 第二条记录:计算
floor(rand(0)*2)
,得到的结果为1
,在虚表中查询,发现键值为1
的记录已存在,于是对虚表进行更新,键值为1
的记录对应的count(*)
的值变为2
- 第三条记录:计算
floor(rand(0)*2)
, 这是第四次计算,结果为0
,在虚表中查询,发现没有键值为0
的记录,于是要执行插入操作。再次计算floor(rand(0)*2)
,得到的结果为1
,此时执行插入操作就会产生报错,因为键值为1
的记录早已存在。
可以看到,只要表中的记录超过三条,使用
floor(rand(0)*2)
必将报错。对于表中记录低于三条的表,也可以尝试用
floor(rand()*2)
,使用这种方法具有一定的概率性,多试几次可能会注入成功。 - 第一条记录:首先计算
还有一种报错注入是利用 updatexml()
和 executexml()
这两个函数都有三个参数,第一个参数表示xml
文件中目标替换文字,第二个参数表示目标文档的绝对路径,第三个参数表示用来替换的文字。如果第二个参数不是文件路径的格式,就会产生报错。
常见的形式为^updatexml(1, concat('~',(select database()),'~'), 1)
基于布尔的盲注
基于布尔的SQL盲注
关键就是构造逻辑表达式,一般可以利用的函数有:
left(s, 1) # 从左往右取字符串的前 1 位
right(s, 1) # 从右往左取字符串的前 1 位
substr(s, 0, 1) # 从字符串s的第 0 位开始取,取 1 位
ascii('a') # 返回字符的ascii码值
注意逻辑符号的使用(|| & ^
)
常见的形式:
ascii(substr((seletc schema_name from information_schema.schemata),0,1))=101
或
substr((select schema_name from information_schema.schemata),0,1)=chr(101)
基于时间的盲注
一般与 IF
语句一起使用,一般形式:' and IF(ascii(substr((select database()),0,1))=101, sleep(), 0)
关于利用:个人觉得只要用逻辑符把语句连在一起就行了。
union注入
union
注入适用于 sql
语句中不含 order by
且是有回显的情况
要注意的是保证 union
前后的 select
语句的字段数是相同的
select uName,email,score from user union select 1,2,password from user
宽字节注入
这种情况比较适用于过滤了'
的情况,利用宽字符注入,我们可以使得'
逃逸出来
原理如下:
' -> \' -> %5c%27
%df' -> %df\' -> %df%5c%27
在utf
编码中,%df%5c
为縗
,所以当我们输入%df'
时,解析结果%df%5c%27
,使用utf
编码,解码结果为縗'
,这样'
就被逃逸出来了
PS: utf解码的时候如果第一个字节大于128的话,就会连读两个字节,解码成一个汉字
防御宽字节注入:
-
调用
mysql_set_charset
将所有连接的字符集设置为gbk
(mysql_set_charset("gbk", $conn)
),并且对所有的输入都进行mysql_real_escape_string
,但是对于一些老的cms
,这样做还是不行的,因为很多老的cms
使用的addslashes
进行过滤,mysql_real_escape_string
的转义会考虑当前的字符集,不会出现将5c
和27
拼接在一起的情况 -
在所有
sql
语句前指定连接的形式是二进制。产生宽字节注入的原因就是mysql
在接受到客户端的信息后,直接进行gbk
解码,导致引号逃逸了出来。所以解决这个问题最根本的手段还是从编码上入手。做以下设置:SET character_set_connection=gbk, character_set_results=gbk, character_set_client=binary
当
mysql
从客户端接收到数据时,会把数据当成character_set_client
的binary
编码,然后再把数据转化character_set_connection
的gbk
编码,之后再进到响应的表中,把数据转化成数据表的具体编码,再进行数据操作。产生执行结果后,把结果转化成character_set_results
的gbk
编码。在上面的过程中,'
无法利用编码逃逸出来,自然就避免了宽字符注入。(但是为啥这样不会使结果错误呢?)- 使用
utf-8
编码
- 使用
堆叠注入
堆叠注入就是以;
为界,同时执行多个语句,与union
相比之下,优点主要是可以使用除select
之外的其他语句(insert delete create update
等)
但是缺点是不是所有的环境下都可以使用堆叠注入,受限于所使用的数据库以及API
,Oracle
在任何情况下不支持堆叠注入,会直接报错,mysql和Postgresql
在本身是支持堆叠注入的,所以就受限于API
,并且一般web
服务只返回一个信息,所以我们不能使用堆叠注入执行获取信息的操作,并且也不能知道注入语句是否执行成功。可以看到,堆叠注入的局限性比较大,使用union
进行注入的频率大于堆叠注入。
二次注入
第一次将值插入到表中,插入的时候进行转义,但是插入到表中的是原本的数据。于是在第二次引出数据的时候,如果没有做转义的话,就会直接将数据拼接在语句中产生执行
其他注入点
limit注入
注入点在 limit
后面,一般的形式有:
select * from xxx limit [注入点]
select * from xxx order by xxx limit [注入点]
对于第一种来说,我们通过 union
注入即可
但是对第二种来说,由于 order by
的存在,我们不可以使用 union
注入,查看mysql 5.x
的文档中 select
的语法:
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
可以看到 LIMIT
后面还可以有 PROCEDRUE
和 INTO
,INTO
的话暂时不考虑
考虑PROCEDURE
,MYSQL
中默认有的存储过程有 ANALYSE
,可以利用这个存储过程。
ANALYSE
存在两个参数(ANALYSE(1,1)
),考虑在参数中加入我们想要执行的注入语句
比如:
select * from xxx order by xxx limit 1,1 procedure analyse((updatexml(rand(),concat(0x3a,IF(substr(version(),0,1) LIKE 5), BENCHMARK(10000000,sha(1)),1),rand())),1)
需要注意的是,这里延迟函数不能用 sleep()
,只能使用 BENCHMARK()
,当然上面这种形式用于无回显的情况,如果有回显显示报错信息的话,那么就不必要使用时间盲注了
order by注入
注入点形式:
select * from xxx order by [注入点]
最基本的利用是,可以通过order by
来测试表中列的数目,order by + num
,如果这个num
数值大于列的数目,就会产生报错。
比较常用的是在order by
后面使用报错注入
select * from xxx order by (updatexml('1',concat('~',version()),'1'));
sql注入的防御手段
-
使用
mysql_real_escape_string()
和addslashes()
mysql_real_escape_string()
会转义' " \r \n NULL Control-Z
addslashes()
会转义' " \ NULL
这对预防万能密码比较有效,同时对字符型注入也有一定的效果,但是要注意使用
addslashes()
函数时要注意宽字符注入。同时也要注意二次注入。 -
使用预编译
将
sql
语句进行预编译,预编译语句接受参数,不直接将参数拼接在语句中,也就试输无法通过参数来改变原先的sql
语句,参数中的敏感符号也会被当作普通字符。JAVA
中常使用预编译,php
可以使用PDO
预编译另外要注意,使用
PDO
编译虽然可以提高整个的安全性,但是也存在一些局限性:PDO
预编译对于很多次的查询来说可以提高效率,但是如果是一次查询使用PDO
查询的话,PDO
就显得不太高效了- 如果忘记销毁预处理语句,就可能导致资源泄露
- 预处理语句不能使用
mysql5.0
以前版本的缓存 - 预处理语句只针对一个连接,所以另外的连接不能使用同样的句柄。出于这个原因,一个先断开再重新连接的客户端会丢失句柄。(我的理解是在用户断开重连后
PDO
会重新预编译一次,也就是句柄变了,不知道这样理解对不对)
-
使用存储过程
存储过程可以一定程度预防注入,但是要注意不要在存储过程中使用动态语句,也就是说不要出现变量拼接,一个例子:
exec("select * from Student where Sno"+@id) select * from Student where Sno=@id
上面两者的区别就是,上面直接拼接,该怎么注入还是怎么注入,但是下面的在执行时会检查
@id
的类型,如果不是int
型的话,就会报错。 -
检查数据类型
对于电话号码,年龄等等这种
int
型的数据,在操作前应该要对接收数据的类型进行检测例如:
is_numeric(),ctype_digit()
,但是这种方法也不是完全安全的,像is_numeric()
就可以通过传入数组进行绕过