Mysql-字符集漏洞分析

— Mysql字符集漏洞 —
— PHP 字符集漏洞 —

mysql字符集漏洞

此篇将介绍两种有关mysql字符集的漏洞,再提及一点类似原理的PHP字符集。

  • 一者是 字符集转换漏洞 ———— 因为字符集不同 躲过关键词过滤 进入mysql后产生的截断 恢复关键词查询
  • 一者是 宽字节注入 ———— 因为字符集不同 躲过SQL注入过滤函数addslashes()使addslashes过滤的单引号、双引号、反斜杠、NULL 可以利用,从而绕过SQL注入防护,达成SQL注入

在介绍这两种漏洞之前,先介绍一些Mysql语句

Mysql语句

查看字符集

查看Mysql中的字符集设置
show variables like 'character_set_%'

设置客户端字符集

在php中,常用以下语句来设置php客户端在Mysql中的字符集
set names utf8
这个语句会修改如下几项客户端设置

1
2
3
character_set_client=utf8
character_set_connection=utf8
character_set_results=utf8

然而如下服务端设置不会修改

1
2
3
4
character_set_database
character_set_server
character_set_filesysytem
character_set_system

这样就造成了 服务端与客户端 不匹配
从而会造成 字符集转换漏洞

而当设置的客户端字符集为GBK时,即set names gbk
(此处以GBK作为典范,但不只是GBK会产生宽字节注入,之后细说)
从而会造成 宽字节注入

下面说明字符集转换漏洞

Mysql字符集转换漏洞

当我们的mysql接受到客户端的数据后,会认为他的编码是character_set_client,然后会将之将换成character_set_connection的编码,然后进入具体表和字段后,再转换成字段对应的编码。
当查询结果产生后,会从表和字段的编码,转换成character_set_results编码,返回给客户端。

于是从php到mysql执行命令的字符集转换关系是

character_set_client -> character_set_connection -> 内部操作字符集(如果是对于数据表字段进行操作,就是那个数据表字段的字符集)

字符集转换漏洞

假设客户端执行了set names utf8,然而服务端的数据库字段格式是 Latin1
即字符集转换是:utf8->utf8->latin1

进行以下测试
php源码:

1
2
3
4
5
6
if ($username === 'admin') {
if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1') {
die('Permission denied!');
}
}
$result = $mysqli->query("SELECT * FROM z_users where username = '{$username}' and password = '{$password}'");

php中过滤了username参数admin,然后从数据库中返回username的内容
我们的目的是:绕过PHP过滤,仍然可以从数据库中查询被过滤admin的内容

1
2
3
http://localhost/1.php?username=admin%e4     #在mysql中utf8转为latin1被截断为admin
http://localhost/1.php?username=admin%e4%bd #在mysql中utf8转为latin1被截断为admin
http://localhost/1.php?username=admin%e4%bd%ac #在mysql中utf8转为latin1失败,出错

中文‘佬’的utf8的16进制\xE4\xBD\xAC
查看汉字UTF8编码:python #encoding:utf8 print '佬'.encode('hex')

转换出错原因:latin1不支持汉字
截断原因:Mysql在转换字符集的时候,会将不完整的字符忽略

所以只要我们给出UTF-8识别的16进制数据,然而不足以转化为latin1的字节数量,就会被截断
payload:?username=admin%e4

只有部分字符可以使用

然而像%e4这样的16进制字符。并不是%00开始到%FF,都是可以被作为截断利用的
这关系到UTF-8的字符实现
戳这里先了解UTF8
这里可以查询UTF8 表

提取出重点,UTF-8是变长编码,有可能是1~4字节表示:

  • 一个字节时,字节的范围是[00-7F]
  • 两个字节时,字节的范围是[C0-DF] [80-BF]
  • 三个字节时,字节的范围是[E0-EF] [80-BF] [80-BF]
  • 四个字节时,字节的范围是[F0-F7] [80-BF] [80-BF] [80-BF]

然后根据RFC 3629规范,又有一些字节值是不允许出现在UTF-8编码中的:

RFC 2629

最终,规范UTF-8的第一字节的取值范围是:[00-7F] [C2-F4]

Mysql UTF8 特性

又因为,Mysql 对于UTF-8有一特性:

Mysql的utf8其实是阉割版utf-8编码,Mysql中的utf8字符集最长只支持三个字节
utf8mb4编码才是包含完整的UTF8

所以,对于类似的绕过利用

  • 对于UTF8,可以利用第一个字节范围[00-7F] [C2-EF]
  • 对于UTF8mb4,可以利用第一个字节范围[00-7F] [C2-F4]

宽字节注入

上面说到 当set names gbk时,可以绕过addslashes过滤,达成sql注入

addslashes过滤原理

addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。
预定义字符变化是:

  • 单引号(‘) -> \'
  • 双引号(“) -> \"
  • 反斜杠() -> \\
  • NULL -> \NULL

当关闭php设置magic_quotes_gpc,在外界参数进入sql语句之前,使用addslashes转移非法符号,再放入sql语句的变量中’’,可以防止sql注入
实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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 "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

payload:?id=1'
经过addslashes函数,进入sql语句就变成
SELECT * FROM news WHERE tid='1\''
无法造成单引号闭合,防止了sql注入

绕过原理

得知addslashes过滤原理,就可以从两方面来绕过:

  • 在addslashes添加的\前面加入一个或奇数个\,变成\\',这样\被转义了,'逃出了限制
  • 想办法把addslashes添加的\弄没

GBK字符有一字节和双字节编码
一个字节时,字节的范围是[00-7F]
两个字节时,字节的范围是[81–FE] [40–7E]/[80-FE] 有根据第一个字节划分情况
Mysql的特性:mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围)

GBK编码

利用以上特性,假如在\前加入一个ASCII码大于128的字符

\'->%5c%27->\'
%df\'->%df%5c%27->運'
SELECT * FROM news WHERE tid='1運''

以上的转换过程可以看出mysql对于字符的转化过程,单引号逃逸,闭合成功

除了%df的其他字符

那么除了%df的还有什么字符可以呢,因为是mysql判断这个字符是不是宽字节字符(2个字节一个字符)
实际上,根据gbk编码,第一个字节ascii码大于128(即16进制80),基本上就可以了。
比如修改%df%a1
%a1%5c可能不是一个汉字,但一定会被mysql认为是一个宽字符,能够让后面的%27逃逸(%57 -> \ %27 -> ‘)

同时对于其他被addslashes过滤的符号,当然也可以用这种方法绕过

除了GBK编码的其他编码

那么除了GBK编码以外,还有什么其他编码可以用来宽字节注入
尝试跟GBK类似的GB2312,注入是不成功的

GB2312的两字节字符范围是[0xA1-0xF7] [0xA1~0xFE]

因为我们需要我们可控的 自定义的字节0x57 (\) 构成宽字节
0x57根本不能作为GB2312的低字节字符,所以就不会被前面的自定义字节当做2个字节的宽字节字符吃掉,所以注入失败

所以我们可以得出:只要低位(最低位)的范围中含有0x5c的编码,就可以进行宽字符注入。

mysql_real_escape_string 替代 addslashes 产生的漏洞

在开发寻找宽字节的解决方案时,会考虑 mysql_real_escape_string
因为在官方文档中说明这个函数会计及连接的当前字符集

于是,有的cms就把addslashes替换成mysql_real_escape_string,来抵御宽字符注入。

但是这样并没有用,仍然可以造成注入
原因就是,程序没有指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下mysql_set_charset函数,设置当前连接的字符集为gbk。

即:

mysql_set_charset('gbk',$conn);
mysql_real_escape_string($_GET['id'])

这样就可以避免注入

防护宽字节注入

  • mysql_set_charset + mysql_real_escape_string
  • character_set_client设置为binary
    SET character_set_connection=gbk,character_set_results=gbk,character_set_client=binary
  • 不使用mysql函数调用数据库,使用mysqli或PDO

类似漏洞原理的php中的iconv

很多cms会将接收到数据,调用这样一个函数,转换其编码:
iconv('utf-8', 'gbk', $_GET['word']);
iconv('gbk', 'utf-8', $_GET['word']);
目的一般是为了避免乱码,特别是在搜索框的位置,这样子的字符集转换,也会有着类似的字符集转换漏洞
关键源码:

1
2
3
4
5
6
7
mysql_query("SET NAMES 'GBK'");
mysql_select_db('test',$conn) OR emMsg('链接数据库失败');
mysql_query("SET character_setconnection=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());

可以看到character_set_client=binary已经进行宽字节注入的防护
试试能不能通过iconv绕过addslashes

iconv(‘utf-8’, ‘gbk’, $_GET[‘id’])

通常来说,一个gbk编码汉字,占用2个字节。一个utf-8编码的汉字,占用3个字节。

如何绕过呢?
UTF-8的编码字符 会转化成GBK下对应的字符,即:
只要一个字符存在于UTF-8GBK编码中,我们就可以控制UTF-8的字符,去得到GBK的字符

我们又知道在addslashes的影响下,输入'会形成%5c%27,那么可以想办法输入一个%5c去注释addslashes加入的5c么
直接输入%5c自然是不行的,因为5c就是\也会被addslashes过滤

那么就可以作为这样的一个输入。
“錦”这个字,它的utf-8编码是0xe98ca6,它的gbk编码是0xe55c

utf8

gbk

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

那么神奇的问题出现了,按照GBK的编码规律,前一个字节大于ASCII码128(即0x80) 会被当成双字节字符解析成一个字符,%e5>%80明明会被当成双字节解析,把后一个5c吃掉,咋会变成都是单字节解析,5c转义5c的尼?
花了忒长时间写这篇了….这里就不测试了,哪个小学弟小学妹帮我测试下吧,弄懂告诉我哈~

那么假如反过来呢

iconv(‘gbk’, ‘utf-8’, $_GET[‘id’])

相同的环境,编码转换反过来,可以利用这个语句来绕过addslashes过滤么

这个利用原理就跟宽字节注入一模一样了吧,只是一个转换是在Mysql中,一个转换是在PHP中而已。
直接%df'->%df/'->%df%5c%27
在iconv gbk 变成 utf-8时,会把%df%5c当做一个字符,转化成utf8编码的该字符,从而形成%27单引号逃逸

后言

尽管是一些比较老的漏洞了,一个PDO预编译+同一字符集就可以解决问题
但是也算是给自己在字符集转换漏洞上面理解了个大概。

理解各个字符集对于16进制数据的处理是很关键的,何时当做单字节字符,何时当做多字节字符,边界16进制范围是多少等。
当弄清楚这些时,去发掘字符集漏洞应该也有些许头绪了。

参考

离别歌:https://www.leavesongs.com/PENETRATION/mysql-charset-trick.html
离别歌:https://www.leavesongs.com/PENETRATION/mutibyte-sql-inject.html