Mysql-字符集漏洞分析
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
3character_set_client=utf8
character_set_connection=utf8
character_set_results=utf8
然而如下服务端设置不会修改1
2
3
4character_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
6if ($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 | http://localhost/1.php?username=admin%e4 #在mysql中utf8转为latin1被截断为admin |
中文‘佬’的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编码中的:
最终,规范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
//连接数据库部分,注意使用了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>
$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,才到汉字的范围)
利用以上特性,假如在\前加入一个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
7mysql_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-8和GBK编码中,我们就可以控制UTF-8的字符,去得到GBK的字符
我们又知道在addslashes的影响下,输入'
会形成%5c%27
,那么可以想办法输入一个%5c
去注释addslashes加入的5c么
直接输入%5c
自然是不行的,因为5c就是\
也会被addslashes过滤
那么錦
就可以作为这样的一个输入。
“錦”这个字,它的utf-8编码是0xe98ca6
,它的gbk编码是0xe55c
当我们的錦
被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