MYSQL 字符编码利用

30

这算是一种绕过手段了, 原理就是通过 mysql 默认字符集和客户端程序连接 mysql 的字符集不一致, 导致处理上出现差异

先创建一个表:

CREATE TABLE `test` (
  `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;

这个表中指定的字符集是 latin1

随便插入一条数据:

INSERT `test` VALUES (1, 'admin', 'admin');

下面是 test.php 代码:

<?php
$mysqli = new mysqli("localhost", "root", "root", "test");

/* 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();

这时候访问这个 http://hostname/test.php?username=admin%c2, 会发现正常查询除了 username = admin 的结果, 这个 %c2 被忽略掉了

在安装 MYSQL 之后, 没有做任何操作的情况下, 在 mysql 的 控制台输入 SHOW VARIABLES LIKE 'character_set_%'; 字符集应该是下面的样子

ffb4bf0c-ba58-444e-a056-d75f6ebf8d38.9251a10908b4

set names utf8; 再看就会变成:

ffb4bf0c-ba58-444e-a056-d75f6ebf8d38.9251a10908b4 copy

这就是问题所在了, 刚刚我们创建的表是 latin1 字符集, 而 php 连接的时候使用 utf8, 会涉及到字符集的转换

mysql 会将收到的请求数据从 character_set_client 转换为 character_set_connection, 再从 character_set_connection 转换为内部操作字符集

于是就会有以下转换过程:

utf8 -> utf8 -> latin1

而如果都是 utf8 自然是没有问题的, 可是这里不一致了, 就需要考虑到 latin1 和 utf8 之间的差异, 以及处理时对不兼容的字符如何处理了

我们以此访问:

http://hostname/test.php?username=admin%E6
http://hostname/test.php?username=admin%E6%88
http://hostname/test.php?username=admin%E6%88%91

发现前两个都返回了 admin 的信息, 说明忽略了, 而第三个没有

%E6%88%91 是汉字 的 url 编码, latin1 并不支持汉字, 所以第三个抛错了:

'Illegal mix of collations (latin1_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='' 

前两个编码不完整被忽略所以返回了结果

你可能会想到的是尝试 %00 或者 %F0, 但是你必须要知道的是 utf8 编码有自己的规范, 有一些字节是不允许出现的, 只能使用 U+0000 到 U+10FFFF :

Screenshot2024-08-11at19.29.52

MYSQL 字符集的问题

%F0 满足 utf8 的范围, 但是却无法获取信息, 这是因为另一个 mysql 的特性

在 mysql 中, utf8 只支持 3 个字节, 而 utf8m64 才是完整的 4 字节编码