众所周知,预编译是解决sql注入的一个很好的方案,但是预编译在现实使用中却有着很多有趣的细节需要研究下。在没有经过实验之前,针对如下问题我也比较模糊,例如:
1、Mysql预编译和模拟预编译有什么不同?哪种方式理论上更加安全呢?
2、PHP中链接数据库Mysqli接口与PDO接口默认采用哪种方式进行预编译?
3、Python中MySQLdb又是默认采用哪种方式进行预编译?
4、程序采用Mysql数据库预编译方式,转义环节是有客户端(PHP、Python、Java)完成的,还是由服务器端(Mysql数据库)完成?
本文将对上述这些问题进行分析
首先介绍下sql预编译和模拟预编译的区别之
以mysql数据库举例:通常情况下,在数据库接收到一条普通的
SQL语句后,首先对其进行语义解析,随后对此条SQL
语句进行优化并制定执行计划并执行;当采用预编译操作时,首先将待执行的SQL
语句中的参数值用占位符替代。当带着占位符的SQL
语句模板被数据库编译、解析后,再通过向占位符绑定参数进行查询操作。
反观Sql注入的根源,是在本应该传递参数数据的地方,
传入了精心构造的sql语句。而经过预编译操作之后,无论后续向模板传入什么参数,这些参数仅仅被当成字符串进行查询处理,因此杜绝了sql注入的产生
接下来看一下预编译在mysql数据库中如何操作
首先,我们可以通过 PREPARE stmt_name FROM preparable_stm
语法来预编译一条sql语句模板,如下图:
接着通过set来绑定参数 ,如下图:
最后通过EXECUTE stmt_name [USING @var_name [, @var_name]...]的语法来选择编译好的stmt_test模板以接收name参数并执行查询,如下图:
通过查看mysql日志可以发现,与执行普通sql语句使用的query命令不同,这里使用了prepare命令与execute命令,见下图
当后续使用同一模板不同参数值(不同的name值)进行查询进行查询时,例如下图:
这里查询name值为”othername”的列,由于这里使用的仍是经过prepaer的stmt_test模板,程序将使用先前存储于缓冲区预编译后的模板进行解析,而不需要再次通过prepare,见下图
上图中可见,预编译可以实现一次编译、多次执行,省去了解析优化等过程。
在实际操作中,当客户端在与mysql数据库通信时,为了表明当前请求消息的类型,会发送命令请求报文,报文格式如下图所示:
通常情况下,如果简单的执行sql语句,数据包中会使用类型值为0x03的COM_QUERY消息报文,见下图
而在使用预编译功能时,则会使用类型值为0x16的COM_STMT_PREPARE进行预编译并使用0x17进行执行,见下图
上图中22对应十六进制的0x16 COM_STMT_PREPARE阶段
上图报文中23对应十六进制的0x17 COM_STMT_EXECUTE阶段
模拟预编译是防止某些数据库不支持预编译而设置的(如sqllite与低版本mysql)。如果模拟预处理开启,那么客户端程序内部会模拟mysql数据库中的参数绑定这一过程。也就是说,程序会在内部模拟prepare的过程,当执行execute时,再将拼接后的完整SQL语句发送给mysql数据库执行
有如下案例,这里使用PDO接口进行数据库操作
从上图代码中可见,使用prepare预编译sql模板,并通过bindParam进行参数绑定,最终通过execute进行执行,但这是否是真正的sql预编译呢?
我们可以看下mysql日志事实记录,如下
可以看到数据库日志中并没有prepare阶段与execute阶段。反而和执行普通的sql查询一样,简简单单的Query了PDO传递过来的sql语句
这是为什么呢?
正如上文所说:为了防止某些数据库不支持预编译而设置的(如sqllite与低版本mysql),PDO默认使用的是模拟预编译而非mysql数据库预处理(本地预处理)。如果模拟预处理开启,那么客户端程序内部会模拟mysql数据库中的参数绑定这一过程。也就是说,程序会在内部模拟prepare这一过程,当execute方法执行时,再将拼接后的完整SQL语句发送给mysql数据库进行查询
PDO中通过PDO::ATTR_EMULATE_PREPARES参数控制所使用的的预编译模式,默认使用模拟预处理进行操作。详细的可见下图官网给出的说明:
模拟预处理并没有实现SQL模板与参数的分离,但的确可以防止sql注入。根据笔者查阅的资料显示:模拟预处理防止sql注入的本质是在参数绑定过程中对参数值进行转义与过滤,这一点与真正的sql数据库预处理是不一样的。理论上,sql数据库预编译更加安全一些。
在介绍完模拟预编译与sql数据库预编译后,我们来看看哪些接口默认使用模拟预编译,而哪些接口不使用
从数据库日志中可见,数据库通过query命令执行了一条简单的sql语句。很显然,默认情况下PDO使用模拟预处理
我们将设置PDO::ATTR_EMULATE_PREPARES为false,见下图
从日志中可见,这里明显有prepare和execute两个过程,显然在将PDO::ATTR_EMULATE_PREPARES设置为false后,使用的是mysql数据库预编译
从上图可见:很显然这是一个sql数据库预编译过程。这说明mysqli与PDO不同,
mysqli默认使用的是sql数据库预编译而非模拟预编译
从数据库日志中可见,数据库日志中只有query命令,MySQLdb默认情况下使用模拟预处理
同样,Pymysql也默认使用模拟预处理
从日志可见:存在Prepare过程,这是一个sql预编译过程,Oursql使用的是sql数据库预处理。
然而奇怪的是,日志中只有Prepare过程,但是程序以及可以查询到数据。我们查看下流量,见下图
从上图可见,这里其实是有Execute过程的,但为什么数据库日志中不存在execute这条记录呢?
首先我们来看下这条Execute数据包,如下图
可见上图红框处,Flags为Read-only cursor
通常情况下,这里值为Defaults。据笔者猜测,数据库中没有这条执行日志可能与这个字段有关,感兴趣的同学可以自己研究下。
在查看数据库日志时可以发现:使用mysql数据库预编译时,在execute阶段,传入参数的特殊字符会被进行转义处理。见下图红框处
这个转义,是在对应的客户端中进行的?
还是在mysql数据库接收到参数后,自行进行转义的?
通过抓取流量可知,见下图
客户端传递的参数,并没有进行转义处理。因此可知,转义操作是在mysql数据库上进行的
使用sql数据库预编译,理论上可以杜绝sql注入攻击,但是也会有例外。
很久之前ThinkPHP5
曾有一个SQL注入漏洞。该漏洞简单来说,就是在预编译阶段即prepare阶段,sql语句的模板中参数名可控,导致的sql注入。具体的可以参见这篇文章
https://www.leavesongs.com/penetration/thinkphp5-in-sqlinjection.html
这次并不是通过参数注入payload,而是在sql模板生成时在参数名处拼接payload,在prepare阶段进行注入攻击。虽然在prepare阶段可以注入payload,但是这样的sql模板会引起mysql数据库的报错从而无法顺利执行到execute阶段。
然而在prepare阶段,仍然是可以执行部分payload的,例如下图demo
最终仍然可以通过报错进行sql注入攻击
可见:prepare阶段的sql模板如果可控,仍然是有注入风险的
https://blog.csdn.net/yanghuan313/article/details/70477360
https://www.leavesongs.com/penetration/thinkphp5-in-sqlinjection.html