这篇博文详细介绍宽字节注入以及SQL注入与编码会出现的安全问题:
浅析白盒审计中的字符编码及SQL注入
在此不做搬运工,只做自己的思考和总结。
个人认为学习的正确过程是理解概念、建立概念的关系、推测、实验论证和归纳。
概念:
(1)字符(character)是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
(2)字符集(Character set)是多个字符的集合,常见的字符集:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。
GBK编码,是对GB2312编码的扩展,因此完全兼容GB2312-80标准。GBK编码依然采用双字节编码方案,其编码范围:8140-FEFE(高字节从81到FE,低字节从40到FE),剔除xx7F码位
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
(3)编码(coding)是信息从一种形式或格式转换为另一种形式的过程,也称为计算机编程语言的代码简称编码。有时指编码的过程,有时指编码的结果。
(4)php的iconv函数作用是字符串按要求的字符编码来转换。
iconv ( string $in_charset , string $out_charset , string $str ) : string
返回的结果是转换字符编码后的字符串。
测试一下:
<?php
$name = $_GET['name'];
echo urlencode($_GET['name']); // %E5%92%8C
echo '<br>';
$name = iconv('utf-8', 'gbk', $name);
echo urlencode($name); // %BA%CD
?>
浏览器访问并执行脚本:
结果分析:
浏览器对"和"做Unicode编码,所以是三个字节。当执行iconv函数后,字符编码被转换成GBK,因此"和"的编码是两个字节。
MySQL宽字节注入
<?php
header('content-type:text/html;charset=gbk');
$conn = mysql_connect('localhost', 'root', 'root') or die('bad');
mysql_select_db('test', $conn);
mysql_query('set names gbk', $conn);
$name=addslashes($_GET['name']);
echo $name.'<br>';
echo urlencode($name).'<br>';
$sql = "insert into user value('{$name}', 'test')";
mysql_query($sql, $conn) or die(mysql_error());
?>
执行结果:
结果分析:
addslashes函数在%27(单引号 ’ )前面插入一个%5c(反斜杠),然后这份数据传递给MySQL。因为MySQL执行了set names gbk,这句代码做了三件事:
set character_set_connection=gbk;
set character_set_results=gbk;
set character_set_client=gbk;
重点在于set character_set_client=gbk,这句代码决定MySQL如何解析对php传输的数据,这里设置了gbk,所以MySQL在处理数据时,将%df%5c看作一个中文字符,%5c仍存在,但反斜杠已经不存在,而单引号逃逸了。
mysql_real_escape_string函数
mysql_real_escape_string函数和mysql_set_query函数配套使用,可以防御宽字节注入,这是方法之一。怎么防御的?
<?php
header('content-type:text/html;charset=gbk');
$conn = mysql_connect('localhost', 'root', 'root') or die('bad');
mysql_select_db('test', $conn);
mysql_query('set names gbk', $conn);
mysql_set_charset('gbk');
$name=mysql_real_escape_string($_GET['name']);
echo $name.'<br>';
echo urlencode($name).'<br>';
$sql = "insert into user value('{$name}', 'test')";
mysql_query($sql, $conn) or die(mysql_error());
?>
执行结果:
结果分析:
这里虽然%27前面的反斜杠仍被“吞掉”了,但是MySQL并没有报错,因为这里显示的数据这是浏览器解析的结果。另外,mysql_real_escape_string()的转义作用会考虑到mysql_set_character()设置的字符集,所以这里%df前面也插入了%5c。当MySQL处理这份数据时,两个%5c被去掉(这里的意思是反斜杠已经起到转义作用),而在插入时,%df%27作为gbk的一个字符插入,这不是一个gbk字符,因此变成一个"?"。
如果传输的数据是%df%df%27,mysql_real_ecsape_string()将%df%df当作一个中文字符,不添加反斜杠转义,就变成%df%df%5c%27。
也就是说,mysql_real_escape_string()考虑mysql_set_charset()的意思是将无法正常解析的字符前面加反斜杠,能够正常解析的字符不加反斜杠。这样一来,从插入数据的结果来看,单引号要么被转义,要么被吞掉,而不会逃逸。(实际上都是反斜杠起到转义作用了)
character_set_client=binary
设置character_set_client=binary也能防御,这是方法之二。
<?php
header('content-type:text/html;charset=gbk');
$conn = mysql_connect('localhost', 'root', 'root') or die('bad');
mysql_select_db('test', $conn);
mysql_query("set character_set_connection=gbk,
character_set_result=gbk,
character_set_client=binary", $conn);
$name = $_GET['name'];
$name = addslashes($_GET['name']);
echo $name.'<br>';
echo urlencode($name).'<br>';
$sql = "insert into user value('{$name}', 'test')";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>
执行结果:
结果分析:
设置了character_set_client=binary之后,MySQL对php传输的数据就看成二进制形式。
这里的 binary 具有迷惑性,起初我以为二进制数据就不是字符了,%27失去了单引号的闭合作用,所以无论传输什么数据都不会破坏sql语句,但其实不是,这些数据只是被当成一个个单字节字符处理。换言之,现在%27前面的反斜杠一定会起到转义作用,它再也不会被多字节“吞掉”了。这是character_set_client=binary防御宽字节注入的原理。
因此,%df无法插入,变成"?"。
再插入一个%df:
两个%df都被转换成"?"。
iconv函数
iconv函数引起的问题是PHP层的问题,如果设置了character_set_client=binary后,多此一举添加了iconv函数就会出问题。
<?php
header('content-type:text/html;charset=gbk');
$conn = mysql_connect('localhost', 'root', 'root') or die('bad');
mysql_select_db('test', $conn);
mysql_query("set character_set_connection=gbk,character_set_result=gbk,character_set_client=binary", $conn);
$name = $_GET['name'];
$name = addslashes($_GET['name']);
echo $name.'<br>';
echo urlencode($name).'<br>';
$name = iconv('gbk','utf-8',$name); // 字符串由gbk编码转换成utf-8编码
echo urlencode($name).'<br>'.;
$sql = "insert into user value('{$name}', 'test')";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>
执行结果:
结果分析:
这里的思路是利用 iconv 函数转换编码的时候将反斜杠吞掉。%df%5c是gbk字符,转换成utf-8编码后,已经失去%5c了,%27逃逸。
为什么不是在字符串转换编码的过程,生成一个%5c转义掉%27前面的反斜杠?因为utf-8编码除了第一个字节外,后面的字节都是以 10 开头,而 %5c 是 01011100 ,所以一个中文字符无法在gbk转utf-8后得到一个%5c。
将iconv()的两个“字符集”参数换一下位置:
$name = iconv('utf-8','gbk',$name); // 字符串由gbk编码转换成utf-8编码
执行结果:
结果分析:
这里的思路是利用 iconv 函数转换编码的时候生成一个%5c。将后面紧跟着的%5c转义造,成%27逃逸。
这里为什么不是把%5c吞掉?还是那个原因,utf-8的低位字节范围没有%5c,所以不能够输入两个字节%xx%xx,将紧跟着的%5c吞掉。另外,gbk的低位字节范围中存在%5c,所以能够生成一个%5c。
总结:
总的来说,不同系统的编码不一致就会出现问题。