來源:離別歌
作者:phithon@長亭科技
0x01 由某CTF題解說起
小密圈里有人提出的問題,大概代碼如下:

看了一下,明顯考點是這幾行:
<?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的賬號,請先看這一篇文章 https://www.leavesongs.com/PENETRATION/Mini-XCTF-Writeup.html
本文中利用?等latin1字符來繞過php的判斷。這個CTF也是用同樣的方法來解決:

可見,我傳入的username=admin%c2,php的檢測if ($username === 'admin')自然就可以繞過的,在mysql中可以正常查出username='admin'的結果。
0x02 Trick復現
那么,為什么執行SELECT * FROM user WHERE username='admin\xC2' and password='admin'卻可以查出用戶名是admin的記錄?
剛好這段時間有人問我為什么在他的計算機上無法復現,我們來深入研究研究。
編寫如下代碼:
<?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");
$username = addslashes($_GET['username']);
/* 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();
然后在數據庫cat中創建表table1:
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;
我特地將字符集設置為latin1,其實默認情況下,Mysql的字符集就是latin1,沒必要寫明。
插入一個管理員賬戶:
INSERT `table1` VALUES (1, 'admin', 'admin');
然后,我們訪問http://localhost/test.php?username=admin%c2,即可發現%c2被忽略,Mysql查出了username=admin的結果:

假設我們將table1表的字符集換成utf8,就得不到結果了。
0x03 Mysql字符集轉換
經過0x02中對該Mysql Trick的復現,大概也能猜到原理了。
造成這個Trick的根本原因是,Mysql字段的字符集和php mysqli客戶端設置的字符集不相同。
set names utf8 的意思是將客戶端的字符集設置為utf8。我們打開mysql控制臺,依次執行SHOW VARIABLES LIKE 'character_set_%';、set names utf8;、SHOW VARIABLES LIKE 'character_set_%';,即可得到如下結果:

如上圖,在默認情況下,mysql字符集為latin1,而執行了set names utf8;以后,character_set_client、character_set_connection、character_set_results等與客戶端相關的配置字符集都變成了utf8,但character_set_database、character_set_server等服務端相關的字符集還是latin1。
這就是該Trick的核心,因為這一條語句,導致客戶端、服務端的字符集出現了差別。既然有差別,Mysql在執行查詢的時候,就涉及到字符集的轉換。
2008年鳥哥曾在博客中講解了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字符串。
0x04 漏洞成因
那么,字符集轉換為什么會導致%c2被忽略呢?
說一下我的想法,雖然我沒有深入研究,但我覺得原因應該是,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的結果。
0x05 為什么只有部分字符可以使用
我在測試這個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個字節表示:
- 一字節時范圍是[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,這也是我在admin后面加上80-C1、F5-FF等字符時會拋出錯誤的原因。
關于所有的UTF-8字符,你可以在這個表中一一看到: http://utf8-chartable.de/unicode-utf8-table.pl
0x06 Mysql UTF8 特性
那么,為什么username=admin%F0也不行呢?F0是在C2-F4的范圍中呀?
這又涉及到Mysql中另一個特性:Mysql的utf8其實是閹割版utf-8編碼,Mysql中的utf8字符集最長只支持三個字節,
所以,我們回看前文列出的UTF-8編碼第一字節的范圍,
三字節時范圍是[E0-EF][80-BF][80-BF] 四字節時范圍是[F0-F7][80-BF][80-BF][80-BF]
F0-F4是四字節才有的,所以我傳入username=admin%F0也將拋出錯誤。
如果你需要Mysql支持四字節的utf-8,可以使用utf8mb4編碼。我將原始代碼中的set names改成set names utf8mb4,再看看效果:

已經成功得到結果。
0x07 總結
本文深入研究了Mysql編碼的數個特性,相信看完本文,對于第一章中的CTF題目也沒有疑問了。
通過這次研究,我有幾個感想:
- 研究東西還是需要深入,之前寫那篇文章的時候并沒有深入研究原理,所以心里總是很迷糊
- 維基百科上涵蓋了很多知識,有必要的時候也可以多看看
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/267/
暫無評論