全局防护Bypass之宽字节注入

1 了解宽字节的背景

宽字节注入的源于程序员设置MySQL连接时的错误配置为:

set character_set_client=gbk

,这样的配置会引发代码转换从而导致注入漏洞。具体分析一下原理:

1.正常情况下GPC开启或者使用addslashes函数过滤GET或POST提交的参数时,我们测试输入的’,就会被转义为\’;

2.若存在宽字节注入,输入%df%27时,经过单引号的转义变成了%df%5c%27,之后再数据库查询语句进行GBK多字节编码,即一个中文占用两个字节,一个英文同样占用两个字节且在汉字编码范围内两个编码为一个汉字。然后MySQL服务器会对查询语句进行GBK编码即%df%5c转换成汉字”運”,单引号逃逸出来,从而造成了注入漏洞。

现在基本都会讲mysql的连接配置设置为

[set character_set_client=binary]

来解决这个问题,这篇博客将介绍php中因为编码或字符编码转换导致的注入问题。

2 mysql中的宽字符注入

测试搭建学习的环境利用了phithon内容管理系统,看代码

<?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 tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>
<!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>

SQL语句是SELECT * FROM news WHERE tid=’{$id}’,根据文章的id把文章从news表中提取出来,在$sql之前,我们只用了限制函数addslashes函数,对$id进行转义,只要我们输入参数在单引号中,就逃逸不出单引号的限制,从而无法注入。

我们这里利用的是mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字节是一个汉字(前一个ascii码要大于128,才到汉字范围),我们测试输入%df’

已经报错,看到报错,说明sql语句出错,看到出错说明可以注入。报错的原因就是多了一个单引号,而单引号前面的反斜杠不见啦。这就是mysql的特性,因为gbk是多字节编码,它人为两个字代表一个字节,所以%df和后面的%5c变成了汉字“運”,而’逃逸了出来。

因为是两个字节代表一个汉字,我们尝试%df%df%27:

不报错了,因为%df%df组成了汉字”哌”,%5c%27不是汉字,仍然是\’

mysql如何判断一个字符是不是一个汉字,根据gbk编码,第一个字节的ascii码大于128,基本上就行,若不用%df二用%a1也可以

%a1%5c虽然不是一个汉字,但一定会被mysql认为是一个宽字符,所以就能让后面的%27逃逸出来,构造一个exp,查询管理人员的账号密码。

####GB12和GBK的区别
gb2312和gbk都是宽字节家族医院,但是当把数据库编码设置为关闭gb2312时,结果就不能注入

<?php
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gb2312'");
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());
?>

这主要是gb2312编码取值范围的事情,它高位范围0xA1~0xF7,低位范围是0xA1~0xFE,\是%5c,是不在低范围中的,即其根本不是gb2312遍吗,故其不会被吃掉。故只要低位的范围中含有0x5c的编码,就可以进行宽字节的注入

3 利用mysql_real_escape_string解决问题

一些cms把addslashes替换为mysql_real_escape_string来防止宽字节的注入

我们若解决需要做的指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下mysql_set_charset函数,设置当前的字符集为gbk,来避免问题

1
2
3
4
5
6
7
8
9
10
<?php
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数库");
//执行sql语句
mysql_set_charset('gbk',$conn)
$id = isset($_GET['id']) ? mysql_real_escape_string($_GET['id']) : 1;
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>

4 宽字节注入修复

character_set_client='binary'设置为binary(二进制),只需要在所有的sql语句前指定一下连接的形式为二进制:
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);,当我们的mysql接受到客户端的数据后,会认为他的编码是character_set_client,然后会将换成character_set_connection的编码,然后在进入具体表和字段后,再转换成字段对应的编码,然后当查询结果产生后,会从表和字段编码,转换成character_set_results编码,返回给客户端。

1
2
3
4
5
6
7
8
9
10
11
<?php
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>

这个方法避免宽字节的注入还是有效的,但是如果开发者画蛇添足的增加一些东西,会让前期的努力前功尽弃。

5 iconv造成的严重后果

很多cms会将接收到的数据,调用这样一个函数,转换其编码:
iconv('utf-8','gbk',$_GET['id']);,目的一般是避免乱码,特别是搜索框的位置

<?php
//连接数据库部分,注意使用了gbk编码
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("连接数据库失败,未找到您填写的数据库");
//执行sql语句
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn); 
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
$id = iconv('utf-8', 'gbk', $id);
$sql = "SELECT * FROM news WHERE tid='{$id}'";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>

可以发现,在sql语句执行前,将character_set_client设置成了binary,所以避免宽字节的注入问题。但之后其调用了iconv将已经过滤的参数$id给转换了一下,测试一下

报错说明我们錦被iconv从utf-8转换成gbk后,变成了%e5%5c,而后面的’被addslashes变成了%5c%27,这样组合起来就是%e5%5c%5c%27,两个%5c就是\,正好把反斜杠转义了,导致’逃逸出单引号,产生注入。利用的是将\转移掉。

利用iconv将gbk转换成utf-8,则可以直接用宽字节注入的姿势来。gbk汉字2字节,utf-8汉字是3字节,若把gbk转换成utf-8,则php会每两个字节一转换。所以,如果\’前面的字符是奇数的话,势必会吞掉\,’逃出限制。

6 总结

1.gbk编码造成的宽字符注入问题,解决方法是设置character_set_client=binary。

2.矫正人们对于mysql_real_escape_string的误解,单独调用set name=gbk和mysql_real_escape_string是无法避免宽字符注入问题的。还得调用mysql_set_charset来设置一下字符集。

3.谨慎使用iconv来转换字符串编码,很容易出现问题。只要我们把前端html/js/css所有编码设置成gbk,mysql/php编码设置成gbk,就不会出现乱码问题。不用画蛇添足地去调用iconv转换编码,造成不必要的麻烦。

7 代码审计实战

对骑士cms审计时发现在plus/ajax_street.php

elseif($act == 'key')
{
    $key=trim($_GET['key']);
    if (!empty($key))
    {
    if (strcasecmp(QISHI_DBCHARSET,"utf8")!=0) 
    //对参数key进行utf-8到GBK编码的转换
    $key=iconv("utf-8",QISHI_DBCHARSET,$key);
    //带入查询,可注入
    //table($table = 'category')=>'qs_74cmscategory'
    $result = $db->query("select * from ".table('category')." where c_alias='QS_street' AND c_name LIKE '%{$key}%' ");
    //将查询结果输出到页面,可回显
    while($row = $db->fetch_array($result))
    {
        if ($listtype=="li")
        {
        $htm.="<li  title=\"{$row['c_name']}\" id=\"{$row['c_id']}\">{$row['c_name']}</li>";
        }
        else
        {
        $_GET['streetid']=$row['c_id'];
        $url=url_rewrite('QS_street',$_GET);
        $htm.="<li><a href=\"{$url}\" title=\"{$row['c_note']}\" class=\"vtip\">{$row['c_name']}</a><span>{$row['stat_jobs']}</span></li>";
        };
    }
    if (empty($htm))
        {
        $htm="<span class=\"noinfo\">没有找到关键字: <span>{$key}</span> 相关道路!</span>";
        }
        exit($htm);
        }
    }

在之前配置文件设置的是mysql_query("SET character_set_connection=" . $dbcharset . ", character_set_results=" . $dbcharset . ", character_set_client=binary", $this->linkid);,其中利用了iconv函数造成致命的错误,同时分析发现页面将查询结果回显回来,构造一些union的查询语句即可获取数据库的敏感信息

漏洞的利用

测试有几个字段,发现category表一共有9个字段,所以可以构造获取数据库用户和先关信息的exp

然后利用union的查询语句爆出可利用的列为4,8,exp:
,然后是爆出数据库和用户名等相关信息

补充

GBK编码中的两个字符是一个汉字,第一个字符需要大于128

------*** end*** ------