Mysql字符编码利用技巧


造成这种漏洞的原因是
Mysql字段的字符集和php mysqli客户端设置的字符集不相同。

MySQL字符集介绍

MySQL服务器可以支持多种字符集,在同一台服务器,同一个数据库,甚至同一个表的不同字段都可以指定使用不同的字符集,相比oracle等其他数据库管理系统,在同一个数据库只能使用相同的字符集,MySQL明显存在更大的灵活性。
MySQL提供了不同级别的设置,包括server级、database级、table级、column级,可以提供非常精准的设置。
我们在进行编写php文件的时候

1
2
3
4
5
6
7
8
9
10
<?php
$mysqli = new mysqli("localhost", "root", "root", "cat");

/* check connection */
if ($mysqli->connect_errno) {
printf("Connect failed: %s\n", $mysqli->connect_error);
exit();
}

$mysqli->query("set names utf8");

设置了字符集为UTF-8
set names utf8 的意思是将客户端的字符集设置为utf8
在mysql可以查看

如上图,在默认情况下,mysql字符集为latin1,而执行了set names utf8;以后,character_set_client、character_set_connection、character_set_results等与客户端相关的配置字符集都变成了utf8,但character_set_database、character_set_server等服务端相关的字符集还是latin1。

这就是该Trick的核心,因为这一条语句,导致客户端、服务端的字符集出现了差别。既然有差别,Mysql在执行查询的时候,就涉及到字符集的转换。

1
2
3
MySQL Server收到请求时将请求数据从character_set_client
转换为character_set_connection;
进行内部操作前将请求数据从character_set_connection转换为内部操作字符集

实战一

1
2
3
4
5
6
7
<?php
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}'");

if语句嫌疑很大,大概是考我们怎么登陆admin的账号

漏洞成因

在我们这个案例中,character_set_client和character_set_connection被设置成了utf8,而内部操作字符集其实也就是username字段的字符集还是默认的latin1。于是,整个操作就有如下字符串转换过程:

utf8 –> utf8 –> latin1

最后执行比较username=’admin’的时候,’admin’是一个latin1字符串。
Mysql在转换字符集的时候,将不完整的字符给忽略了。
举个简单的例子,佬这个汉字的UTF-8编码是\xE4\xBD\xAC,我们可以依次尝试访问下面三个URL:

http://localhost:9090/test.php?username=admin%e4
http://localhost:9090/test.php?username=admin%e4%bd
http://localhost:9090/test.php?username=admin%e4%bd%ac

可以发现,前两者都能成功获取到username=admin的结果,而最后一个URL,也就是当我输入佬字完整的编码时,将会失败:

为什么会抛出错误?
原因很简单,因为latin1并不支持汉字,所以utf8汉字转换成latin1时就抛出了错误。

那前两次为什么没有抛出错误?因为前两次输入的编码并不完整,Mysql在进行编码转换时,就将其忽略了。

这个特点也导致,我们查询username=admin%e4时,%e4被省略,最后查出了username=admin的结果。

只有部分字符可以使用

我在测试这个Trick的时候发现,username=admin%c2时可以正确得到结果,但username=admin%c1就不行,这是为什么?
简单fuzz了一下,如果在admin后面加上一个字符,有如下结果:

\x00~\x7F: 返回空白结果
\x80~\xC1: 返回错误Illegal mix of collations
\xC2~\xEF: 返回admin的结果
\xF0~\xFF: 返回错误Illegal mix of collations

实战二


同理是这个if语句

比较$row['id']和$id的区别

令id=%c2 即可

实战三

修复建议

使用MySQL字符集时的建议

建立数据库/表和进行数据库操作时尽量显式指出使用的字符集,而不是依赖于MySQL的默认设置,否则MySQL升级时可能带来很大困扰;

• 数据库和连接字符集都使用latin1时虽然大部分情况下都可以解决乱码问题,但缺点是无法以字符为单位来进行SQL操作,一般情况下将数据库和连接字符集都置为utf8是较好的选择;

• 使用mysql C API时,初始化数据库句柄后马上用mysql_options设定MYSQL_SET_CHARSET_NAME属性为utf8,这样就不用显式地用 SET NAMES语句指定连接字符集,且用mysql_ping重连断开的长连接时也会把连接字符集重置为utf8;

• 对于mysql PHP API,一般页面级的PHP程序总运行时间较短,在连接到数据库以后显式用SET NAMES语句设置一次连接字符集即可;但当使用长连接时,请注意保持连接通畅并在断开重连后用SET NAMES语句显式重置连接字符集。

其他注意事项
• my.cnf中的default_character_set设置只影响mysql命令连接服务器时的连接字符集,不会对使用libmysqlclient库的应用程序产生任何作用!

• 对字段进行的SQL函数操作通常都是以内部操作字符集进行的,不受连接字符集设置的影响。

• SQL语句中的裸字符串会受到连接字符集或introducer设置的影响,对于比较之类的操作可能产生完全不同的结果,需要小心!

参考

Mysql字符编码利用技巧
深入Mysql字符集设置
XCTF两道web题目的writeup

带你5分钟读懂MySQL字符集设置