<! DOCTYPE html >
< meta charset = "gbk" ><!--仅用于基础的显示,换成 utf8 也行就是不好看-->
<? php
error_reporting ( 0 );
$conn = mysql_connect ( '127.0.0.1' , 'root' , '' );
mysql_select_db ( 'mysql' , $conn );
mysql_query ( "set names gbk" ); //不安全的编码设置方式 在mysql_query中设置
character_set_client$res = mysql_query ( "show variables like 'character%';" ); //显示当前数据库设置的各项字符集
while ( $row = mysql_fetch_array ( $res )){
var_dump ( $row );
}
$user = addslashes ( $_GET [ 'sql' ]); //mysql_real_escape_string() magic_quote_gpc=On addslashes() mysql_escape_string()功能类似
$sql = "SELECT host,user,password FROM user WHERE user='{$user}'" ;
echo $sql . '</br>' ;
if ( $res = mysql_query ( $sql )){
while ( $row = mysql_fetch_array ( $res )){
var_dump ( $row );
}
}
else {
echo "Error" . mysql_error (). "<br/>" ;
}
?>
前言
在mysql+PHP中,用于转义的函数有addslashes,mysql_real_escape_string,mysql_escape_string等,还有一种情况是magic_quote_gpc,不过高版本的PHP将去除这个特性。
首先,宽字节注入与HTML页面编码是无关的,笔者曾经看到
<meta charset=utf8> <而是数据库的字符集设置为了GBK等宽字节>
就放弃了尝试,这是一个误区,SQL注入不是XSS。虽然他们中编码的成因相似,不过发生的地点不同。
本文就介绍一下具体漏洞发生的原理与简单的利用。在这里我们限定使用的语言是PHP5.4,数据库MYSQL5.6。
涉及到的一些概念
字符、字符集与字符序
字符(character)是组成字符集(character set)的基本单位。对字符赋予一个数值(encoding)来确定这个字符在该字符集中的位置。
字符序(collation)指同一字符集内字符间的比较规则。
UTF8
由于ASCII表示的字符只有128个,因此网络世界的规范是使用UNICODE编码,但是用ASCII表示的字符使用UNICODE并不高效。因此出现了中间格式字符集,被称为通用转换格式,及UTF(Universal Transformation Format)。
宽字节
GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节。宽字节带来的安全问题主要是吃ASCII字符(一字节)的现象。<一个字符与ASCII的一个字符形成一个二个字节的宽字节>
MYSQL的字符集转换过程
1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:
? 使用每个数据字段的CHARACTER SET设定值;
? 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
? 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
? 若上述值不存在,则使用character_set_server设定值。
将操作结果从内部操作字符集转换为character_set_results。
重点:宽字节注入发生的位置就是PHP发送请求到MYSQL时字符集使用character_set_client设置值进行了一次编码。
PHP--<发生在这个位置>--character_set_client-- character_set_connection-- 内部操作字符集-- CHARACTER SET<数据字段、表、库、 character_set_server >-- character_set_results
PHP测试代码:
http://localhost/xl.php?sql=root%df%27%20or%201=1%23
是可以执行成功的!
URL解码sql=root?’ or 1=1#
解析过程:
$_GET[‘sql’] 经过 addslashes编码之后带入了‘\’
1、root%df%5C%27%20or%201=1%23
2、带入mysql处理时使用了gbk字符集
%df%5c -> 運 成功的吃掉了%5c
%27 -> ‘ 单引号成功闭合
执行了插入的sql语句。
怎么吃的:
GBK编码,它的编码范围是0×8140~0xFEFE(不包括xx7F),在遇到%df(ascii(223)) >ascii(128)时自动拼接%5c,因此吃掉‘\’,而%27、%20小于ascii(128)的字符就保留了。
补充:
GB2312是被GBK兼容的,它的高位范围是0xA1~0xF7,低位范围是0xA1~0xFE(0x5C不在该范围内),因此不能使用编码吃掉%5c。
其它的宽字符集也是一样的分析过程,要吃掉%5c,只需要低位中包含正常的0x5c就行了。
安全过滤
上文中代码使用了mysql_query(“set names gbk”)来设置编码,其实在mysql中是推荐mysql_set_charset(“gbk”);函数来进行编码设置的,这两个函数大致的功能相似, 唯一不同之处是后者会修改mysql对象中的mysql->charset属性为设置的字符集。
同时配套的过滤函数为mysql_real_escape_string()。上面代码中列出了几个过滤的函数,他们之间的区别就是 mysql_real_escape_string()会根据mysql对象中的mysql->charset属性来对待传入的字符串,因此可以根 据当前字符集来进行过滤。
具体差别可参考:http://www.laruence.com/2010/04/12/1396.html
同理可得
由上文可得宽字节注入是由于转编码而形成的,那具有转编码功能的函数也成了漏洞的成因。
转码函数
mb_convert_encoding()
iconv() iconv ( 'GBK' , 'UTF-8' , $user );将utf-8转换成GBK
以下用iconv()来演示,修改上面的代码:
<! DOCTYPE html >
< meta charset = "gbk" >
<? php
error_reporting ( 0 );
$conn = mysql_connect ( '127.0.0.1' , 'root' , '' );
mysql_select_db ( 'mysql' , $conn );
mysql_set_charset ( "utf8" ); //推荐的安全编码
$user = mysql_real_escape_string (( $_GET [ 'sql' ])); //推荐的过滤函数
$user = iconv ( 'GBK' , 'UTF-8' , $user ); //将UTF-8转换成GBK
$sql = "SELECT host,user,password FROM user WHERE user='{$user}'" ;
echo $sql . '</br>' ;
$res = mysql_query ( $sql );
while ( $row = mysql_fetch_array ( $res )){
var_dump ( $row );
}
?>
http://localhost/xl.php?sql=root%e5%27or%201=1%23
同样可以执行成功,编码解析的过程依然如上。
总结一下漏洞成因:
代码一
1、使用了不安全的字符集设置函数与过滤函数。
2、漏洞发生在PHP请求mysql时使用character_set_client值进行一次转码。
代码二
1、使用了推荐的设置函数与过滤函数。
2、解析错误发生在iconv()函数转码时,GBK转向UTF8吃掉了“\”
3、PHP请求mysql时转码安全。
另外:
当改变编码方向时$user = iconv(‘UTF-8′, ’gbk’,$user);
通过访问http://localhost/xl.php?sql=root%e9%8c%a6可以带入一个\,进而注释掉单引号。<解码之后生成了一个\,%5c>
这种情况下需要两个参数来配合注入。
例如:
http://localhost/xl.php?sql=root%e9%8c%a6?=%20or%201=1%23
总结:
宽字节注入跟HTML页面编码无关。
Mysql编码与过滤函数推荐使用mysql_real_escape_string(),mysql_set_charset()。
转编码函数同样会引起宽字节注入,即使使用了安全的设置函数。