0x00 背景
SQL注入是一種常見Web漏洞,所謂SQL注入,就是通過把SQL命令插入到Web表單提交或輸入域名或頁面請求的查詢字符串,最終達到欺騙服務器執行惡意的SQL命令。本文以代碼審計的形式研究SQL注入原理、挖掘形式、防御方案及缺陷。
0x01 SQL注入產生原理
SQL注入與其他常見Web漏洞一樣,均是由外部可控的參數引起的。由於程序沒有經過任何過濾就將外部可控的參數拼接進入SQL語句,直接放入數據庫執行,達到了欺騙服務器執行黑客惡意SQL命令的目的。在這里我們采用DVWA中low級別的源碼學習SQL注入的產生原理。
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
在這里我們注意到變量id是由用戶進行掌控的變量,用戶所輸入的id值並沒有進行任何的過濾,直接拼接到SQL語句中執行,我們重點關注這條SQL語句:
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
此處,如果我們所輸入的變量id為1' or 1=1 or '1,那么此條SQL語句就變為了:
$query = "SELECT first_name, last_name FROM users WHERE user_id = '1' or 1=1 or '1';";
這樣,數據庫就會根據上面這條語句執行操作,這就是SQL注入漏洞的產生原理。
0x02 SQL注入的挖掘形式
SQL注入往往出現在用戶登陸、信息查詢等頁面,通常在HTTP頭中會出現漏洞,例如Cookie值、用於獲取用戶IP的client-ip中,在代碼審計中我們需要着重關注這幾個模塊。
普通注入
普通形式的SQL注入例如上文中提到的,直接通過聯合查詢就可以對數據庫進行操作,這種注入形式往往容易被掃描器檢測出來,在這里不過多進行講解。在代碼審計中,我們只需要關注一些關鍵字,例如select from、mysql_connect、mysql_query、update、delete、insert等即可。
編碼注入
編碼注入包括寬字節注入、URLdecode注入等,利用程序的編碼規則缺陷,輸入與轉碼函數不兼容的特殊字符,導致輸入的字符拼接成為了惡意的SQL語句。
1.寬字節注入
寬字節注入是利用mysql的一個特性,mysql在使用GBK編碼的時候,會認為兩個字符是一個漢字(前一個ascii碼要大於128,才到漢字的范圍),當PHP連接MySQL的時候,設置了“set_character_set_client=gkb”時,往往就會產生寬字節注入。
例如以下程序:
<?php
//連接數據庫部分,注意使用了gbk編碼,把數據庫信息填寫進去
$conn = mysql_connect('localhost', 'root', 'toor!@#$') 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()); //sql出錯會報錯,方便觀察
?>
<html>
<head>
<meta charset="gbk" />
<title>新聞</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>
以上程序中,sql語句是SELECT * FROM news WHERE tid='{$id}。參數id存在寬字節注入漏洞,程序采用addslashes函數,將$id的值轉義。addslashes函數產生的效果就是,讓“ ’ ”變成“ \’ ”,對此我們要想進行注入需要繞過“\”。
我們提交/test.php?id=-1' and 1=1#時,數據庫執行的操作是select * from news where id = '1\' #,顯然這里無法進行注入,但是我們提交/test.php?id=-1 %df ' and 1=1#時,數據庫執行的操作就變成了select * from user where id = '1運' and 1=1 #。這樣我們就可以實現SQL注入了。
這是因為單引號被addslashes轉義成為\',我們提交的%df與\(url編碼為%5c)組合成為了%df%5c,也就是gbk編碼中的“運”字,組合之后被轉義的“ \' ”中的“ ‘ ”還存在,成功閉合了之前的單引號。
挖掘這類的SQL注入漏洞只需要搜索關鍵詞,確認是否采用了gbk編碼即可,關鍵詞主要有以下三種:
character_set_client=gkb
mysql_set_charset('gbk')
SET NAMES 'gbk' //這條語句等同於如下代碼:
SET character_set_connect='gbk', character_set_results='gbk', character_set_client=gbk;
寬字節注入除了上述方法外,有時為了避免亂碼,程序員使用iconv()函數將GBK編碼轉換為utf-8編碼,例如:
mysql_query(“set names UTF-8”) ; $bar =iconv(“GBK”,”UTF-8”, addslashes($_GET[‘’bar])) ;
例如我們提交http://127.0.0.1/test.php?id=1%e5%5c%27,%e5%5c%27經過addslashes()變為%e5%5c%5c%5c%27,再經過iconv()變為%e9%8c%a6%5c%5c%27,這樣我們所提交的請求多了一個%5c,反斜杠本身被轉義,單引號(%27)生效。
2.URLdecode注入
現在絕大多數基於PHP的Web程序都會使用addslashes()等過濾函數對用戶提交的變量等進行過濾,如果某處采用了urldecode()函數進行了url解碼,那么將會大概率的導致URLdecode注入,例如以下代碼:
<?php $a=addslashes($_GET['c']); $b=urldecode($a); echo '$a='.$a;
echo '$b='.$b; ?>
我們在地址欄提交/test.php?c=1%2527時,得到的結果是$a=1%27 $b=1'
這是因為%25的解碼結果為“ % ”,與后面的27拼接得到%27,也就是單引號,因此產生了注入。
所以在代碼審計中挖掘urldecode注入時,只需要搜過兩個關鍵函數即可:
urldecode
rawurldecode
0x03 防御方案及缺陷
黑名單過濾
黑名單過濾是一種原始的、低效甚至無效的過濾手段,將關鍵字select等只使用replace()函數置換為空,企圖達到阻止SQL注入的發生。
這種防御方案的缺陷非常明顯,我們只需要將關鍵字雙向,例如seleselectct(過濾掉中間完整的select之后剩余內容仍是select),也可以大小寫繞過的方式,例如SeLeCt(數據庫大小寫不敏感),以此繞過 防御實現sql注入。
魔術引號
在PHP4.2.3以上版本,可以開啟魔法引號自動過濾
magic_quotes_gpc開啟后,會在GET、POST、COOKIE中的單引號、雙引號、反斜杠、空字符的前面加上反斜杠( \ ),但在PHP5中對$_SERVER變量停止了過濾,導致client-ip等容易被利用。
magic_qutoes_runtime開啟后,與GPC的過濾方案一致,區別是它對數據庫和數據文件進行過濾,而非GET、POST、COOKIE值。
在PHP5.4中,魔術引號被取消。
過濾函數
1.addslashes
addslashes是當前常見的一種過濾方式,過濾的內容和范圍與GPC一致,不過該函數的參數必須是string型。例如:
<?php $str=" 1' "; echo addslashes($str); ?>
得到的結果是1\‘。
2.mysql_escape_string
在PHP4.0.3以上版本中引入了mysql_escape_string函數和mysql_real_escape_string函數,這兩個函數也是對字符串進行過濾,受函數影響的字符包括單引號、雙引號、\、\n、\r等。例如:
<?php
$con = mysql_connect("localhost","root","password");
$id = mysql_real_escape_string($_GET['id'],$con);
echo `select * from table where id = ' ".$id." '`;
?>
當用戶發出請求/test.php?id=1'時,數據庫做以下處理:select * from test where id = '1\"。
PDO預編譯
PDO prepare預編譯類似於Java中的preparestatement,采用預編譯的形式處理數據庫查詢。
<?php
$dbms='mysql'; //數據庫類型
$host='localhost'; //數據庫主機名
$dbName='test'; //使用的數據庫
$user='root'; //數據庫連接用戶名
$pass='pass'; //對應的密碼
$dsn="$dbms:host=$host;dbname=$dbName";
$dsn -> exec("SET NAME 'gbk'");
$sql = "select * from table where user=? and pass=?";
$pstmt = $dbh -> prepare($sql);
$exeres = $pstmt -> excute (array($user,$pass));
以上代碼雖然采用了PDO進行預編譯,但是再PHP5.3.6之前仍然會存在寬字節注入漏洞,因為這樣查詢是PHP本地模擬prepare,然后再把完整的sql語句發送給數據庫,php與MySQL編碼不一致導致寬字節注入漏洞的產生,所以我們需要禁止本地模擬prepare,在上述代碼中加入一行即可:
$dsn -> setAttribute(PDO::ATTER_EMULATE_PREPARES, flase);
iconv
