前言
隨著數據庫參數化查詢的方式越來越普遍,sql注入漏洞較之于以前也大大減少,而pdo作為php中最典型的預編譯查詢方式,使用越來越廣泛。
眾所周知,PDO是php中防止SQL注入最好的方式,但并不是100%杜絕SQL注入的方式,關鍵還要看如何使用。
之前在一篇文章中了解到PDO場景下參數可控導致的多句執行等問題(https://xz.aliyun.com/t/3950)于是對PDO場景下的SQL注入又進行了一些探究。
PDO查詢語句可控存在的安全問題:
首先在本地新建一個庫和表,隨便寫點東西。
然后寫一個test.php,用PDO進行簡單的查詢:
<?php try{ $db = new PDO('mysql:host=localhost;dbname=pdotest','root',''); } catch(Exception $e) { echo $e->getMessage(); }if(isset($_GET['id'])) { ??$id?=?$_GET['id']; }else{ ??$id=1; } $query?=?"select?balabala?from?table1?where?1=?";echo?"id:".$id.""; $row?=?$db->prepare($query); $row->bindParam(1,$id); $row->execute(); $result?=?$row->fetch(PDO::FETCH_ASSOC);if($result) {??echo?"結果為:"; ??print_r($result);??echo?""; }
將輸入的內容和得到的結果打印在頁面上:
PDO與安全問題相關的主要的設置有下面三個:
PDO::ATTR_EMULATE_PREPARES PDO::ATTR_ERRMODE PDO::MYSQL_ATTR_MULTI_STATEMENTS
分別與模擬預編譯、報錯和多句執行有關。
PDO默認是允許多句執行和模擬預編譯的,在之前的很多文章中已經寫到,在參數可控的情況下,會導致堆疊注入。
例如我們把查詢語句改成:
$query?=?"select?balabala?from?table1?where?1={$id}"; $row?=?$db->query($query);
則在$db->query()這一步執行之前,我們便可以對$query進行非法操作,那PDO相當于沒用:
PDO默認設置存在的安全隱患:
如果我們在查詢語句中沒有可控的參數,并把輸入的參數按照prepare->bindParam->execute的方式去寫就一定沒有問題了嗎?
我們按如下語句進行查詢:
$query?=?"select?balabala?from?table1?where?1=?"; $row?=?$db->prepare($query); $row->bindParam(1,$_GET[‘id’]); $row->execute();
我們在URL中隨便輸入一個參數:?id=asdasd,然后通過設置SET GLOBAL GENERAL_LOG=ON,從.log里實時監控,看看sql語句到底執行了什么:
我們發現模擬預編譯的請求發送方式和以往的mysqli并沒有什么區別,但我們注意到,在原有的查詢語句中對參數并沒有用單引號包裹,而在此卻用單引號進行了包裹,于是我們可以嘗試輸入一些特殊字符,比如單引號:
發現單引號被轉義了,這時我們不由得想到如果設置了gbk編碼會怎么樣:
我們會發現select * from table1成功執行了,盡管PDO只會返回一個結果,但是它的的確確執行了。
也就是說,即使查詢語句里沒有可控參數,只有?或者:id這類被綁定的參數,依然可以進行堆疊注入。
那如果把多句執行關掉呢?
我們把PDO::MYSQL_ATTR_MULTI_STATEMENTS設為false,重復上述操作:
發現已經行不通了。
實際也只執行了設置gbk這一條語句
但是這樣就結束了嗎?
為什么不試試union注入等其他方式呢?
經過嘗試,發現union注入也是可以的!根本不需要進行多句執行!
實際上,在模擬預編譯的情況下,PDO對于SQL注入的防范(PDO::queto()),無非就是將數字型的注入轉變為字符型的注入,又用類似mysql_real_escape_string()的方法將單引號、雙引號、反斜杠等字符進行了轉義。
這種防范方法在GBK編碼的情況下便可用寬字節進行繞過,而在非GBK編碼的情況下,若存在二次注入的情況,是否能利用呢?
答案是否定的。
二次注入是由于對添加進數據庫中的數據沒有再次處理和轉義而導致的,而預編譯對每次查詢都進行轉義,則不存在二次注入的情況。
上述安全隱患,是由于未正確設置PDO造成的,在PDO的默認設置中,PDO::ATTR_EMULATE_PREPARES和PDO::MYSQL_ATTR_MULTI_STATEMENTS都是true,意味著模擬預編譯和多句執行是默認開啟的。
而在非模擬預編譯的情況下,若語句中沒有可控參數,是否還能這樣做呢?
答案是否定的。
我們將PDO::ATTR_EMULATE_PREPARES設為false,來看看sql語句到底執行了什么:
它對每一句sql語句都進行了預編譯和執行兩個操作,在執行select balabala from table1 where 1=?這句時,如果是GBK編碼,那么它將會把?綁定的參數轉化成16進制,這樣無論輸入什么樣的東西都無法再進行注入了。
如果不是GBK編碼,如上面所說,也不存在二次注入的情況,故可以避免SQL注入漏洞。
相同原理的Prepare Statement方法
PDO的原理,與Mysql中prepare語句是一樣的。上面PDO所執行的SQL語句,用如下的方式可以等效替代:
Set?@x=0x31 Prepare?a?from?“select?balabala?from?table1?where?1=?” Execute?a?using?@x
我們可以手動將輸入的參數設置為@x,并將其轉化為16進制,隨后預編譯,再執行
也就是說,不用PDO也可以仿照其原理手動設置預編譯:
$db?=?new?mysqli('localhost','root','','pdotest');if(isset($_GET['id'])) { $id?=?"0x".bin2hex($_GET['id']); }else{ $id=1; }echo?"id:".$id.""; $db->query("set?names?gbk"); $db->query("set?@x={$id}"); $db->query("prepare?a?from?'select?balabala?from?table1?where?1=?'"); $row?=?$db->query("execute?a?using?@x"); $result?=?$row->fetch_assoc();if($result) { echo?"結果為:"; print_r($result); echo?""; }
得到的結果和使用PDO是一樣的:
這樣設置不用擔心沒有合理地設置PDO,或是用了GBK編碼等情況。
Prepare Statement在SQL注入中的利用
Prepare語句在防范SQL注入方面起到了非常大的作用,但是對于SQL注入攻擊卻也提供了新的手段。
Prepare語句最大的特點就是它可以將16進制串轉為語句字符串并執行。如果我們發現了一個存在堆疊注入的場景,但過濾非常嚴格,便可以使用prepare語句進行繞過。
例如我們將createtable table2 like table1轉化成16進制,然后執行:
我們發現數據庫中已經多了一個表table2。則語句成功執行了。
總結
對于此類問題的防范,主要有以下三個方面:
1. 合理、安全地使用gbk編碼。即使采用PDO預編譯的方式,如若配置不當,依然可造成寬字節注入
2.?使用PDO時,一定要將模擬預編譯設為false
3.?可采用使用Prepare Statement手動預編譯,杜絕SQL注入
相關文章教程推薦:https://xz.aliyun.com/t/3950