如果一个字符的大小是一个字节的,称为窄字节;如果一个字符的大小是两个字节的,则称为宽字节。
像GB2321、GBK、GB18030、BIG5、Shift_JIS等这些编码都是常说的宽字节,也就是只有两个字节。
英文默认占一个字节,中午占两个字节。
原理:宽字节注入发送的位置就是PHP发送请求到MYSQL时字符集使用charater_set_client
设置一次编码。在使用PHP连接Mysql的时候,当设置"character_set_client=gbk"
时会导致一个编码转换的问题,也就是我们熟悉的宽字节注入。
宽字节注入是利用mysql的一个特性,mysql在使用GBK编码(GBK就是常说的宽字节之一,实际上只有两个字节)的时候,会认为两个字符是一个汉字(前一个ASCII码要大于128,才到汉字的范围)
GBK首字节对应0x81-0xFE
,尾字节对应0x40-0xFE(除0x7F)
,例如%df和%5C会结合;GB2312是被GBK兼容的,它的高位范围是0xA1-0xF7,低位范围是0xA1-0xFE(0x5C不在该范围内)
,因此不能使用编码吃掉%5c
常见转义函数与配置:
addslashes
、mysql_real_escape_string
、mysql_escape_string
、
php.ini中magic_quote_gpc
的配置
root %df' or 1=1 #
原理:在GBK编码中,反斜杠的编码是%5c,在输入%df后,使得添加反斜杠后形成%df%5c,而%df%5c是繁体字"連",单引号成功逃逸,爆出mysql数据库的错误。
我们先搭建一个实验环境。
源码很简单(注意先关闭自己php环境的magic_quotes_gpc):
<?php
//连接数据库部分,注意使用了gbk编码,把数据库信息填写进去
$conn = mysql_connect('localhost', 'root', 'toor!@#$') 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 tid='{$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 "{$row['title']}
{$row['content']}
\n"
;
mysql_free_result($result);
?>
</body>
</html>
SQL语句是SELECT * FROM news WHERE tid='{$id}'
,就是根据文章的id把文章从news表中取出来。
在这个sql语句前面,我们使用了一个addslashes函数,将$id
的值转义。这是通常cms中对sql注入进行的操作,只要我们的输入参数在单引号中,就逃逸不出单引号的限制,无法注入,如下图:
那么怎么逃过addslashes的限制?众所周知addslashes函数产生的效果就是,让'
变成\'
,让引号变得不再是“单引号”,只是一撇而已。一般绕过方式就是,想办法处理\'
前面的\
:
1.想办法给\
前面再加一个\
(或单数个即可),变成\\'
,这样\
被转义了,'
逃出了限制
2.想办法把\
弄没有。
我们这里的宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围)。如果我们输入%df'
看会怎样:
我们可以看到,已经报错了。我们看到报错,说明sql语句出错,看到出错说明可以注入了。
为什么从刚才到现在,只是在'
也就是%27前面加了一个%df就报错了?而且从图中可以看到,报错的原因就是多了一个单引号,而单引号前面的反斜杠不见了。
这就是mysql的特性,因为gbk是多字节编码,他认为两个字节代表一个汉字,所以%df和后面的\
也就是%5c变成了一个汉字“運”,而'
逃逸了出来。
因为两个字节代表一个汉字,所以我们可以试试%df%df%27:
不报错了。因为%df%df是一个汉字,%5c%27不是汉字,仍然是\'
。
那么mysql怎么判断一个字符是不是汉字,根据gbk编码,第一个字节ascii码大于128,基本上就可以了。比如我们不用%df,用%a1也可以:
%a1%5c他可能不是汉字,但一定会被mysql认为是一个宽字符,就能够让后面的%27逃逸了出来。
于是我可以构造一个exp出来,查询管理员账号密码:
-1%aa%27union%20select%201,2,concat(name,0x23,pass)%20from%20admin%23
gb2312和gbk应该都是宽字节家族的一员。但为什么gb2312不能注入
我们来做个小实验。把内容管理系统中set names修改成gb2312:
结果就是不能注入了:
有些同学不信的话,也可以把数据库编码也改成gb2312,也是不成功的。
为什么,这归结于gb2312编码的取值范围。它的高位范围是0xA1~0xF7
,低位范围是0xA1~0xFE
,而\
是0x5c,是不在低位范围中的。所以,0x5c
根本不是gb2312中的编码,所以自然也是不会被吃掉的。
所以,把这个思路扩展到世界上所有多字节编码,我们可以这样认为:只要低位的范围中含有0x5c
的编码,就可以进行宽字符注入。
部分cms对宽字节注入有所了解,于是寻求解决方案。在php文档中,大家会发现一个函数,mysql_real_escape_string
,文档里说了,考虑到连接的当前字符集。
于是,有的cms就把addslashes替换成mysql_real_escape_string,来抵御宽字符注入。我们继续做试验,内容管理系统v1.2:,就用mysql_real_escape_string来过滤输入:
我们来试试能不能注入:
一样没压力注入。为什么,明明我用了mysql_real_escape_string,但却仍然不能抵御宽字符注入。
原因就是,你没有指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下mysql_set_charset函数,设置当前连接的字符集为gbk。
就可以避免这个问题了:
在上面我们说到了一种修复方法,就是先调用mysql_set_charset
函数设置连接所使用的字符集为gbk,再调用mysql_real_escape_string
来过滤用户输入。
这个方式是可行的,但有部分老的cms,在多处使用addslashes来过滤字符串,我们不可能去一个一个把addslashes都修改成mysql_real_escape_string
。我们第二个解决方案就是,将character_set_client
设置为binary(二进制)。
只需在所有sql语句前指定一下连接的形式是二进制:
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
这几个变量是什么意思?
当我们的mysql接受到客户端的数据后,会认为他的编码是character_set_client,然后会将之将换成character_set_connection的编码,然后进入具体表和字段后,再转换成字段对应的编码。
然后,当查询结果产生后,会从表和字段的编码,转换成character_set_results编码,返回给客户端。
所以,我们将character_set_client设置成binary,就不存在宽字节或多字节的问题了,所有数据以二进制的形式传递,就能有效避免宽字符注入。
比如,我们的内容管理系统v2.0版本更新如下:
已经不能够注入了:
总结一下全文中提到的由字符编码引发的安全问题及其解决方案:
set names gbk
和mysql_real_escape_string是无法避免宽字符注入问题的。还得调用mysql_set_charset来设置一下字符集。