首页 代码审计

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内置的根命名对应有关,如下图所示,
68429-5u86nhels3j.png

然后有关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方法入手,路由检测处下断点(此处主要用于获取当前请求的相关信息,如模块、控制器、操作等)
84784-i8komyd3h8.png
跟进routeCheck()方法
56733-ytamcbjjxmg.png
route_check_cache默认不开启
88862-0lgb12ytjx0i.png
跟进path()方法
01630-upq661extz.png
再跟进pathinfo()方法,这里根据不同的请求方式来获取pathinfo信息,这里没使用PATH_INFO传递路由是因为$_SERVER['PATH_INFO']获取值时,系统会把\自动转换为'/',后面控制器解析会发生问题,不能完整的解析到thinkContainer
35942-yro0x4esaii.png
这里传参使用的兼容模式s=xxx的方式,在获取后使用unset销毁了$_GET数组中的s元素,然后$_GET数组中剩下的只有传入action的参数了,上述红框所示
66416-hweb2qbgxw9.png
通过$_GET['s']来获取pathinfo信息,最后将获取到的pathinfo信息去掉左侧的/然后返回到path()方法
12913-zj38zk5yncp.png
在path()中再去除配置的后缀,这里没有,直接返回index/think\Container/invokefunction到routeCheck()方法
04728-kct3vq630qb.png
回到routeCheck()后,强制路由模式默认为false,进入check()方法进行路由检测
28413-j6g1gsbf66g.png
check()方法内替换了pathinfo的分隔符为|,最后返回的是一个UrlDispatch对象
29747-n6qlwp95xb.png

3.3 路由解析

这部分主要对pathinfo地址进行了解析、验证、格式处理然后赋值,最终获取到相应的模块、控制器、操作名

UrlDispatch实际实例化的为think\route\dispatch\Url
20940-msnq5bh8dln.png
此类继承Dispatch,因此跟进后会进入父类的构造函数对响应数据进行初始化,最后返回一个Dispatch对象到routeCheck()方法,dispatch对象包含的数据如下图所示,最后routeCheck()方法再将dispatch对象返回到APP类的run()方法
52704-a93awr7z4f.png
然后调用init(),即调用Url类下的init方法
27974-sksx6j1jnu.png
init方法下开始路由解析,跟进parseUrl方法
80133-myitvide4z.png
跟进parseUrlPath()方法
07026-cjwod1w8wo6.png
在parseUrlPath()内对路由进行分割 拿到模块、控制器、操作、参数、参数值,这里的url是前面的pathinfo,因为采用的兼容模式(s=xx/xx/xx)用$_GET['s']获取的,所以这里的url没有参数和参数值。这里使用的分隔符为/,所以在payload中的控制器使用\进行分割,确保了控制器的完整
34027-7laxrlnnyeu.png
回到parseUrl(),后面就是解析模块、控制器、操作、参数、参数值,然后存到$route数组并返回
31309-ktdez7dic3.png
返回后回到Url类的init方法,赋值给$result,然后实例化Module类,此类同样继承Dispatch,所以要先进父类的构造方法__construct,对参数赋初始值,就不截图了
75334-l7133pkykyj.png
直接跟进到Module类的init方法,前半部分是对模块进行简单处理,并判断模块是否可用,可用则置$available = true。这里得使用存在且不在deny_module_list内的模块,算是这个漏洞利用的一个小前置条件,index这种一般就存在且可用。
68757-y9wxn8hhr9.png
后半部分是模块初始化,以及对控制器、操作名的处理以及获取。最后返回当前Module类的实例化的对象
09289-5u2top82imn.png
然后这一波一直返回,就回到了App类中,赋值给$dispatch,到这里前面那些获得的模块、控制器、操作等都在dispatch中了
01543-6dy83py6hcs.png
继续跟进到App类中,下图所示这部分对此处漏洞成因影响不大,直接跳过
22047-gmzv8xd975i.png

3.4 路由调度

然后就到了比较关键的部分了,这里自动加载了Middleware类(中间件),这玩意儿我搜了下,说是用于拦截或过滤应用的HTTP请求,并进行必要的业务处理,根据网上的说法是在闭包函数内在Dispatch类中的run()方法中进行路由调度。
然后这部分会开始对控制器的实例化
97927-2wbrzuvvflb.png
这里调用了的middleware类的add方法,传入的参数是一个闭包函数

function (Request $request, $next) use ($dispatch, $data) {
            return is_null($data) ? $dispatch->run() : $data;
        }

跟进add()方法,这里是将闭包函数注册为中间件,然后赋值给了$this->queue['route'][]
40039-4i7pfnvmzbq.png
然后再回到APP类调用middleware的dispatch()方法
21899-n95lt3ug4c.png
跟进dispatch方法,这里面又调了resolve()方法,跟进resolve
55000-78masugx6mc.png
这里面会再次回调之前的那个闭包函数
42649-5muio3m7n64.png
最后就是回到APP类的那个闭包函数中执行$dispatch->run()
29669-h1ijpn064h.png
从这里就是开始真正滴路由调度了
跟进run()方法,前面的似乎对漏洞成因没啥影响,跳过
61055-1b6kqvwj9aw.png
跟进exec()方法,在这个方法下会对控制器进行实例化
41965-ipzm77u8ow.png
终于开始进入正题了,不容易啊
跟进controller()方法
48683-0l0jq460488.png
看下控制器的具体实例过程,跟进parseModuleAndClass(),这个方法用来获得控制器的命名空间路径,因为控制器为think\container所以进入了第一个条件判断
获得$class = "think\container", $module = "index"
27235-i1aju74307i.png


如果使用的s=index/index/hello那么这里会进入$class = $this->parseClass($module, $layer, $name, $appendSuffix);来获得控制器具体的命名空间路径
09730-h0nv3egp0s.png
然后会对控制器的类的命名空间进行拼接,这里就拼接成了app\index\controller\Index


回到原题,返回$module和$class后回到controller()方法,红框内就是用来实例化类think\container
94682-zuqut17e72s.png
这里条件判断中会先使用class_exists()函数检查thinkcontainer类是否定义过,class_exists() 也会触发自动加载函数。跟进__get(),这里会调用$this->make("thinkcontainer")进行具体的实例化了
73836-iv6rk9pfqzp.png
跟进make(),此方法用于创建类的实例,进入else分支
11755-rroorkicy3o.png

再跟进invokeClass(),这里使用反射类实例化think\container
74567-pki6dm631o8.png
上面返回后回到make方法,赋值给$object,然后再返回回到exec()方法,赋值给$instance
79601-vi2npnf1nsg.png
如图$instance的值
46994-vl2ewbjn18.png
紧接着在exec()方法中调用$this->app['middleware']->controller(),传参是一个巨长的闭包函数,这里先不贴图,先看下controller()方法,在方法里同样调用了中间件类下的add方法,将匿名函数注册控制器中间件
30682-d53fqz385gh.png
在exec()方法末尾同样调用中间件类的display方法$this->app['middleware']->dispatch($this->request, 'controller');
77861-6koorabgr4a.png
和上面一样的味儿,直接看匿名函数了,先看前半部分,这里会进入$this->request->param()获取操作的传入参数(如果开启ThinkPHP的debug后,则会在最前面APP类中,完成路由解析后进行一次调用,用来记录相关参数、请求头、请求的路由进行记录)
66120-ky6iqfa7fa.png
$this->rule->getConfig('url_param_type')默认为0,所以会进入param()方法
53360-2zimufg7o4p.png
跟进param(),获取当前请求的参数
92482-ac1t2xhg7im.png
这些以请求方式获取参数的方法中会都会调用input方法,input方法中会调用过滤函数进行过滤,因为上述调用参数获取方法传了参数name=false所以不会进入到过滤那一步,而且也没指定过滤函数
42656-wd4bl1fp4o.png
16758-x2fi67yz18q.png
最后返回到exec方法中将参数赋给$vars,后半部分就是调用invokeReflectMethod()调用反射执行类的方法(传参为think\Container实例化对象$instance、invokefunction方法的反射类$reflect、invokefunction方法的参数$vars),然后是调用autoResponse对结果输出了
15935-oiowyc84iab.png

跟进invokeReflectMethod()方法,将$vars参数与$reflect绑定(即将invokefunction方法与其参数绑定好)
66762-4ha7yj23b7h.png
最后调用$reflect->invokeArgs($instance, $args),相当于反射调用think\Container下的invokefunction方法了,所以这里跟进到invokeArgs就直接到了invokefunction方法,到这就成了
04871-l04ks8gurum.png
ThinkPHP其他核心类库下的方法应该是都可以此方法调用,还存在好几处可利用点,如:?s=index/think\request/input?data[]=phpinfo()&filter=assert



文章评论

评论已关闭

目录