前言
文章的灵感来自于此文ThinkPHP3.2.3框架实现安全数据库操作分→https://xz.aliyun.com/t/79
由于接触框架比较晚,本着紧跟技术最前沿(手动滑稽)的想法,学习了TP5。之前在开发过程中遇到问题也会去看一下源码,但一直没有系统的看过TP5底层是如何保证安全的。在动手写之前上面的大佬紧接着又更新了TP5的分析框架filterExp函数过滤不严格导致SQL注入→https://www.jianshu.com/p/654b506d7531
先膜一发,然后开始读代码。。。
准备工作
目前(2017.09.20)thinkphp官网上的最新版本是5.0.11,我们用来分析的也是这个版本。
测试环境为Windows php5.5.3 mysql apache
测试代码:
applicationindexcontrollerIndex.php
解释一下这段代码,input和db这两个函数在tp5中称为助手函数,与TP3中的单字母函数I()、M()相对应。
这是官方手册:
input:https://www.kancloud.cn/manual/thinkphp5/118044
助手函数本质上还是调用的相应的类来进行一系列操作,并没有另外进行其他的实现。
以db函数为例:
thinkphphelper.php
分析
前面瞎扯的有点多。。。现在我们进入正题。
在分析代码之前,我们需要知道TP5使用了PDO预处理机制及自动参数绑定功能。这种技术号称可以完全防止sql注入,当然这是吹n*的,没有绝对的安全,但是对付一般的注入足够了。
按照惯例加个单引号,不出意外被过滤了,我们不妨把admin'看成一个小蝌蚪,看看它到底经历了什么最终进入了数据库(怎么感觉有点污呢。。。)
3.1 input()
首先是input函数了:
thinkphphelper.php
可以看到这个函数只是将我们输入的字符串进行解析,参数是什么,用什么方法传递的(get || post || ...),最后调用了另一个助手函数request,继续跟下去。request函数只是返回了一个Request类的实例化,就不贴代码了,现在来到了Request类的post方法。
3.1.1 post()
thinkphplibrarythinkRequest.php
post方法又对我们传入的参数进行了一些解析,最后将结果传入了input方法。(其实不光post方法,其他如get、param等方法最终也都进入了input方法)。到这里都是对参数进行了解析,对于我们传入的值admin',没有进行任何操作。。。下面重点来了:
3.1.2 input()
thinkphplibrarythinkRequest.php
小蝌蚪的第一个阻碍好像来了,跟进filterValue函数看一下,贴代码之前还有个有意思的地方
但是不要高兴的太早。。
3.1.3 filterValue()
这个函数注释写的比较明白,如果配置文件以及开发者都没有设置过滤函数的话,就直接走到最后了,现在跟进filterExp方法。
3.1.4 filterExp() *
thinkphplibrarythinkRequest.php
这个函数很简单,匹配一些敏感关键字,如果匹配到的话,就在关键字后面加一个空格。这么做有啥用?这个地方在这里很难说清楚,需要结合后面,我们先记一下。
到这里input之旅就结束了,虽然经过了一些奇奇怪怪的过滤,但是似乎并没有威胁到单引号,事实上如果没有修改配置文件,仅仅靠input('post.user')是无法过滤单引号的(由于pdo的存在,其实完全没必要)。
这里虽然没有sql注入的威胁,但是什么过滤都不加会导致xss。
3.2 select()
从我们在控制器中调用到函数执行走了这么多文件。。
要搞懂这一连串的调用真有点不容易。。。用一张图片来说明吧
通过分析流程我们知道对我们输入的数据进行过滤等操作的地方是在thinkphplibrarythinkdbBuilder.php
3.2.1 parseWhere()
跟进parseWhere函数
我们输入的字符串先后进入了buildWhere和parseWhereItem方法,首先跟进buildWhere方法,发现其内部也是调用了parseWhereItem方法,我们直接来看这个方法。
3.2.2 parseWhereItem()
一个长到爆炸的方法,还好注释是中文而且写的十分详细。。。在这个方法中实现了pdo的参数绑定(bind方法),结合注释看一下代码,发现数据基本要过parseValue这个方法,跟进去看一下。
3.2.3 parseValue()
这里的quote方法并不是tp框架实现的,而是pdo自带的一个方法,具体功能看手册吧。
到这里,我们的查询语句的解析、参数的过滤、sql语句的组装全部都结束了,将组装好的sql语句返回到Query类中执行,我们输入的admin',最终到达了数据库。
3.2.4 回到filterExp()
在最前面也说过了,TP5采用了pdo来操作数据库,一般的注入根本不起作用,现在修改一下测试代码:
php中参数可以用数组的形式传递,TP5接收这种类型的参数有两种方式,一种是通过方法的形参来接收,另一种是用input函数,前者用的比较多,后者基本没见过。
在正式开始之前,先来看一下TP5是如何做到直接通过形参来接受请求参数的,这种骚操作叫参数绑定(https://www.kancloud.cn/manual/thinkphp5/118043)。
这个不是这次的重点,粗略的画个图:
thinkphplibrarythinkApp.php的bindParam方法直接调用了Request中的param方法(自动判断请求类型),再往后就和我们之前的分析相同了。这里没有经过助手函数input,也就不存在类型问题,字符串、数组照单全收。这趟走下来,对TP5的运行流程也会有一个比较清晰的认识了。
有瞎扯了这么多,下面进入正题。我们在开发过程中对数据库的查询会有许多条件运算,不仅仅是上面最简单的相等(=)运算,还有其他如LIKE、IN、BETWEEN等等其他运算。如果我们控制器的方法允许传入数组,在上面这个例子中,进行什么样的条件运算就是可控的了。
注意传递的参数要和方法中的形参保持一致,这样传递参数理想情况下得到的sql语句应该是这样的:
我们注意到在parseWhere方法中解析条件运算的部分并没有做任何特殊符号的过滤,一切都是那么的美好,但是。。。它报错了。。报错了。。。
这里就要归功于之前我们记下的filterExp方法了,还记得它吗?它将一些运算符匹配出来,在后面加了一个空格,来到parseWhereItem方法时,会经历这样一个过程:
如果传过来的运算符不在框架指定的运算符中,就会报错,这里我们传入的运算符后面被加了一个空格,当然是匹配不到的,所以报错。注入什么的,也不用想了。
3.3 番外
到这里,该说的差不多都说完了,但是TP5操作数据库并不只有上面这一种方法,还有另外一种比较常用的就是使用Model ORM。我个人也比较喜欢用这种方法,因为它跟我理解的mvc模式比较相近。
这里多说一句有关orm的:
ORM 的基本特性就是表映射到记录,记录映射到对象,字段映射到对象属性。模型是一种对象化的操作 封装,而不是简单的 CURD 操作,简单的 CURD 操作直接使用前面提过的 Db 类即可
显然ORM是一种更高级的用法,即使完全不懂sql语句,也可以与操作数据库。
在修改测试代码之前我们需要先创建appindexmodelUser类:
接着修改测试代码:
有好多种使用方式,只写了几个常用的,还有一些。。条条大路通罗马。。
通过对经过的文件的分析,我们可以看到调用过程和前面是几乎一样的,只是中间经过Model.php做了一些处理和封装,具体内部的调用就不多啰嗦了,总体的流程与我们上面分析的相同。
5.0.10版本的一处注入
写了这么多,不找个漏洞确实不太合适。根据前面3.2.4分析的,如果允许以数组的形式传入参数,在解析条件运算的时候没有任何过滤,filterExp方法是最后也可能是唯一一道防线,如果他出了问题呢?
首先看5.0.10与5.0.11的filterExp方法的差别:
5.0.10
5.0.11
增加了一个NOT LIKE的匹配,再看一下5.0.10与5.0.9框架提供的数据库表达式的差别:
thinkphplibrarythinkdbBuilder.php
5.0.9
5.0.10
5.0.10新增了一个not like的表达式,但是filterExp方法并没有做出相应的修改,导致漏洞的出现。
由于是框架低层出了问题,不管用什么方法操作数据库都会存在漏洞。
防御的话更新到5.0.11就好了,还有在开发过程中,虽然参数绑定非常方便,最好还是使用input助手函数来获取参数。
最后
本来只是想看一下框架底层如何保证数据库安全的,读的时候就想看一下到底是怎么运行怎么调用的。在读代码的过程遇到了许多困难,也看到了许多骚操作,之后要多学习一下php的高级用法,多读读源码。最后奶一口thinkphp,毕竟是国货,希望它越来越好吧,2333333333。