【安全漏洞】ThinkPHP 3.2.3 漏洞复现

$this->show 造成命令执行

在 Home\Controller\IndexController 下的index中传入了一个可控参数,跟进调试看一下。


跟进 display()


一路跟进到 fetch(),然后一路进入 Hook::listen('view_parse', $params);


关键地方在这,我们之前 index 里的内容被存入了缓存文件php文件中,连带着我们输入的可控的php代码也在其中,然后包含了该文件,所以造成了命令执行。



sql注入

/Application/Home/Controller/IndexController.class.php 添加一段SQL查询代码。http://localhost/tp323/index.php/Home/Index/sql?id=1 查询入口。


传入 id=1 and updatexml(1,concat(0x7e,user(),0x7e),1)--+ ,跟进调试。进入 find() 函数,先进行一段判断,传入的参数是否是数字或者字符串,满足条件的话 $options['where']['id']=input。


随后进行一个判断 if (is_array($options) && (count($options) > 0) && is_array($pk)),getPk()函数是查找mysql主键的函数,显然 $pk 值是 id,不满足条件


随后执行 $options = $this->_parseOptions($options); ,



先获取查询的表的字段和字段类型。


关键代码在于下面这个判断里,进入 $this->_parseType($options['where'], $key) 。


这里由于id字段的类型是 int ,所以进入第二个分支,将我们的输入转化为十进制,恶意语句就被过滤了,后面就是正常的SQL语句了。


如果我们传参是传入一个数组 id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)--+ ,在find() 函数的第一个判断就没有满足条件不会进入这个判断,此时 $options 就是 $options[where]='1 and updatexml(1,concat(0x7e,user(),0x7e),1)-- ',而没有上面的键 id。


然后到下面的关键代码的判断 if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) ,is_array($options['where']) 显然是false,因为此时 $options['where'] 是一个字符串而不是数组,所以不会进入下面的判断,也就是说不会进入函数 _parseType() 对我们的输入进行过滤。

之后回到 find() 函数中进入 $resultSet = $this->db->select($options);,此时的 $options 就是我们输入的恶意SQL语句,显然注入成功。


反序列化 & sql注入

/Application/Home/Controller/IndexController.class.php 添加一段代码。http://localhost/tp323/index.php/Home/Index/sql?data= 查询入口。


全局搜索 function __destruct,找一个起点。

在文件:/ThinkPHP/Library/Think/Image/Driver/Imagick.class.php 中找到了 Imagick 类的 __destruct 方法。


这里 $this->img 是可控的,所以我们接着找一下 destroy() 函数。共有三个,选择了 ThinkPHP/Library/Think/Session/Driver/Memcache.class.php 中的 Memcache 类的 destroy 函数。这里有个坑,由于上面调用 destroy() 函数时没有参数传入,而我们找到的是有参数的,PHP7下起的ThinkPHP在调用有参函数却没有传入参数的情况下会报错,所以我们要选用PHP5而不选用PHP7.


这里handle 可控,那么就接着找 delete 函数。在 ThinkPHP/Mode/Lite/Model.class.php 的 Model 类中找到了合适的函数,当然选用 /ThinkPHP/Library/Think/Model.class.php 中的该函数也是可以的。我们的目的就是进入 $this->delete($this->data[$pk])。所以这里只截取了前面部分的代码。


我们想要调用这个if中的 delete ,就要使得我们传入的 $options 为空,且 $this->options['where'] 为空,是可控的,所以走到第二个if,$this->data 不为空,且 $this->data[$pk] 存在,满足条件就可以调用 delete($this->data[$pk]) 了。而 $pk 就是 $this->pk ,都是可控的。

之前因为 destroy() 调用时没有参数,使得调用 delete 函数参数部分可控,而现在我们正常带着参数进入了 delete 函数,就可以接着往下走了。直到运行至 $result = $this->db->delete($options);,调用了ThinkPHP数据库模型类中的 delete() 方法。

这里的 $table 是取自传入的参数,可控,直接拼接到 $sql 中,然后传入了 $this->execute。


接着调用 $this->initConnect(true);,随后是 $this->connect() ,这里是用 $this->config 来初始化数据库的,然后去执行先前拼接好的SQL语句。


所以POP链就出来了:





注释注入

触发注释注入的调用为:$user = M('user')->comment($id)->find(intval($id));。

调试跟进一下,调用的是 Think\Model.class.php 中的 comment


之后调用 Think\Model 的find方法。一直到调用了 Think\Db\Driver.class.php 中的 parseComment 函数,将我们输入的内容拼接在了注释中,于是我们可以将注释符闭合,然后插入SQL语句。此时的SQL语句为 "SELECT * FROMuserWHEREid= 1 LIMIT 1 /* 1 */"


如果这里没有 LIMIT 1 的话我们可以直接进行union注入,但是这里有 LIMIT 1 ,进行union注入会提示 Incorrect usage of UNION and LIMIT,只有同时把union前的SQL查询语句用括号包起来才可以进行查询,但是显然我们无法做到,那么我们可以利用 into outfile 的拓展来进行写文件。


?id=1*/ into outfile "path/1.php" LINES STARTING BY ''/* 就可以进行写马了。


exp注入

触发exp注入的查询语句如下。


这里一路跟进到 parseSql() 函数,然后调用到 parseWhere() 。


parseWhere() 调用了 parseWhereItem() ,截取了部分关键代码,这里的 $val 就是我们传入的参数,所以当我们传入数组时,$exp 就是数组的第一个值,如果等于exp,就会使用.直接将数组的第二个值拼接上去,就会造成SQL注入。


也就是说当我们传入 ?id[0]=exp&id[1]== 1 and updatexml(1,concat(0x7e,user(),0x7e),1) 时,拼接后的字符串就是 "`id` = 1 and updatexml(1,concat(0x7e,user(),0x7e),1)",最后的SQL语句也就成了 "SELECT * FROM `user` WHERE `id` =1 and updatexml(1,concat(0x7e,user(),0x7e),1) LIMIT 1 ",可以进行报错注入了。

这里使用了全局数组 $_GET 来传参,而不是tp自带的 I() 函数,是因为在 I() 函数的最后有这么一句代码,


调用了 think_filter() 函数来进行过滤,刚好就过滤了 EXP ,在后面加上了一个空格,那么自然也就无法进行上面的流程,不能进行注入了。



 bind注入


payload:?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1

这里一路执行到上面的 parseWhereItem() 处,除了exp外,还有一处bind,这里同样也是用点拼接字符串,但是不同的是这里还拼接了一个冒号。也就是说拼接之后是 "`id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)" 这样的。


拼接到SQL语句后是 "UPDATE `user` SET `password`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)"。

随后在 update() 中调用了 execute() 函数,执行了如下代码


这里就将 :0 替换为了我们传入的password的值,SQL语句也就变为了 "UPDATE `user` SET `password`='1' WHERE `id` = '1' and updatexml(1,concat(0x7e,user(),0x7e),1)",所以我们在传参的时候 id[1] 最开始的字符传入的是0,才能去除掉冒号。最后SQL注入成功。


变量覆盖导致命令执行

触发rce的代码如下。


先调用 assign() 函数。


当我们传入 ?name=_content&from= 时经过 assign() 函数后就有:$this->view->tVar["_content"]=""

display() 函数跟进,$content 获取模板内容。


这里调用了 fetch() 函数,有一个if判断,如果使用了PHP原生模板就进入这个判断,这个就对应的是 ThinkPHP\Conf\convention.php 中的 'TMPL_ENGINE_TYPE' => 'php',。


这里进入判断后,执行了 extract($this->tVar, EXTR_OVERWRITE); ,而通过前面的分析得知我们已有 $this->view->tVar["_content"]="" ,因此这里就存在变量覆盖,将 $_content 覆盖为了我们输入的要执行的命令。

随后执行 empty($_content)?include $templateFile:eval('?>'.$_content); ,此时的 $_content 显然不为空,所以会执行 eval('?>'.$_content); ,也就造成了命令执行。


【免费领取网络安全学习资料】

你可能感兴趣的:(【安全漏洞】ThinkPHP 3.2.3 漏洞复现)