目录
MySQL字符编码利用技巧
一.源代码
1.源码
<?php
$mysqli = new mysqli("localhost", "root", "123456", "security");
/* check connection */
if ($mysqli->connect_errno) {
printf("Connect failed: %s\n", $mysqli->connect_error);
exit();
}
$mysqli->query("set names utf8");
$username = addslashes($_GET['username']);
if ($username === 'admin') {
die('Permission denied!');
}
/* Select queries return a resultset */
$sql = "SELECT * FROM `table1` WHERE username='{$username}'";
if ($result = $mysqli->query( $sql )) {
printf("Select returned %d rows.\n", $result->num_rows);
while ($row = $result->fetch_array(MYSQLI_ASSOC))
{
var_dump($row);
}
/* free result set */
$result->close();
} else {
var_dump($mysqli->error);
}
$mysqli->close();
2.建表语句
CREATE TABLE `table1` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE latin1_general_ci NOT NULL,
`password` varchar(255) COLLATE latin1_general_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
3.插入数据
INSERT `table1` VALUES (1, 'admin', 'admin');
4.数据库表内容
二、过程实现
1.代码分析
通过查看源码可知,当查询username=admin时,会被程序进行拦截,返回Permission denied!
因此,为了进行绕过,需要让程序认为我们传入的参数不是admin,但是数据库却理解为admin,即可实现绕过,成功获取数据
实操验证:
2.绕过实现
未传参时:
实现绕过:
3.过程思考
这是什么原理呢?
为什么在后面添加%c2就能成功绕过呢?
是因为有相应的规则还是运气好蒙到了呢?如果是别的像:%00可以吗?
4.原因解释
造成这个问题的根本原因是,Mysql字段的字符集和php mysqli客户端设置的字符集不相同。
我们打开mysql控制台,依次执行以下代码,
SHOW VARIABLES LIKE 'character_set_%';
set names utf8;
SHOW VARIABLES LIKE 'character_set_%';
即可得到如下结果:
这是直接从MySQL官网下载安装MySQL的结果,由于我是使用小皮系统进行的安装,导致结果与之不同:
但是在建表时可以选择latin1编码:
character_set_server:默认的内部操作字符集
character_set_client:客户端来源数据使用的字符集
character_set_connection:连接层字符集
character_set_results:查询结果字符集
character_set_database:当前选中数据库的默认字符集
character_set_system:系统元数据(字段名等)字符集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设定值。
3. 将操作结果从内部操作字符集转换为character_set_results。也就是: character_set_client -> character_set_connection -> 内部操作字符集
如上图,在默认情况下,mysql字符集为latin1,而执行了set names utf8;
以后,character_set_client
、character_set_connection
、character_set_results
等与客户端相关的配置字符集都变成了utf8,但character_set_database
、character_set_server
等服务端相关的字符集还是latin1。
这就是该Trick的核心,因为这一条语句,导致客户端、服务端的字符集出现了差别。既然有差别,Mysql在执行查询的时候,就涉及到字符集的转换。
-
MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
-
进行内部操作前将请求数据从character_set_connection转换为内部操作字符集
在我们这个案例中,character_set_client和character_set_connection被设置成了utf8,而内部操作字符集其实也就是username字段的字符集还是默认的latin1。于是,整个操作就有如下字符串转换过程:
utf8 --> utf8 --> latin1
最后执行比较username='admin'的时候,'admin'是一个latin1字符串
5.漏洞成因
那么,字符集转换为什么会导致%c2被忽略呢?
个人分析原因应该是,Mysql在转换字符集的时候,将不完整的字符给忽略了。
例如:‘中’字的UTF-8编码是:E4 B8 AD;
我们可以依次尝试访问下面三个URL:
http://127.0.0.1:81/Test/mysql.php?username=admin%e4
http://127.0.0.1:81/Test/mysql.php?username=admin%e4%b8
http://127.0.0.1:81/Test/mysql.php?username=admin%e4%b8%ad
可以发现,前两者都能成功获取到username=admin的结果,而最后一个URL,也就是当我输入‘中’ 字完整的编码时,将会被抛出一个错误:
为什么会抛出错误?原因很简单,因为latin1并不支持汉字,所以utf8汉字转换成latin1时就抛出了错误。
那前两次为什么没有抛出错误?因为前两次输入的编码并不完整,Mysql在进行编码转换时,就将其忽略了。
这个特点也导致,我们查询username=admin%e4时,%e4被省略,最后查出了username=admin的结果。
6.为什么只有部分字符可以成功绕过
测试这个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
这就涉及到Mysql编码相关的知识了,先看看维基百科吧。
UTF-8编码是变长编码,可能有1~4个字节表示:
一字节时范围是[0x00-0x7F]
两字节时范围是C0-DF
三字节时范围是E0-EF[80-BF]
四字节时范围是F0-F7 80-BF
以下是UTF-8编码的一些规则:
- 对于ASCII字符(码点范围0x00至0x7F),UTF-8使用单个字节表示,与ASCII编码相同。
- 对于码点范围在0x80至0x7FF之间的字符,UTF-8使用两个字节表示。第一个字节的前缀为110xxxxx,第二个字节的前缀为10xxxxxx。
- 对于码点范围在0x800至0xFFFF之间的字符,UTF-8使用三个字节表示。第一个字节的前缀为1110xxxx,接下来的两个字节的前缀分别为10xxxxxx。
- 对于码点范围在0x10000至0x10FFFF之间的字符,UTF-8使用四个字节表示。第一个字节的前缀为11110xxx,接下来的三个字节的前缀分别为10xxxxxx。
然后根据RFC 3629规范,又有一些字节值是不允许出现在UTF-8编码中的:
所以最终,UTF-8第一字节的取值范围是:00-7F、C2-F4,这也是我在admin后面加上80-C1、F5-FF等字符时会抛出错误的原因。
关于所有的UTF-8字符,你可以在这个表中一一看到: http://utf8-chartable.de/unicode-utf8-table.pl
7.MySQL UTF8特性
那么,为什么username=admin%F0也不行呢?F0是在C2-F4的范围中呀?
这又涉及到Mysql中另一个特性:Mysql的utf8其实是阉割版utf-8编码,Mysql中的utf8字符集最长只支持三个字节,
所以,我们回看前文列出的UTF-8编码第一字节的范围,
三字节时范围是E0-EF[80-BF]
F0-F4是四字节才有的,所以我传入username=admin%F0也将抛出错误。
如果你需要Mysql支持四字节的utf-8,可以使用utf8mb4编码。我将原始代码中的set names utf8改成set names utf8mb4,再看看效果:
原始:
修改后:
修改后F0-F4也能成功使用
三、总结思考
因为客户端来源数据使用的字符集与连接层字符集以及数据库内部默认字符集可能存在差别,导致数据的传入可能会产生问题,导致某些数据被泄露从而产生安全问题。