1 影响范围
ThinkPHP 5.0.5 - 5.0.22
ThinkPHP 5.1.0 - 5.1.31
2 实验环境
ThinkPHP 5.1.20
php 5.6.27
3 漏洞分析
3.1 前置知识
先看下payload:?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
payloa采用的兼容模式(即s=xx/xx/xx),tp正常解析的路径是application下的模块、控制器、操作,而这里的模块为:index,控制器为:thinkContainer,操作为:invokefunction。
此处控制器对应类文件路径为thinkphp/library/think/Container.php
,和thinkphp内置的根命名对应有关,如下图所示,
然后有关thinkphp自动加载:
如存在文件路径application/index/controller/xxx.php
,内容为:
namespace app\index\controller
class xxx{
}
那么在实例化时候可直接使用$class = new \app\index\controller\xxx();
系统会自动加载xxx类对应路径的类文件application/index/controller/xxx.php
3.2 路由检测
这部分主要是用于获取pathinfo
使用paylaod下断点进调试看下路由解析以及调度流程
跳过前面注册系统的自动加载方法及相应文件加载,直接从入口文件实例化的thinkphp/library/think/App.php
类下的run方法入手,路由检测处下断点(此处主要用于获取当前请求的相关信息,如模块、控制器、操作等)
跟进routeCheck()方法
route_check_cache默认不开启
跟进path()方法
再跟进pathinfo()方法,这里根据不同的请求方式来获取pathinfo信息,这里没使用PATH_INFO传递路由是因为$_SERVER['PATH_INFO']获取值时,系统会把\
自动转换为'/',后面控制器解析会发生问题,不能完整的解析到thinkContainer
这里传参使用的兼容模式s=xxx的方式,在获取后使用unset销毁了$_GET数组中的s元素,然后$_GET数组中剩下的只有传入action的参数了,上述红框所示
通过$_GET['s']来获取pathinfo信息,最后将获取到的pathinfo信息去掉左侧的/然后返回到path()方法
在path()中再去除配置的后缀,这里没有,直接返回index/think\Container/invokefunction
到routeCheck()方法
回到routeCheck()后,强制路由模式默认为false,进入check()方法进行路由检测
check()方法内替换了pathinfo的分隔符为|,最后返回的是一个UrlDispatch对象
3.3 路由解析
这部分主要对pathinfo地址进行了解析、验证、格式处理然后赋值,最终获取到相应的模块、控制器、操作名
UrlDispatch实际实例化的为think\route\dispatch\Url
类
此类继承Dispatch,因此跟进后会进入父类的构造函数对响应数据进行初始化,最后返回一个Dispatch对象到routeCheck()方法,dispatch对象包含的数据如下图所示,最后routeCheck()方法再将dispatch对象返回到APP类的run()方法
然后调用init(),即调用Url类下的init方法
init方法下开始路由解析,跟进parseUrl方法
跟进parseUrlPath()方法
在parseUrlPath()内对路由进行分割 拿到模块、控制器、操作、参数、参数值,这里的url是前面的pathinfo,因为采用的兼容模式(s=xx/xx/xx)用$_GET['s']获取的,所以这里的url没有参数和参数值。这里使用的分隔符为/
,所以在payload中的控制器使用\
进行分割,确保了控制器的完整
回到parseUrl(),后面就是解析模块、控制器、操作、参数、参数值,然后存到$route数组并返回
返回后回到Url类的init方法,赋值给$result,然后实例化Module类,此类同样继承Dispatch,所以要先进父类的构造方法__construct,对参数赋初始值,就不截图了
直接跟进到Module类的init方法,前半部分是对模块进行简单处理,并判断模块是否可用,可用则置$available = true。这里得使用存在且不在deny_module_list内的模块
,算是这个漏洞利用的一个小前置条件,index这种一般就存在且可用。
后半部分是模块初始化,以及对控制器、操作名的处理以及获取。最后返回当前Module类的实例化的对象
然后这一波一直返回,就回到了App类中,赋值给$dispatch,到这里前面那些获得的模块、控制器、操作等都在dispatch中了
继续跟进到App类中,下图所示这部分对此处漏洞成因影响不大,直接跳过
3.4 路由调度
然后就到了比较关键的部分了,这里自动加载了Middleware类(中间件),这玩意儿我搜了下,说是用于拦截或过滤应用的HTTP请求,并进行必要的业务处理,根据网上的说法是在闭包函数内在Dispatch类中的run()方法中进行路由调度。
然后这部分会开始对控制器的实例化
这里调用了的middleware类的add方法,传入的参数是一个闭包函数
function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
}
跟进add()方法,这里是将闭包函数注册为中间件,然后赋值给了$this->queue['route'][]
然后再回到APP类调用middleware的dispatch()方法
跟进dispatch方法,这里面又调了resolve()方法,跟进resolve
这里面会再次回调之前的那个闭包函数
最后就是回到APP类的那个闭包函数中执行$dispatch->run()
从这里就是开始真正滴路由调度了
跟进run()方法,前面的似乎对漏洞成因没啥影响,跳过
跟进exec()方法,在这个方法下会对控制器进行实例化
终于开始进入正题了,不容易啊
跟进controller()方法
看下控制器的具体实例过程,跟进parseModuleAndClass(),这个方法用来获得控制器的命名空间路径,因为控制器为think\container
所以进入了第一个条件判断
获得$class = "think\container", $module = "index"
如果使用的s=index/index/hello
那么这里会进入$class = $this->parseClass($module, $layer, $name, $appendSuffix);来获得控制器具体的命名空间路径
然后会对控制器的类的命名空间进行拼接,这里就拼接成了app\index\controller\Index
回到原题,返回$module和$class后回到controller()方法,红框内就是用来实例化类
think\container
了
这里条件判断中会先使用class_exists()函数检查thinkcontainer类是否定义过,class_exists() 也会触发自动加载函数。跟进__get(),这里会调用$this->make("thinkcontainer")进行具体的实例化了

跟进make(),此方法用于创建类的实例,进入else分支

再跟进invokeClass(),这里使用反射类实例化think\container
上面返回后回到make方法,赋值给$object,然后再返回回到exec()方法,赋值给$instance
如图$instance的值
紧接着在exec()方法中调用$this->app['middleware']->controller(),传参是一个巨长的闭包函数,这里先不贴图,先看下controller()方法,在方法里同样调用了中间件类下的add方法,将匿名函数注册控制器中间件
在exec()方法末尾同样调用中间件类的display方法$this->app['middleware']->dispatch($this->request, 'controller');
和上面一样的味儿,直接看匿名函数了,先看前半部分,这里会进入$this->request->param()获取操作的传入参数(如果开启ThinkPHP的debug后,则会在最前面APP类中,完成路由解析后进行一次调用,用来记录相关参数、请求头、请求的路由进行记录)
$this->rule->getConfig('url_param_type')默认为0,所以会进入param()方法
跟进param(),获取当前请求的参数
这些以请求方式获取参数的方法中会都会调用input方法,input方法中会调用过滤函数进行过滤,因为上述调用参数获取方法传了参数name=false所以不会进入到过滤那一步,而且也没指定过滤函数
最后返回到exec方法中将参数赋给$vars,后半部分就是调用invokeReflectMethod()调用反射执行类的方法(传参为think\Container实例化对象$instance、invokefunction方法的反射类$reflect、invokefunction方法的参数$vars),然后是调用autoResponse对结果输出了
跟进invokeReflectMethod()方法,将$vars参数与$reflect绑定(即将invokefunction方法与其参数绑定好)
最后调用$reflect->invokeArgs($instance, $args),相当于反射调用think\Container下的invokefunction
方法了,所以这里跟进到invokeArgs就直接到了invokefunction方法,到这就成了
ThinkPHP其他核心类库下的方法应该是都可以此方法调用,还存在好几处可利用点,如:?s=index/think\request/input?data[]=phpinfo()&filter=assert
评论已关闭