前段时间,ThinkPHP发布了V5.0.16版本的release,该版本提到了安全更新。本篇文章以此次安全更新入手,对ThinkPHP 5.0版本 SQL注入漏洞进行了详细分析。文末还有测试小问题,看看大家get到这个漏洞的精髓了吗?

前言

Thinkphp V5.0.16版本的release说明如下:

说明中提到了安全更新,但并没有提到是什么安全问题。

V5.0.16的commits记录如下,可以看到在3月26日出现了一个关于安全性的提交,但26日似乎没有一次性改好,在27日又对这个inc/dec查询改动了一次

接下来看下这个inc/dec查询到底有什么问题,需要一改再改。

漏洞分析

先看下26日改了什么

再看看27日改了什么

改动都在Builder.php这个文件的相同位置,而且反反复复的折腾的,就是$val[1]这个变量。

接下来看看完整的函数部分,看看$val[1]到底怎么了。

漏洞部分在parseData函数

可以看出这个方法是用来传入的字典类型$data数据的,具体传入的$data是什么,还需要进一步分析。

先不管调用关系,单单看这个方法,在处理$data的value时,会分情况处理,

是否是空,是否是数组,是否是常量,而漏洞恰恰出在了是否是数组这个elseif上了。

接下来看看谁调用了parseData,传入的$data又是什么。

向上跟踪到Builder.php中的insert方法

这个insert也不是最上层,但我们就先看看这个insert做了什么。

假如最上层入口的$data我们可控,$data这个字典中的value值还是个数组,那个经过parseData方法后,最终的返回值就可控,原因如下:

只要$val[0]的值是exp/inc或者是dec,那么我们就能将$val[1]恶意构造的值传入$result[$item]中,这个$result值最终会返回给insert方法中的$data变量,看下图:

并且,最终$fields的值会是$item的值;$values的值会是$result[$item]的值,即为

不要担心这个parseKey方法会破坏我们构造的$val[1],因为。。。。

到目前为止,我们的推断是,只要$data的值可控,那么我们就能将恶意构造的值传入$values这个参数。接下来,看看$values这个可控参数又如何造成sql注入的。

很明显,这里是要拼接sql语句了,最终的$values会被拼接到$sql变量中

抛开恶意构造的部分,有经验的朋友一看就能想到这个$sql变量是要做什么的。对,这个$sql变量是要用作参数化查询的sql指令部分。

为了验证我们的猜想,继续往上层跟踪。

在\library\think\db\Query.php中,调用了我们刚才分析的builder中的insert方法

刚才的$sql变量,这次又传递给了Query.php中的$sql变量了,这里的$bind,实际上就是用来取出真是value值的。

然后在上图红圈中,使用execute进行参数化查询。由于$sql变量可控,里面可以包含我们传入的恶意字符串,因此,即使用了参数化查询,也没法避免sql注入的产生。

这个漏洞怎么利用呢?这关系的Query.php中的这个insert方法,看看thinkphp中关于这个方法的使用说明

我这里给个demo,帮助大家理解下应用场景。

总结

理论上来说,利用参数化查询,将要执行的sql语句和参数分开传入,的确可以防止sql注入的产生。但是像这个案例,要执行的sql语句中的内容竟然可控,那就比较尴尬了。

思考

漏洞分析虽然告一段落了,这里我给大家提出几个问题,看看大家有没有真的弄明白这个漏洞。

  1. 如果我直接通过get方法传入一个字符串,这个漏洞会利用成功吗?
  2. 最终的修补如下图

当$val[0]=exp的时候,$val[1]仍然可控,并且也传入了$result[$item]里了,这里是否还是有漏洞呢?为什么thinkphp不修这里呢?

答案

最终的$result[$item]中的$key值是不可控的,

对于我给出的那个例子

$key就是红圈中的内容

因此最终执行时,$sql不可控,还利用了参数化查询,完全没有注入的可能,如下图

 

答案2

这个问题也困扰到我了,找到这个问题的时间简直比分析漏洞的时间还长。

通过Input方法传入的变量,会中途经过\library\think\Request.php中的input方法进行处理,然而在这个方法中,有一个过滤器。。。

如果$data是数组形式的,就利用$this->filterValue进行处理

这个过滤器还没对我们的$data下手,注意看红框处,filterExp

在这里,filterExp如果匹配到了exp,则会给它后面加一个空格,这就导致了我们通过get/post

提交进来的数组中,如果有exp,则会被处理为“exp ”,因此无法进入”exp”这个case

源链接

Hacking more

...