前言
隨著資料庫參數化查詢的方式越來越普遍,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."</br>"; $row = $db->prepare($query); $row->bindParam(1,$id); $row->execute(); $result = $row->fetch(PDO::FETCH_ASSOC);if($result) { echo "结果为:"; print_r($result); echo "</br>"; }
然後寫一個test.php,用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);
分別與模擬預編譯、報錯和多句執行有關。
PDO預設是允許多句執行和模擬預先編譯的,在之前的許多文章中已經寫到,在參數可控的情況下,會導致堆疊注入。
例如我們把查詢語句改成:
$query = "select balabala from table1 where 1=?"; $row = $db->prepare($query); $row->bindParam(1,$_GET[‘id’]); $row->execute();
則在$db->query()這一步執行之前,我們便可以對$query進行非法操作,那PDO相當於沒用:
PDO預設設定存在的安全隱患:
如果我們在查詢語句中沒有可控的參數,並且把輸入的參數按照prepare->bindParam->execute的方式去寫就一定沒有問題了嗎?
我們按如下語句進行查詢:
Set @x=0x31 Prepare a from “select balabala from table1 where 1=?” Execute a using @x
我們在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."</br>"; $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 "</br>"; }
得到的结果和使用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注入
相关文章教程推荐:网站安全教程
以上是PDO原理及正確使用方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!