天天看點

魔術引号、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:

php:

結果:

可以看出來不論是使用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:

本文轉自 wt7315 51CTO部落格,原文連結:http://blog.51cto.com/wt7315/1931667