0x01 前言

继续分析 thinkphp 框架,本次分析的是 thinkphp 5.1版本的rce漏洞分析。

本篇文章也总结了两种POC,与thinkphp 5.0 也有部分联系。

0x02 thinkphp 路由调度分析

我们知道,thinkphp不是基于文件访问,而是基于路由。所以首先要弄清楚thinkphp是怎么实现路由调度的。

thinkphp规定了两种url访问模式,PATHINFO模式和兼容模式。

例如控制器中编写这样的一个方法

URL使用PATHINFO模式访问

1.就是控制器所在的模块名,think就是一个根命名空间

2.就是think类库下名为Index的控制器

3.4和5所对应的就是方法名,参数以及参数值。

再看看URL用兼容模式访问,其实都大同小异

就多了一个s参数,这个参数在上篇分析thinkphp5.0也遇到过,是config类中定义的默认参数。

代码分析

使用代码调试分析thinkphp的路由调度

在入口文件处下断点,开始调试

$dispatch默认为空,随即开始路由检测

获取url路径信息,继续跟进到pathinfo方法

我URL使用的是兼容模式,通过$GET全局变量获取到路径信息,随后return返回到APP.php文件

通过获取到的路径信息对路由进行检测

然后开始路由匹配,由于我们没有定义路由规则,所以会走到默认路由解析这里

开始解析url规则

其实就是对url进行分类,对应包名,控制器,方法,参数等等分别检测,然后返回解析好的url参数

接下来会实例化Module类,也是对这些参数进行格式化处理

回到APP类之后就开启了路由调度

跟进这个add方法,这个方法做了一个闭包函数当作参数传入,然后存入 queue 数组中

$middleware 就是一个封装着闭包函数的类,然后执行middleware类中的dispatch()方法

继续跟进 resolve 方法

这时候将闭包函数赋值给了$call属性,然后会继续回调函数,闭包函数注册为中间件,中间件的作用官方介绍说主要是用于拦截或过滤应用的HTTP请求,并进行必要的业务处理。所以可以推测这里是为了调用闭包函数中的run()方法,进行路由调度业务。返回APP类调用 $dispatch->run()

跟进exec方法后,根据url的参数创建控制器实例,通过控制器实例获取方法名,获取变量参数等等

然后会执行 invokeReflectMethod 方法进行函数调用,跟进之后发现已经调用到了控制器方法

然后将执行结果作为响应返回给客户端,最后一段不再继续跟了。

0x03 漏洞分析

POC1

首先我们需要找到能够函数调用,命令执行的危险函数,例如 call_user_func,或者call_user_func_array()等等

而在/thinkphp/library/think/Container.php文件中就有 call_user_func_array 函数

这个文件不在我们的think模块内,我们能否像调用控制器方法那样

我们尝试按照上面写的url规则去访问

aaa/Container/invokefunction (这个类不是think模块里的,但是我们也不知道Container属于什么模块,所以就随便写)

发起url请求看看报错

调试后执行流会走到这个报错,这里 $available 为false,我们需要其满足为true

在以上三种情况中 $available 可以赋值为true,在默认配置下,由于没有路由绑定,所以$bind为null,empty_module默认模块也没有定义,所以有机会实现的也只有第二个if分支了。

来分析一下这个if条件,需要满足两个条件

第一:$module 不能存在于 deny_module_list 这个黑名单中,跟进 getConfig 函数看一下

就一个common,目前是比较好满足的。

第二:module 于路径拼接后要是一个目录,跟进 getAppPath 函数看一下

大体意思就是application工作目录下是否存在 $module 目录,源码中该工作目录下只有index。所以目前来看,想要绕过报错也只能 $module 为index

继续往后看,当获取到类名,模块名,方法名后 就开始创造类的实例了

首先对其进行解析,构造命令空间,如果 $name 中有反斜杠会将其作为类的命名空间路径,通常情况下,我们需要进入到 parseClass 函数对模块与类名进行拼接,构造命名空间,而这种方法摆脱了模块的限制

例如我们需要调用的恶意类是 Container,它的命令空间为

该方法传递了两个参数

构造poc

?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

可以成功执行命令

因为解析路由的时候是以 / 解析路由,所以解析后的控制器是 think\container,在构造命令空间的时候

think就会被当作命名空间,container类就会成功实例化,随后就是获取方法名等操作。

这样的话我们就可以实例化或者调用任意类的public方法

poc2

危险函数不止一处,在之前分析thinkphp 5.0.x 的rce漏洞分析中,在Request类中 filterValue 方法也存在 call_user_func 函数

这个类的命名空间也是 think,那么就可以任意函数调用了。但是调用的方法不能是 filterValue 方法。因为修饰属性不是public,所以需要往上一层找

最终找到input函数,追溯参数

我们需要控制的就是data和filter参数,那么编写的poc就是

?s=index/think\Request/input&filter=system&data=whoami

也是可以成功执行命令的。

0x04 结语

到此也算是对thinkphp有了初步的认识,在后续的时间里会开始对其他php框架做一个漏洞的代码审计分析。