/ Web安全

SQL Prepared Statement

0x01 History

究竟是什么情况下诞生了各类数据库的预编译功能,我们知道SQL Inject诞生于1998年,MYSQL最早在4.1版本被支持,2004年6月测试版中发布。预编译语句的诞生是用来解决sql注入问题的吗?我对几个比较大型的关系型数据库更新历史进行了追踪,在mysql4.1版本支持预编译语句后的文档中,有如下说明:
mysql-4-1-document
可以看到,官方文档在介绍中说明了预编译在安全和性能两个方面有很大的提升。在效率上,同一语法结构的sql查询在使用预编译并开启缓存的情况下,可以减少编译查询语句的时间,从而提高查询效率;安全方面,使用预编译可以解决大部分的sql注入问题。

0x02 工作原理

先抛弃一些数据库连接驱动,直接从命令行来理解预编译。Mysql预编译方式执行数据库语句主要涉及PREPARE、EXECUTE两个语句,大家最
mysql-prepare
再看一眼记录的sqllog情况
mysql-prepare-log
日志中最后显示的告诉我们执行的语句就是

select * from test.test where id=1

再看两个例子,假如现在我将占位符放在查询字段和order by两个地方,会有什么效果呢
mysql-prepare-2
mysql-prepare-log-2
从日志可以看出,在最后执行的语句中直接将字符串替换在查询字段的位置,导致最后查询出来的结果都是text。

mysql-prepare-3
mysql-prepare-log-3
order by字段中也是,替换后的语义效果是将我们的变量当做字符串插入占位符所在位置。

PREPARE语句会对语句进行编译到一个可执行状态,其语句结构基本可以说已经固定。这个上下文结构通过一个名字标识,如上文中stmt1等,而在一些其他数据库驱动中可能是一个id标识,如php pdo mysqli。在进行编译时其中?为占位符,占位符标识的是一些在运行时才决定的语法部分,在EXECUTE语句中,会根据传入的参数,绑定到占位符所在的语法位置,因此我们是无法像往常那样逃逸出单引号造成注入。

0x03 模拟预编译处理

我们知道我们之前提到预编译,是在数据库Server端进行语句的编译,那么为什么又冒出一个模拟预编译呢,这里就要扯到数据库驱动了,在PHP里有一个通用的数据库访问接口,他是一个扩展叫做PDO,PDO里提供了调用各类数据库驱动预编译方式执行SQL的接口,类似于java中的jdbc,但是存在一个问题,有的数据库是不支持预编译的,因此存在一个属性ATTR_EMULATE_PREPARES,该属性值为true的情况下,就不管DBServer支不支持预编译,都会使用模拟预编译。模拟预编译会做哪些处理呢?无脑添加单引号,无脑转义!然后拼接发送到Server段执行。
这个模拟预编译存在一个安全问题,就是在低版本的Mysql上,在字符编码设置不当的情况下存在款字节注入。其实这个东西可以不看,太特殊了,详情可以见链接2。

0x04 预编译解决了所有问题吗

在回答这个问题前,我们探讨一下预编译参数绑定的方式不能在语义上带来的两个限制:

  1. 变量绑定并不能支持sql语法结构中的所有部分,比如表名等
  2. 变量绑定实际上是字符串的填充,在某些sql语法结构中如order by其后跟的是列名,而不是字符串,因此虽然可以通过预编译的方式处理order by,但是实际上不具有实际列名意义,而只是一个字符串。

因此在使用预编译的场景下,我们可以关注一些不能通过预编译完成部分注入点,以及复杂查询,这个一般要结合白盒代码审计,黑盒测试下可重点关注order by。针对此类问题的修复方式也很明确,做好类型转换以及使用白名单做限制,尽量避免使用复杂查询语句。

  1. Prepared Statements
  2. Are PDO prepared statements sufficient to prevent SQL injection?