Thinkphp 5.1 启动流程浅析/控制器未过滤RCE[详细长文]
Abstract
此文较为详细的动态调试并分析了Thinkphp5.1的路由分发以及执行调度流程,通俗点说就是Thinkphp5.1是如何处理请求的。漏洞分析部分网上资料较多,故较为简略。网上Thinkphp5.1的源码分析较少且不完整,希望能给研究框架/漏洞的朋友提供一点小小的帮助。
Thinkphp5.1 启动流程浅析
Thinkphp5.1对底层架构做了进一步的改进,减少依赖,主要就是新增了容器(Container)、门面(Facade)、中间件(Middleware)三大块功能。
容器(Container)的功能,一句话概括就是管理对象实例,缓存已创建的实例。中间件(Middleware)如同字面意思一样,就是中间件。门面(Facade)的功能暂时不关注。
Thinkphp5.1对比Thinkphp5.0,个人感觉比较明显的差别有两点,一是使用Container来进行实例管理,调用都是动态的,非常灵活。二是在Think\App中取消了exec()方法,在Middleware中进行相关的逻辑。 这里分析的版本是Thinkphp 5.1.10,最新版本是5.1.35,基本架构一样,有一些细节大同小异。
建议阅读这篇文章 thinkphp5.1新特性 作为基础知识。
Dynamic Debugging
Container调用过程
public/index.php
5.0.x包含start.php然后静态调用App::run()方法,这里动态调用App::run方法。
thinkphp/library/think/Container.php
获取容器中的对象实例。
thinkphp/library/think/Container.php
获取容器中的实例,从这里可以看出这里是个单例模式,也就是说内存中只有一个$instance
存在。这里的new static
是静态延迟绑定。
可以看到static::$instance
中的bind属性存在很多别名。
thinkphp/library/think/Container.php
创建新的实例,读取$instance
中缓存的实例内容,如果没有则创建,创建完成后就会存到$isntance
中。由于$instance
中没有App实例,所以需要创建新的App实例。获取到命名空间后再调用make进入invokeClass逻辑。
thinkphp/library/think/Container.php
通过反射实例化think\App并返回。
thinkphp/library/think/Container.php
App的构造方法中获取Container的实例,为了动态调用相应属性(见后文)。
获取相应实例并返回,将$object
放入$instances
属性中。至此Container的动态调用过程已经完成,简单总结一下,Container通过别名和命名空间的绑定,反射调用相应的类并进行实例化。
路由分发
进入think\App->run()的逻辑,代码比较长,分块来看。
跟进路由检测。
导入路由配置并且进行定义后,调用$this->route
,但是此时$this
对象中并没有route属性。
于是触发think\App->__get()方法,通过Container来获取route类的实例。这就是为什么前面将Container的实例赋给$this->container
属性
返回后跟进think\Route->check()。
触发SPL自动加载,加载UrlDispatch类,并调用构造函数。
由于UrlDispatch继承了Dispatch抽象类,所以先进行Dispatch初始化,然后调用think\route\dispatch\Url->init()进行初始化。
从think\App->check()中传进来的$path
在这里进行正式解析,也就是url中s参数。
跟进解析函数parseUrl(),可以看到详细的解析过程,代码比较长,比较简单,就不再贴了。由于我这里是默认index的URL所以返回空。
实例化Moudle,在构造函数中调用$this->init()
进行初始化。
初始化就完成了,该设置的已经设置好了,该加载的也加载完了,就等后面的调用了。
接下来回到think\App的逻辑中,将调度信息保存在request实例(__get调用Container加载)的dispatch属性中。再检查是否存在缓存。然后进入中间件流程。进入相应的控制器进行执行。
执行调度
Thinkphp5.1把路由相应的逻辑处理放在了中间件中。关于中间件的源码分析请看这里。
跟进middleware的add中的参数是一个闭包,也就是PHP的Closure。PHP中的闭包是通过Closure对象来传递的。跟进Add看一下详情。
将传入的闭包进行处理然后放入队列中,此时$middleware
变量如图。$middleware
中的值为闭包对象,static为传入的参数,因为使用use,所以传入了父作用域的参数。parameter中注明了闭包定义的参数是否是必须的,同时传入this变量,指向父作用域。跟进$this->buildMiddleware
。
如果为闭包的话就返回一个数组,数组中的值为$middleware
和$param
。返回think\App跟进$this->mdiddleware->dispatch()
。
调用$this->resolve()。
返回一个闭包,调用刚才$middleware
中的闭包。可以看见,如果传入的闭包还有$next
变量的话,会继续调用下一个闭包此处为一个洋葱结构。
回到think\App的闭包中。
跟进,上面分析了,$dispatch
为UrlDispatch实例,这个实例中存储的dispatch属性是一个Moudle对象。
thinkphp/library/think/route/dispatch/Url.php
再跟进,进入Module,在Module的run方法中,调用所需要的控制器和方法。
thinkphp/library/think/route/dispatch/Module.php
前面都分析清楚地话这里就很简单了,不再赘述了。
封装的通过反射调用相应类的方法。
bingo!!!调用到相应的控制器。
漏洞分析
其实前面的调用过程分析清楚后就不难发现漏洞出在哪里了,在路由分发的时候获取url的s参数,获取到相应的模块和方法并且设置到Moudle对象中,在中间件的调用流程中直接通过反射进行调用。在这个过程中没有任何的过滤,也就是意味着可以通过前台s参数调用任意的控制器并且实例化,从而达到任意代码执行。
其实能使用的Exp非常多,只需要找一下就可以了,此处网上非常多的资料,也就不再赘述了。我通过调试代码发现,其实在路由分发的时候不一定必须使用s参数,比如如图这样,也可以调用相应的控制器方法,原理就不再赘述了,调一下代码就能看得出来。
这里就没有必要去fuzz了,遇到不同版本找到相应的代码看一下就能知道是否可用,找到相应的Exp,或者构造符合情况的Exp。只要理解了Thinkphp的启动流程以及相应的细节,这些都比较简单。Thinkphp5.0的启动流程以及路由分发要比Thinkphp5.1简单的多,相应情况相应分析就好。
Conclusion
从Thinkphp中学到了使用闭包可以实现灵活的中间件加载(通过闭包也可以实现IoC控制反转),通过别名绑定命名空间的方式可以实现像Java静态代理一样的功能,通过__get方法可以实现动态代理,并且PHP也有反射调用。
PHP大部分框架的设计思路都是差不多,写完此篇,对于PHP有新的感觉。