whip1ash

Thinkphp 5.1 启动流程浅析/控制器未过滤RCE[详细长文]

2019-04-03

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
-w561
5.0.x包含start.php然后静态调用App::run()方法,这里动态调用App::run方法。

thinkphp/library/think/Container.php
-w853
获取容器中的对象实例。

thinkphp/library/think/Container.php
-w583
获取容器中的实例,从这里可以看出这里是个单例模式,也就是说内存中只有一个$instance存在。这里的new static静态延迟绑定

可以看到static::$instance中的bind属性存在很多别名。

thinkphp/library/think/Container.php
-w809
创建新的实例,读取$instance中缓存的实例内容,如果没有则创建,创建完成后就会存到$isntance中。由于$instance中没有App实例,所以需要创建新的App实例。获取到命名空间后再调用make进入invokeClass逻辑。

thinkphp/library/think/Container.php
-w1076
通过反射实例化think\App并返回。

thinkphp/library/think/Container.php
-w1112
App的构造方法中获取Container的实例,为了动态调用相应属性(见后文)。

获取相应实例并返回,将$object放入$instances属性中。至此Container的动态调用过程已经完成,简单总结一下,Container通过别名和命名空间的绑定,反射调用相应的类并进行实例化。

路由分发

进入think\App->run()的逻辑,代码比较长,分块来看。
-w1112

跟进路由检测。
-w1156

导入路由配置并且进行定义后,调用$this->route,但是此时$this对象中并没有route属性。

于是触发think\App->__get()方法,通过Container来获取route类的实例。这就是为什么前面将Container的实例赋给$this->container属性
-w826

返回后跟进think\Route->check()。
-w1125

触发SPL自动加载,加载UrlDispatch类,并调用构造函数。
-w1026

由于UrlDispatch继承了Dispatch抽象类,所以先进行Dispatch初始化,然后调用think\route\dispatch\Url->init()进行初始化。
-w911
从think\App->check()中传进来的$path在这里进行正式解析,也就是url中s参数。
-w498
跟进解析函数parseUrl(),可以看到详细的解析过程,代码比较长,比较简单,就不再贴了。由于我这里是默认index的URL所以返回空。
-w919
实例化Moudle,在构造函数中调用$this->init()进行初始化。
-w1175
-w1252
-w1251

初始化就完成了,该设置的已经设置好了,该加载的也加载完了,就等后面的调用了。

接下来回到think\App的逻辑中,将调度信息保存在request实例(__get调用Container加载)的dispatch属性中。再检查是否存在缓存。然后进入中间件流程。进入相应的控制器进行执行。

执行调度

Thinkphp5.1把路由相应的逻辑处理放在了中间件中。关于中间件的源码分析请看这里

-w1205
跟进middleware的add中的参数是一个闭包,也就是PHP的Closure。PHP中的闭包是通过Closure对象来传递的。跟进Add看一下详情。
-w673
将传入的闭包进行处理然后放入队列中,此时$middleware变量如图。
-w372
$middleware中的值为闭包对象,static为传入的参数,因为使用use,所以传入了父作用域的参数。parameter中注明了闭包定义的参数是否是必须的,同时传入this变量,指向父作用域。跟进$this->buildMiddleware
-w942
如果为闭包的话就返回一个数组,数组中的值为$middleware$param。返回think\App跟进$this->mdiddleware->dispatch()
-w727
调用$this->resolve()。
-w1259
返回一个闭包,调用刚才$middleware中的闭包。可以看见,如果传入的闭包还有$next变量的话,会继续调用下一个闭包此处为一个洋葱结构。

回到think\App的闭包中。
-w1032
跟进,上面分析了,$dispatch为UrlDispatch实例,这个实例中存储的dispatch属性是一个Moudle对象。

thinkphp/library/think/route/dispatch/Url.php
-w878
再跟进,进入Module,在Module的run方法中,调用所需要的控制器和方法。

thinkphp/library/think/route/dispatch/Module.php
-w1116
-w1283
前面都分析清楚地话这里就很简单了,不再赘述了。
-w869
封装的通过反射调用相应类的方法。

-w1304
bingo!!!调用到相应的控制器。

漏洞分析

其实前面的调用过程分析清楚后就不难发现漏洞出在哪里了,在路由分发的时候获取url的s参数,获取到相应的模块和方法并且设置到Moudle对象中,在中间件的调用流程中直接通过反射进行调用。在这个过程中没有任何的过滤,也就是意味着可以通过前台s参数调用任意的控制器并且实例化,从而达到任意代码执行。
其实能使用的Exp非常多,只需要找一下就可以了,此处网上非常多的资料,也就不再赘述了。我通过调试代码发现,其实在路由分发的时候不一定必须使用s参数,比如如图这样,也可以调用相应的控制器方法,原理就不再赘述了,调一下代码就能看得出来。

这里就没有必要去fuzz了,遇到不同版本找到相应的代码看一下就能知道是否可用,找到相应的Exp,或者构造符合情况的Exp。只要理解了Thinkphp的启动流程以及相应的细节,这些都比较简单。Thinkphp5.0的启动流程以及路由分发要比Thinkphp5.1简单的多,相应情况相应分析就好。

Conclusion

从Thinkphp中学到了使用闭包可以实现灵活的中间件加载(通过闭包也可以实现IoC控制反转),通过别名绑定命名空间的方式可以实现像Java静态代理一样的功能,通过__get方法可以实现动态代理,并且PHP也有反射调用。
PHP大部分框架的设计思路都是差不多,写完此篇,对于PHP有新的感觉。

Tags: PHP

扫描二维码,分享此文章