PDO(预编译参数化查询)和安全问题

前言

在看梅子酒师傅的一篇文章中:

https://meizjm3i.github.io/2018/04/04/对PHP类CMS审计的一点总结/
看到了对SQL注入的挖掘思路:
cms中如果使用了PDO,我们的挖掘思路就是跟进涉及到table,order by等字段的拼接去,因为这些字段是无法使用PDO的。为什么无法使用?这篇文章来分析一下。

什么是PDO

毫无疑问,他的无法使用和PDO有关。我们先来了解一下PDO。

PDO(PHP Data Object),PHP数据对象,它提供了一个轻量级的接口,使得我们不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据,同时,它的预编译机制可以用来防止SQL注入。PDO有两种模式:本地预处理和模拟预处理。

我们先来看一下模拟预处理。
因为默认情况下就是使用的模拟预处理。会有什么问题?
我们先看代码:
比如以下代码:

stmt=PDO -> prepare("SELECT * FROM userWHERE id = ?");
stmt -> bindParam(1,stmt−>bindParam(1',_GET['id']);
$stmt -> execute();

这个就是PDO处理的三个步骤,不再解释。重点关注我们第二步绑定参数的时候是1'
这个执行结果其实可以这样拆分:

Connect    root@localhoston nowcoder
Query    SELECT * FROM user WHERE id = '1\''
Quit

我们看到这里有个问题,他这里整个SQL语句由PHP处理并转义,然后交由MySQL处理。也就是说整个过程只有连接、查询、退出三个操作,并没有预编译的过程。他在经过
PreparedStatement处理后(PreparedStatement能防止注入,是因为它把单引号转义了,变成了'),然后直接拼接执行。
总结:模拟预处理用于数据库不支持预编译机制的情况,其本质是在底层先对用户输入进行转义后,再对SQL语句进行拼接,然后将完整的语句发送给数据库执行。任何可控的拼接都是具有一定危险性的,在PHP 5.3.6前,这种转义是通过单字节字符集来完成的,因此存在宽字节注入。但在正确设置字符集的前提下,不可否认这种方式是可以防止SQL注入的。

我们再来看一下本地预处理模式即使用了真正的数据库预编译。
先上网上被写烂的一句话:

预编译防止注入的原因:

关于预编译能够防止注入的原因,还要从预编译的运行机制说起。通常来说,在MySQL中,一条SQL语句从传入到执行经历了以下过程:检查缓存、规则验证、解析器解析为语法树、预处理器进一步验证语法树、优化SQL、生成执行计划、执行。

预编译使用占位符?代替字段值的部分,将SQL语句先交由数据库预处理,构建语法树,再传入真正的字段值多次执行,省却了重复解析和优化相同语法树的时间,提升了SQL执行的效率。

正因为在传入字段值之前,语法树已经构建完成,因此无论传入任何字段值,都无法再更改语法树的结构。至此,任何传入的值都只会被当做值来看待,不会再出现非预期的查询,这便是预编译能够防止SQL注入的根本原因。
说了这么一段话,怎么概况呢?这里本菜来说下自己的理解:

网上对PDO防注入的回答大多都停留在预编译会将传入的数据只当做数据,却没解释为什么会当成数据。事实上这个数据并不需要识别。
也就是说我们比如执行:

select * from user where username='admin'

使用PDO后他会生成

select * from user where username='?'

请注意这个语句无法再改变,我们可以理解为他只能执行这个语句。
那么我们的数据比如admin或者admin' union select * fromxx
这种他会不做操作直接拼接到占位符位置处。
这样保证了我们的sql语句原有的查询不会被改变。

安全问题

在了解了PDO后,我们来说下我们今天讨论的东西:

table,order by等字段无法使用PDO,为什么?
这里以order by为例:
这里我以最简单的来说明:

order by是mysql中对查询数据进行排序的方法, 使用示例

select * from 表名 order by 列名(或者数字) asc;升序(默认升序)
select * from 表名 order by 列名(或者数字) desc;降序
select * from user order by id; 
selecr * from user order by 1;

结合union来盲注:

 $sql = 'select * from admin where username='".$username."'';
$result = mysql_query($sql);
$row = mysql_fetch_array($result);
if(isset($row)&&row['username']!="admin"){
	$hit="username error!";
}else{
	if ($row['password'] === $password){
		$hit="";
	}else{
		$hit="password error!";
	}
             
}

payload

username=admin' union 1,2,'字符串' order by 3

这里就会对第三列进行比较,即将字符串和密码进行比较。然后就可以根据页面返回的不同情况进行盲注。 注意的是最好加上binary,因为order by比较的时候不区分大小写。

这里的order by 3是根据第三列进行排序,如果我们union查询的字符串比password小的话,我们构造的 1,2,a就会成为第一列,那么在源码对用户名做对比的时候,就会返回username error!,如果union查询的字符串比password大,那么正确的数据就会是第一列,那么页面就会返回password error!.

这里就来说一下我们上面的payload:
我们想要爆破密码,那么我们可以轮流带入查询来观察排序情况,那么之后一定能注入出密码。
在说以下PDO特性:

表名和列名是不能够被预编译的,这是由于生成语法树的过程中,预处理器在进一步检查解析后的语法树时,会检查数据表和数据列是否存在,因此数据表和数据列不能被占位符?所替代。这个就很好理解,我们如果传入一个表名他会先检查在该查询语句中是否存在,不存在则报错。但在很多业务场景中,表名需要作为一个变量存在,因此这部分仍需由加号进行SQL语句的拼接,若表名是由外部传入且可控的,仍会造成SQL注入。
所以:
ORDER BY后的ASC/DESC也不能被预编译,因为在使用PDO后查询时不能识别会报错。当业务场景涉及到用户可控制排序方式,且ASC/DESC是由前台传入并拼接到SQL语句上时,就可能出现危险了

在上面进行查询时我没有写ASC,因为他默认就是ASC,我们也可以改变:

我们可以设想一下,假如order by后面有拼接,ASC/DESC没有做严格检查后,就会导致注入。
order by注入的防范的话直接做个intval强制类型转换,其他的更有效的还没想到。。

参考

https://meizjm3i.github.io/2018/04/04/对PHP类CMS审计的一点总结/

https://cloud.tencent.com/developer/news/378220

猜你喜欢

转载自www.cnblogs.com/wangtanzhi/p/12927199.html